Реализация класса String
Я работаю над проектом (это язык), и для этого проекта я решил использовать низкоуровневый стиль C ++, так что это означает создание моих собственных типов данных. Недавно я закончил создание собственной реализации строкового класса. На самом деле это первый раз, когда я создал строковый класс раньше, поэтому, возможно, я делаю много чего неправильно. Насколько мне известно, я тестировал его, и он работает и делает то, для чего был предназначен, но я могу что-то упустить, или что-то может быть не лучшей практикой.
Моей целью здесь было создать строковый класс «низкоуровневого C ++», то есть я бы создал все сам, без использования каких-либо заголовков.
У меня все в одном .hфайле, и я знаю, что это не лучший способ сделать это, но я не очень люблю разбивать свой код, когда это всего лишь небольшой класс.
Вот обзор методов и того, что они делают, и почему я выбрал их (обратите внимание, что это не фактический .hфайл, я просто показываю это, чтобы предоставить некоторый контекст и обзор того, что я делаю):
class string
{
public:
string();
string(const char* buffer);
string(const string& buffer);
~string();
public:
string& operator=(const char* buffer);
string& operator=(const string& buffer);
void operator+=(char buffer);
void operator+=(const char* buffer);
void operator+=(const string& buffer);
bool operator==(const char* buffer) const;
bool operator==(const string& buffer) const;
bool operator!=(const char* buffer) const;
bool operator!=(const string& buffer) const;
char operator[](int index) const;
char& operator[](int index);
public:
int length() const;
// returns the actual string
const char* get() const;
private:
int str_len(const char* buffer) const;
// given a block of memory 'dest', fill that with characters from 'buffer'
void str_cpy(char* dest, int dest_size, const char* buffer);
void str_cpy(char* dest, int dest_size, const string& buffer);
// allocate a given size of memory
char* str_alc(int size);
private:
int size;
char* str;
};
Как видите, в этом нет ничего особенного, просто некоторые базовые функции, которых должно хватить для моего проекта. Несколько комментариев к коду:
Я решил добавить get()метод вместо чего-то подобного, operator const char*()поскольку мне кажется, что перегрузки оператора будет достаточно, и я хочу сделать доступ к фактической строке более явным.
Также обратите внимание на частные методы, они в основном очень похожи на методы, которые можно найти в <string.h>заголовке, например, strncpy()и str_len().
Вот фактический string.hфайл:
#pragma once
namespace night { // 'night' is the project I'm working on
class string
{
public:
string()
{
size = 0;
str = str_alc(1);
}
string(const char* buffer)
{
size = str_len(buffer);
str = str_alc(size + 1);
str_cpy(str, size + 1, buffer);
}
string(const string& buffer)
{
size = buffer.size;
str = str_alc(size + 1);
str_cpy(str, size + 1, buffer);
}
~string()
{
delete[] str;
}
public:
string& operator=(const char* buffer)
{
delete[] str;
size = str_len(buffer);
str = str_alc(size + 1);
str_cpy(str, size + 1, buffer);
return *this;
}
string& operator=(const string& buffer)
{
delete[] str;
size = buffer.size;
str = str_alc(size + 1);
str_cpy(str, size + 1, buffer);
return *this;
}
void operator+=(char buffer)
{
char* temp = str_alc(size + 2);
str_cpy(temp, size + 2, str);
temp[size] = buffer;
temp[size + 1] = '\0';
delete[] str;
size += 1;
str = temp;
}
void operator+=(const char* buffer)
{
size += str_len(buffer);
char* temp = str_alc(size + 1);
str_cpy(temp, size + 1, str);
str_cpy(temp, size + 1, buffer);
delete[] str;
str = temp;
}
void operator+=(const string& buffer)
{
size += buffer.size;
char* temp = str_alc(size + 1);
str_cpy(temp, size + 1, str);
str_cpy(temp, size + 1, buffer);
delete[] str;
str = temp;
}
bool operator==(const char* buffer) const
{
if (size != str_len(buffer))
return false;
for (int a = 0; a < size; ++a)
{
if (str[a] != buffer[a])
return false;
}
return true;
}
bool operator==(const string& buffer) const
{
return operator==(buffer.str);
}
bool operator!=(const char* buffer) const
{
return !operator==(buffer);
}
bool operator!=(const string& buffer) const
{
return !operator==(buffer.str);
}
char operator[](int index) const
{
if (index < 0 || index >= size)
throw "[error] index is out of range";
return str[index];
}
char& operator[](int index)
{
if (index < 0 || index >= size)
throw "[error] index is out of range";
return str[index];
}
public:
int length() const
{
return size;
}
const char* get() const
{
return str;
}
private:
int str_len(const char* buffer) const
{
int length = 0;
for (int a = 0; buffer[a] != '\0'; ++a)
length += 1;
return length;
}
void str_cpy(char* dest, int dest_size, const char* buffer)
{
int start = 0;
while (dest[start] != '\0')
start += 1;
if (dest_size - start < str_len(buffer))
throw "[fatal error] function 'void str_cpy(char* dest, const char* buffer)' does not have enough space";
for (int a = 0; a < str_len(buffer); ++a)
dest[start + a] = buffer[a];
dest[start + str_len(buffer)] = '\0';
}
void str_cpy(char* dest, int dest_size, const string& buffer)
{
int start = 0;
while (dest[start] != '\0')
start += 1;
if (dest_size - start < buffer.size)
throw "[fatal error] function 'void str_cpy(char* dest, const string& buffer)' does not have enough space";
for (int a = 0; a < buffer.size; ++a)
dest[start + a] = buffer.str[a];
dest[start + buffer.size] = '\0';
}
char* str_alc(int size)
{
char* buffer;
try {
// set the new string to contain null-terminators by default
buffer = new char[size]{ '\0' };
}
catch (...) {
throw "[fatal error] function 'char* str_alc(int size)' cannot allocate enough memory";
}
return buffer;
}
private:
int size;
char* str;
};
} // namespace night
И как пример, вот как вы могли бы его использовать:
int main()
{
night::string test = "class";
test += ' ';
test += "string";
std::cout << test.get() << '\n';
night::string test1 = "string class";
test = test1;
test[0] = 'S';
test[7] = 'C';
std::cout << test.get() << '\n';
night::string test2 = "String Class";
std::cout << (test == test2) << '\n';
std::cout << (test != test2) << '\n';
}
Вот что меня беспокоит в первую очередь:
Нужен ли мне конструктор перемещения и оператор присваивания перемещения? Я знаю, что в этом нет необходимости, но будут ли они иметь большое значение в данном случае?
Эффективны ли частные методы? Можно ли их улучшить?
Метод
str_alc()хорош? Как это хорошая практика - заключатьnewв оператор try-catch? И следует ли мне\0по умолчанию заполнять строку s? Или это приносит больше вреда, чем пользы?
Также у меня небольшой вопрос: bufferправильное ли имя параметра ? Я не совсем уверен, как назвать параметры ...
Любые другие отзывы также приветствуются!
Спасибо!
Ответы
Почему вы преобразовываете исключение, имеющее значение, в бессмысленную строку?
char* str_alc(int size)
{
char* buffer;
try {
// set the new string to contain null-terminators by default
buffer = new char[size]{ '\0' };
}
catch (...) {
throw "[fatal error] function 'char* str_alc(int size)' cannot allocate enough memory";
}
return buffer;
}
То, как сообщается об ошибке, в конечном итоге зависит от того, где она обнаружена. Вы не должны просто перебрасывать строку. Перехватите исключение в момент сообщения об ошибке и преобразуйте его в соответствующее сообщение об ошибке в этот момент. Или выбросьте более значимый тип исключения (не строку).
Кроме того, если вы собираетесь выполнить эту проверку, просто используйте новую версию без метания, а затем проверьте, что буфер не равен нулю, и выбросите новое исключение.
Не изобретайте заново закрывающиеся функции:
int str_len(const char* buffer) const
Для этого уже существует C-функция, и я гарантирую, что она НЕ медленнее, чем ваша версия, и, скорее всего, на порядок быстрее.
void str_cpy(char* dest, int dest_size, const char* buffer)
Опять же, уже есть функции копирования C-String. Если вы собираетесь изобретать их заново, используйте алгоритмы C ++ для копирования байтов, а не вручную записывайте циклы.
Если вы сравниваете два строковых объекта. Вы переходите к сравнению строкового объекта с C-String в качестве наиболее общего случая.
bool operator==(const char* buffer) const
{
if (size != str_len(buffer))
return false;
for (int a = 0; a < size; ++a)
{
if (str[a] != buffer[a])
return false;
}
return true;
}
bool operator==(const string& buffer) const
{
return operator==(buffer.str);
}
bool operator!=(const char* buffer) const
{
return !operator==(buffer);
}
bool operator!=(const string& buffer) const
{
return !operator==(buffer.str);
}
В результате вы вычисляете длину строки для объекта, для которого вы уже знаете длину строки!
Вы реализовали проверенный operator[]:
char operator[](int index) const
{
if (index < 0 || index >= size)
throw "[error] index is out of range";
return str[index];
}
char& operator[](int index)
{
if (index < 0 || index >= size)
throw "[error] index is out of range";
return str[index];
}
В C ++ operator[]обычно не отмечен и используется в ситуациях, когда вы уже установили, что доступ находится в установленных пределах, и, следовательно, проверка является избыточной.
В C ++ мы обычно также предоставляем непроверенную версию, поэтому вам не нужно выполнять ручную проверку. В C ++ мы называем эту версию at().
for(int loop = 0; loop < str. length(); ++loop) {
std::cout << str[loop]; // Why do I need the index
// checked here (every loop)
// I have already established that
// loop is within bounds by checking
// it against the length of the string.
}
Вы не реализовали семантику перемещения.
Вы не реализовали резервный размер. Существует разница между текущей длиной и максимальной длиной до того, как потребуется изменить размер.
Ваш оператор присваивания не безопасен для исключений.
string& operator=(const char* buffer)
{
delete[] str; // you have modified the object here
size = str_len(buffer);
str = str_alc(size + 1); // This can throw. If it does
// your object is in a bad state
// the member str is pointing at
// memory that has been released
// back to the runtime. Any
// use of this will be broken.
//
// You have to hope that that exception
// is not caught and the application
// exits.
str_cpy(str, size + 1, buffer);
return *this;
}
Правильный способ сделать это - реализовать идиому копирования и обмена.
string& operator=(const char* buffer)
{
string tmp(buffer); // safely create a copy.
// Now that you have done the copy swap this with tmp
std::swap(size, tmp.size)
std::swap(buffer, tmp.buffer);
return *this;
}
// destructor of tmp is called here.
// it will release the buffer that you just placed into the object
Версия стандартной библиотеки этого std::stringреализует приятную оптимизацию коротких строк поверх базовой версии распределения динамической памяти, которую вы реализовали.
Я могу что-то упустить, но почему бы и нет std::string? Я не понимаю, почему вы не можете использовать STL или другие библиотеки с открытым исходным кодом. Вы дали какое-то объяснение, но я его не понимаю. Использование STL и библиотек с открытым исходным кодом сэкономит вам массу времени на разработку и отладку.
Для вашей строковой реализации - основные проблемы:
Добавление одного символа приводит к перераспределению, ужасающему с точки зрения памяти и производительности. Обычно у человека есть резервный размер, и он увеличивается в геометрической прогрессии (каждый раз х2 или х1,5). Таким образом, вам не нужно применять перераспределение каждый раз, когда кто-то иногда добавляет один или несколько символов.
В нем отсутствует оптимизация коротких строк. Если строка достаточно короткая, скажем, менее 32 символов, вам не следует выполнять динамическое распределение и вместо этого хранить данные локально. Для этого вам, вероятно, понадобится дополнительный буфер в строковом классе. Это важно, так как большинство струн довольно короткие.
Кроме того, для решения этих проблем вы должны поддерживать более или менее те же функции, которые std::stringподдерживает. Взгляните на его API на cppreference .
Плохая идея - иметь ваши строки как с завершающим нулем, так и с явным размером. C ++ std :: string, совершенно не случайно, этого не делает.
Вы можете выделить дополнительный символ и установить его равным нулю для упрощения преобразования в строки в стиле C. При преобразовании или сравнении со строками C вы можете (и должны) проверить наличие нулевого терминатора в строке C. Никогда не ищите нулевой терминатор в любом другом месте вашего кода. Используйте size.
Вы также забыли реализовать семантику перемещения.