Реализация класса String

Sep 01 2020

Я работаю над проектом (это язык), и для этого проекта я решил использовать низкоуровневый стиль 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';
}

Вот что меня беспокоит в первую очередь:

  1. Нужен ли мне конструктор перемещения и оператор присваивания перемещения? Я знаю, что в этом нет необходимости, но будут ли они иметь большое значение в данном случае?

  2. Эффективны ли частные методы? Можно ли их улучшить?

  3. Метод str_alc()хорош? Как это хорошая практика - заключать newв оператор try-catch? И следует ли мне \0по умолчанию заполнять строку s? Или это приносит больше вреда, чем пользы?

Также у меня небольшой вопрос: bufferправильное ли имя параметра ? Я не совсем уверен, как назвать параметры ...

Любые другие отзывы также приветствуются!

Спасибо!

Ответы

5 MartinYork Sep 01 2020 at 02:56

Почему вы преобразовываете исключение, имеющее значение, в бессмысленную строку?

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реализует приятную оптимизацию коротких строк поверх базовой версии распределения динамической памяти, которую вы реализовали.

4 ALX23z Sep 01 2020 at 01:57

Я могу что-то упустить, но почему бы и нет std::string? Я не понимаю, почему вы не можете использовать STL или другие библиотеки с открытым исходным кодом. Вы дали какое-то объяснение, но я его не понимаю. Использование STL и библиотек с открытым исходным кодом сэкономит вам массу времени на разработку и отладку.

Для вашей строковой реализации - основные проблемы:

  1. Добавление одного символа приводит к перераспределению, ужасающему с точки зрения памяти и производительности. Обычно у человека есть резервный размер, и он увеличивается в геометрической прогрессии (каждый раз х2 или х1,5). Таким образом, вам не нужно применять перераспределение каждый раз, когда кто-то иногда добавляет один или несколько символов.

  2. В нем отсутствует оптимизация коротких строк. Если строка достаточно короткая, скажем, менее 32 символов, вам не следует выполнять динамическое распределение и вместо этого хранить данные локально. Для этого вам, вероятно, понадобится дополнительный буфер в строковом классе. Это важно, так как большинство струн довольно короткие.

Кроме того, для решения этих проблем вы должны поддерживать более или менее те же функции, которые std::stringподдерживает. Взгляните на его API на cppreference .

1 n.'pronouns'm. Sep 01 2020 at 14:34

Плохая идея - иметь ваши строки как с завершающим нулем, так и с явным размером. C ++ std :: string, совершенно не случайно, этого не делает.

Вы можете выделить дополнительный символ и установить его равным нулю для упрощения преобразования в строки в стиле C. При преобразовании или сравнении со строками C вы можете (и должны) проверить наличие нулевого терминатора в строке C. Никогда не ищите нулевой терминатор в любом другом месте вашего кода. Используйте size.

Вы также забыли реализовать семантику перемещения.