Перегрузка операций. Перегружаем += для самописного класса string

Одн Одной из тяжелоидущих для меня тем в начале моего обучения оказалась перегрузка операций в языке С++.

В этой статье показывается основа написания перегрузки для определённого пользователем класса "строка". В самых ранних версиях С++ не было типа string, в настоящих версиях есть. Но как бы то ни было, многие новички проходят своё обучение с написанием собственного класса String, имитирующего встроенный ныне в язык тип, представляющий собой строку.
Самописные классы, как правило, требуют себе дополнительного учения для применения к ним операций. Например, если мы пишем самодельный класс String, то если хочется, чтобы строка умела вот так:



нужно учить класс String операции +
Язык С++ не ограничивается одной операцией, но контекст некоторых операций очень неочевиден. Например, что должно давать деление строки на строку? Хотя перегрузить операцию деления для пользовательского класса, представляющего строку, возможно, само применение операции деления между строками скорее запутывает и вносит неясности.

    Совет!

  • В общем случае не рекомендуется перегружать операции для типа, если нет полной уверенности, что это значительно улучшит код
  • Перегрузка операций предназначена для облегчения читаемости программ.
Перегрузка операций — это очень полезная возможность языка C++. Если есть собственный класс, то иногда имеет смысл научить объекты этого класса работать так, как от них ожидается на уровне взаимодействия между собой или с другими объектами. Например, сложение двух строк, логично, должно отдавать результатом одну строку, собранную из тех двух. Например, в С++ вывод на экран данных по объекту возможно делать операцией <<, но поначалу объекты, зарождённые от написанных нами самими классов не умеют выполнять таких действий. Этому всему надо учить пишемые нами классы. Вот такое научивание класса выполнять действие-операцию и называется перегрузкой операций. К сожалению, некоторые люди увлекаются перегрузкой, используют её так, что читать код становится тяжело. Главная цель перегрузки — это удобство, а не запутывание всех.
Символ + для чисел обозначает одно, а для строк другое. Когда программист пишет собственный класс, компилятор не знает, что нужно будет делать, если объекты того класса попытаться, например, сложить друг с другом или если попытаться вычесть один из другого. Компилятор же не знает, что именно создаётся, и как к этому относиться, только угадывать может, но если бы компилятор угалывал, то поведение программы бы зависело от компилятора, а не от нас. Так же происходит и с другими операциями языка C++.
Но хватит отступлений. Перейдём к простому примеру, напишем свой класс, который будет очень урезанной версией типа данных std::string, встроенного в современные компиляторы С++. Научим такой класс складывать зарождаемые от него объекты между собой. Для сложения в C++ возможно использовать такие операции, как +, +=, ++. Я, чтобы код получился понятным, использую операцию +=. Надеюсь, операция такая хорошо знакома изучающим С++, читающим эту тему сейчас.

           a += b      //--- короткая форма --- //
           a = a + b;  //--- полная форма -- //
        

Листинг #1 можно описать приблизительно так:

Когда создаётся первый объект, вовнутрь переменной data, принадлежащей этому объекту, попадает значение "String one". Это происходит благодаря конструктору класса. Следующее действие после инициализации объекта строкой выполняется операция +=. По той причине, что внутри класса описана особая функция-член (там, где operator+), становится возможно использовать операцию += для зарождаемых таким классом объектов.

Единственный случай, когда допускается использовать не цифры и не буквы латинского алфавита в качестве имени сущности — перегрузка операций для пользовательских классов. Таким образом получается, что имеется как бы функция, с припаянным к ней обозначением operator, имя которой сама операция.

В качестве параметра, в круглых скобках, указывается такой объект, тип которого соответствует типу объекта, стоящего справа от объекта, к которому операция применяется. Всё остальное — это то же самое, что и обычные функции. Важно только отметить, что для перегрузки операций всегда используется ключевое слово operator, оно даёт знать компилятору, что это операция, а не функция.

Сложение объекта класса с константной строкой — это, конечно, хорошо, но это неполная перегрузка. Чаще всё-таки объекты класса складываются между собой. В этом, показанном выше примере, в листинге #1, фактически складываются два разных типа данных: мой тип данных с типом константная строка. В связи с этим, сложить два объекта непосредственно моего класса между собой не выйдет.


Чтобы научить класс такому действию, нужно научиться понимать, что в параметр функции-перегрузки-операции, туда, где operator+=, надо отдавать объект того же типа: тип свой собственный класс. Так как в листинге #1 отдавалась константная строка "СТРОКА" и для параметра внутри функции указан тип, подходящий для константной строки (const char*), то всё прошло гладко. Если в параметре функции использовать указатель на константную строку, то функция легко проглотит константную строку, не так ли? Но если в параметре функции использовать указатель на константную строку, а кормить функцию объектом неизвестного компилятору происхождения (т. е. объекта нашего собственного класса), то с чего бы функция такое вхождение поймёт? Поэтому чтобы объект можно было складывать с таким же объектом, какой он сам по типу, нужно использовать в параметре тот тип, который представляет сам объект, т. е. в качестве типа в параметре нужен наш класс. Добавим классу умение, перегрузим метод класса, перегружающий операцию:

Теперь работает как задумывалось.
Злоупотребим немного советом и добавим возможность вычитания к объекту нашего класса. Делать так, конечно, не нужно, это сейчас происходит в качестве тренировочного эксперимента. Знак минус () будет использоваться для удаления подстроки из строки.

Нужно знать:

  • Чтобы перегрузить операцию, нужно определить класс, к объектам которого операция будет назначена
  • Когда перегружается операция, перегрузка действует только для класса в котором операция и определяется. Если тип объекта не является классом (или любым видом класса(структура, объекдинение…)), то действуют стандартные правила, переопределить или перегрузить операции для неклассовых объектов нельзя.
  • Для перегрузки операции используется ключевое слово С++ operator. Это слово определяет метод класса, который С++ вызывает каждый раз, когда переменная-объект класса вызывает операцию
  • На перегрузку операций С++ накладывает разные ограничения.

    • Старшинство операций не может быть изменено перегрузкой
    • Ассоциативность операций не может быть изменена перегрузкой
    • Изменить количество операндов, которое берет операция невозможно: Перегруженные унарные операции остаются унарными, перегруженные бинарные остаются бинарными
    • Каждая из операций &, *, +, может иметь и унарный, и бинарный варианты. Эти унарные и бинарные варианты могут перегружаться отдельно
    • Создавать новые операции невозможно. Возможно только использовать существующие.
    • Нельзя изменить операцию для объекта встроенного типа
    • Неявной перегрузки не существует (например, object1 = object1 + object2 не равно object1 += object2, если явно перегрузки не прописано)
В любом случае, важно помнить, что перегрузка операций нужна для облегчения читаемости кода и злоупотребление перегрузкой в невыгоду читаемости принесёт только вред.
Перегружать операции таким образом, как было показано выше, — хорошо, но этот способ перегрузки, когда функция перегрузки операции возвращает void, имеет определённые недостатки. По коду может быть сложно сказать, что делает операция для объектов класса; к тому же, нельзя сохранить полученный результат в отдельную переменную. Давайте посмотрим наглядный пример. Перегрузим обычный +.

Поэтому возвращаемые значения для функций операции-перегрузки часто написаны с типом не void. Иногда имеет смысл ничего не возвращать, но если результат в перспективе будет нужно сохранять, то нужно возвращать соответствующий тип данных. В показанном примере, листинг #4, этот тип был int.
  • Нельзя перегрузить возвращаемый тип.

В листинге #5.1 если убрать комментарий, то получится, что операция + выдаёт два результата: void и int, но такое проделывать нельзя по стандарту языка, возвращаемый тип должен быть один и тот же на все случаи.
Листинг #5.1 лучше бы выглядел вот так:

Статья дополнена и сильно исправлена 12.04.2018

18 комментариев на «“Перегрузка операций. Перегружаем += для самописного класса string”»

  1. Gen:

    С большим трудом удалось скомпелировать в Code::Block,но так и не понял что происходит.У меня просто копируется введенная строка и удаляется символ.А как складываются строки так и не увидел..С этой строкой

    вылетают ошибки,заменяю на

    ошибок нет,но мне кажется и не работает.Пожалуйста объясните в чем дело.Название класса пришлось поменять из за конфликта с инклудой string,и переменные несколько изменить

    • Пример неправильно описан. Поэтому буду переделывать, но это в течении след. недели. Сейчас нет возможности.
      а про конфликты) не юзайте using namespace std и будет меньше геморроя) пишите прямо std::cout,std::string и т.п.

    • Точнее с минусом неправильно. С плюсом работает.
      Например вы вводите 2 слова:
      Привет
      Пока
      Сначала происходит сложение этих двух строк = «Привет Пока»
      Потом должен удалиться символ «в», что там получится должно не помню. В общем это зависит от того, что у минуса в классе описано. Можно 1 символ удалять, можно все, можно каждый второй и т.п.

      Чтоб другие не мучались код для CodeBlock

  2. Gen:

    Вот так у меня работает правильно.С тегами не получается в виде текста покажу код.
    #include
    #include
    #include

    using namespace std;
    //СОЗДАЕМ СОБСТВЕННЫЙ КЛАСС
    class stroka
    {
    public:
    stroka (char*); //конструктор класса принимающий один параметр
    void operator+(char*); //определение оператора +
    void operator-(char); //определение оператора —
    void show_stroka(void); //метод класса для отображения строки
    private:
    char data[256]; //символьный массив, доступный только классу
    };

    stroka::stroka(char *str) //Транзитом через конструктор
    {
    strcpy(data, str); //копируем в символьный массив класса данные из принимаемой извне строки
    }

    void stroka::operator+(char *str) //Определяем оператор +
    {
    strcat(data, str); //как функцию сложения двух строк
    }

    void stroka::operator-(char letter)//Определяем оператор —
    {
    char temp[256]; // будем создавать новую строку
    int i, j; //счетчики циклов
    //Проходим по всей строке класса с помощью цикла и если символ строки не равен принятому символу (параметру), то копируем его в новую строку
    for (i = 0, j = 0; data[i]; ++i) if (data[i] != letter) temp[j++] = data[i];
    temp[j] = NULL;
    strcpy(data, temp); //Копируем новую строку в символьный массив класса
    }
    void stroka::show_stroka(void)
    {
    cout << data << endl; //Показываем символьный массив класса
    }

    int main()
    {

    char st[256], st2[256]; //Объявление двух указателей для строк
    cin.get(st, 256); //Считывание первой строки с клавиатуры
    cin.get();
    cin.get(st2, 256); //Считывание второй строки с клавиатуры
    stroka title(st); //Объявление переменной типа нашего класса и передача в конструктор первой строки
    title + " "; //С помощью перегрузки операторов добавили к строке пробел
    title + st2; //C помощью перегрузки операторов добавили к строке вторую строку
    title.show_stroka(); //Отобразили результирующую строку на экране
    title — 'b'; //При помощи перегрузки операторов пытаемся удалить символ в
    title.show_stroka(); //Отобразили результирующую строку

    return 0;
    }
    только в этой строке предупреждение старое преобразование title + " "; //С помощью перегрузки операторов добавили к строке пробел

  3. Gen:

    Разобрался и с этим.Переписал так void operator+(char const*); //определение оператора + и void stroka::operator+(char const* str) //Определяем оператор +j.Теперь кампиляция чистая и все работает.

  4. Gen:

    Наверное правильней будет не temp[j] = NULL; а temp[j] = »;

  5. Gen:

  6. Gen:

    не получается в ‘ ‘

  7. дон:

    в
    private:
       char data[256];
    я думаю, что очень хорошо

  8. Mr.Ex:

    Можно все сделать гораздо проще)

     

  9. big-town:

    Ну когда уже научатся люди задействовать свой мозг! Ctrl+c,v уже задолбало!

    Не ужели этот пример может чему то научить?! Особенно начинающего!
    fn.cpp

    CFLAGS=-std=gnu++11
    all: fn.cpp
    g++ $(CFLAGS) -o fn fn.cpp

    • Пример немного неправильно написан. Вот так лучше:

      А вообще, спасибо за участие. Хороший пример.

      • big-town:

        Нет, не забыл это осталось от предыдущего кода. operator-вообще не обязан что либо возвращать, это тоже известное заблуждение из копипастов. Проверьте, вот так тоже будет работать.

        #include "cstring"
        #include "cstdio"

        class MyStr
        {
        public:
        char str[256];
        void operator=(char *pstr)
        {
        strcpy(str,pstr);
        }
        };

        int main()
        {
        MyStr a;
        a="This is string";
        puts(a.str);
        return 0;
        }

        Сейчас уже очень мало людей которые пишут на чистом Си и даже С++. Немодно :). Сейчас принято плодить говнокод на различных фрэймворках и явах. И аргумент — это скорость программирования. Может быть это и так, а вот качество кода при этом ужасно страдает. Быстрых и не требовательных приложений очень мало. И то что существуют такие порталы как ваш — это очень хорошо. Но не отпугивайте начинающих примерами монстрами в которых и опытный прогер не сразу разберется. Удачи, надеюсь что популяция сишников восстановится :)))!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Поиск

 
     

Случайная книга в электронном формате

https://www.litres.ru/robert-s-martin/idealnyy-programmist-kak-stat-professionalom-razrabotki-po/?lfrom=15589587
Яндекс.Метрика