Перегрузка операторов C++, часть вторая - бинарные операторы
newbie
Опубликован: | 2020-02-08T06:44:51.748471Z |
Отредактирован: | 2020-02-08T06:44:41.580589Z |
Продолжаем изучать C++ и его возможности, в этом обзоре рассмотрим основные механизмы перегрузки бинарных операторов - арифметических операторов и операторов сравнения - на примере двух классов различного характера. Как и в предыдущем описании, рассмотренные примеры заимствованы из книги Роберта Лафоре и в некоторых деталях модифицированы под компилятор g++ и терминал Linux.
1. Общие сведения
Как известно, бинарные операторы взаимодействуют с двумя объектами: объектом, расположенным справа от оператора, и объектом, расположенным слева от оператора. Бинарными являются все арифметические операторы (+
, -
, *
, /
, %
), комбинированные операторы (+=
, -=
etc.) и операторы сравнения (<
, >
, ==
etc.).
Как и в случае с унарными операторами, перегрузка бинарных операторов в C++ осуществляется на основе подобного механизма, то есть в определении класса для каждого конкретного бинарного оператора, который необходимо перегрузить, определяется функция член с именем operator
и обозначением конкретного оператора, например - +
. Так как бинарные операторы взаимодействуют с двумя объектами, эта функция член обычно принимает один обязательный аргумент, определяющий объект справа от оператора. В таком случае объектом слева от оператора является тот объект, которому принадлежит определяющая перегружаемый оператор функция член. Например, для комбинированного оператора сложения и присваивания выражение выглядит как:
b += c;
А декларация соответствующей функции-члена, определяющей поведение этого оператора, будет иметь такой вид:
class Obj { private: ... public: ... void operator += (Obj c); };
В данном выражении (b += c
) объект b
- это объект, в котором определена перегружающая оператор функция-член, а объект c
- это аргумент перегружающей оператор функции члена, и оба эти объекта однотипны, то есть являются экземплярами класса Obj
.
Как всегда, при реализации конкретного класса в конкретной программе всегда всплывает масса мелких характерных деталей, на которые следует обращать внимание. Разобрать эти детали и проникнуться механизмом перегрузки бинарных операторов мне помогут два характерных примера из книги Роберта Лафоре, которые я продемонстрирую далее по списку ниже...
2. Перегрузка арифметических операторов и операторов сравнения
Конкретные примеры перегрузки арифметических операторов и операторов сравнения мне поможет разобрать представленный в книге Роберта Лафоре класс Distance
, описывающий английские меры длины - футы и дюймы.
Все действия для этой демонстрации я выполню на базе операционной системы Debian buster, для компиляции разработанных программ буду использовать компилятор g++, а для набора текста исходника разрабатываемой программы - текстовый редактор с расширенными возможностями Kate. Запускаю текстовый редактор.
И прямо во встроенном терминале создаю новый файл - distance.cpp
.
Исходный код разрабатываемой программы будет храниться в этом файле. Для разработки программы мне понадобится один модуль стандартной библиотеки.
// distance.cpp #include <iostream>
Определяю класс Distance
, который в перспективе будет описывать выраженные в английских мерах длины объекты.
class Distance { private: // private block public: // public block };
Поскольку класс описывает объекты, состоящие из футов и дюймов, в приватном блоке я определю два свойства.
... private: int feet; float inches;
Количество футов конкретного объекта как правило выражается целым значением - переменная feet
имеет соответствующий тип, а количество дюймов - числом с плавающей точкой - переменная inches
.
В публичном блоке этого класса мне потребуются два конструктора. Первый конструктор будет определять состояние созданных в объявлениях без инициализации экземпляров, этот конструктор не принимает аргументов.
public: Distance() : feet(0), inches(0.0F) {}
Второй конструктор предполагает два аргумента и в данном случае необходим для возможности определения безымянных экземпляров с определённым состоянием свойств объекта - переменных в блоке private
.
Distance(int f, float i) : feet(f), inches(i) {}
Дополнительно для этого класса я определю две вспомогательные функции. Первая -getdist
- даст возможность инициировать созданные объявлениями экземпляры с запросом соответствующих данных у пользователя программы.
void getdist();
Вторая - showdist
- даст возможность вывести на экран значения хранящихся в объекте мер длины в заданной форме.
void showdist() const { std::cout << feet << "\'-" << inches << "\"\n"; }
На текущем этапе разработки класса Distance
я перегружу три характерных оператора: +
, <
и +=
.
Distance operator + (Distance dd) const;
Арифметический оператор сложения может использоваться в сложных выражениях типа:
a = b + c + d;
Поэтому необходимо, чтобы соответствующая функция оператора возвращала экземпляр этого же класса - Distance
. Аргумент функции, как было уже отмечено ранее, определяет объект в выражении справа от оператора, и в свою очередь тоже будет экземпляром этого же класса - Distance
.
Следующий оператор - <
- будет перегружен такой функцией:
bool operator < (Distance dd) const;
Здесь возвращаемое значение имеет стандартный тип bool
.
Последний оператор, перегрузку которого я покажу в рамках этого обзора, - +=
будет выражен такой функцией.
void operator += (Distance dd);
Я полагаю, что экземпляры класса Distance
в сочетании с этим оператором будут использоваться только в простых выражениях типа a += b
, и поэтому соответствующая функция не возвращает ничего (тип void). В противном случае возвращаемое значение было бы другим.
Так как декларация класса содержит функции требующие определения, приступим к следующему этапу разработки, определим функции-члены.
void Distance::getdist() { std::cout << "Enter feet: "; std::cin >> feet; std::cout << "Enter inches: "; std::cin >> inches; }
Функция getdist
запрашивает у пользователя программы два значения: футы и дюймы; и присваивает эти значения соответствующим свойствам объекта. Здесь следует иметь ввиду, что пример учебный и не предполагает никакой валидации введённых пользователем данных, а значит, программа будет работать правильно только в том случае, если пользователь введёт значения определённого типа в соответствующих полях ввода.
У меня осталось три функции, которые необходимо определить - основная цель этого обзора. Определение функции operator +
для этого примера будет иметь следующий вид:
Distance Distance::operator + (Distance dd) const { // определяем результирующее значение для футов int f = feet + dd.feet; // определяем результирующее значение для дюймов float i = inches + dd.inches; // проверяем полученные данные и // в случае необходимости корректируем результирующие значения if (i >= 12.0F) { i -= 12.0F; f++; } // с помощью второго конструктора класса Distance // создаём безымянный экземпляр этого класса, используя // в качестве аргументов полученные значения переменных f и i // и возвращаем этот безымянный экземпляр return Distance(f, i); }
Перегрузку оператора сравнения <
в данном случае можно определить следующим образом:
bool Distance::operator < (Distance dd) const { // переводим английские единицы футы и дюймы // в простую десятичную дробь - количество футов // для обеих участвующих в операции сравнения объектов float bf1 = static_cast<float>(feet) + inches / 12; float bf2 = static_cast<float>(dd.feet) + dd.inches / 12; // а затем сравниваем два полученных значения // и результат сравнения возвращаем return (bf1 < bf2) ? true : false; }
Здесь следует обратить внимание, что возвращаемое значение я получил операцией сравнения аналогичной перегружаемой, в данном случае <
.
Определение функции для перегрузки оператора +=
будет иметь следующий вид:
void Distance::operator += (Distance dd) { // получаем новые значения свойств объекта feet += dd.feet; inches += dd.inches; // проверяем полученные данные и // в случае необходимости корректируем значения свойств объекта if (inches >= 12.0F) { inches -= 12.0F; feet++; } }
Как было отмечено выше, в этом учебном примере планируется использовать оператор +=
в простых выражениях типа a += b
, поэтому полученная функция не возвращает ничего (void).
С определением класса полный порядок, все намеченные мероприятия выполнены. Теперь настало время протестировать всё это великолепие на примере конкретных действий. Кроме этого, чтобы программа была целостной и компилировалась в исполняемый файл, этому коду необходима функция main
, в которой я и опишу возможную реализацию создания и использования экземпляров класса Distance
. И выглядеть эта функция будет следующим образом:
int main() { // Тестируем первый конструктор класса Distance dist1, dist3, dist4; // Тестируем функцию член getdist dist1.getdist(); // Тестируем второй конструктор класса Distance dist2(11, 6.25F); // Тестируем оператор < if (dist1 < dist2) std::cout << "dist1 is less than dist2\n"; else std::cout << "dist1 is greater than or equal to dist2\n"; // Выводим на экран полученные значения std::cout << "dist1 = "; dist1.showdist(); std::cout << "dist2 = "; dist2.showdist(); // Тестируем оператор + с двумя объектами dist3 = dist1 + dist2; std::cout << "dist3 = "; dist3.showdist(); // Тестируем оператор + с количеством объектов больше двух dist4 = dist1 + dist2 + dist3; std::cout << "dist4 = "; dist4.showdist(); // Тестируем оператор += std::cout << "Before addition:\n"; std::cout << "dist1 = "; dist1.showdist(); std::cout << "dist2 = "; dist2.showdist(); dist1 += dist2; std::cout << "After addition:\n"; std::cout << "dist1 = "; dist1.showdist(); return 0; }
Настал момент истины, сохраняю файл в редакторе, компилирую его и пробую исполнить полученный исполняемый модуль.
g++ -o exe -Wall distance.cpp ./exe
Как видно на снимке экрана, программа выхлопнула на экран терминала следующий текст.
Enter feet: 6 Enter inches: 8.75 dist1 is less than dist2 dist1 = 6'-8.75" dist2 = 11'-6.25" dist3 = 18'-3" dist4 = 36'-6" Before addition: dist1 = 6'-8.75" dist2 = 11'-6.25" After addition: dist1 = 18'-3"
Теперь достаточно вооружиться калькулятором, просчитать все выполненные арифметические операции вручную и убедиться, что код работает в полном соответствии с замыслом. Для полной убедительности можно второй раз запустить программу и на запрос ввести значения, превышающие заданные в переменной dist2
- 11 футов 6 с четвертью дюймов, дабы проверить правильность работы оператора <
.
Как видно на снимках экрана выше, операторы +
, <
и +=
с экземплярами класса Distance
работают в полном соответствии с замыслом, а цели первой части этого обзора полностью достигнуты, код рассмотренного примера целиком доступен по ссылке.
3. Конкатенация и сравнение строк
Как известно, некоторые арифметические операторы могут иметь различный функционал в зависимости от контекста, в котором применяются. Например оператор +
может выражать арифметическую операцию сложения двух чисел в одном контексте, в другом же контексте этот оператор может выражать конкатенацию строк - что в сути не является арифметикой.
Перегруженные операторы сравнения в свою очередь не всегда в своей реализации для сравнения двух объектов используют аналогичные операторы сравнения. Например в примере с классом Distance
реализация функции operator <
использовала для сравнения данных аналогичный перегружаемому оператор <
.
В этой части обзора я покажу пример, в котором оператор +
не является арифметическим, а оператор сравнения ==
реализуется использованием вспомогательных средств, и как уже стало понятно, в этом примере пойдёт речь о строках.
В C++ строки имеют две возможные реализации:
- Так называемые С-строки;
- Экземпляры базового класса
string
одноимённого модуля стандартной библиотеки.
О первой реализации пойдёт речь далее...
C-строка - это в сути массив символов (тип char
), последним элементом которого следует символ \0
. Манипуляция с такими строками осуществляется вспомогательными средствами, которые не всегда удобны. В следующем примере будет показан класс String
призванный упростить некоторые манипуляции с C-строками. Создаю в текстовом редакторе новый файл - stringify.cpp
.
Для разработки очередной программы мне потребуется три заголовочных файла стандартной библиотеки.
// stringify.cpp #include <iostream> #include <cstring> #include <cstdlib>
Определяю главное действующее лицо этой программы - класс String
.
class String { private: // private block public: // public block };
В приватном блоке этого класса я определю целую константу SZ
, с помощью которой впоследствии задам размер массива.
... private: static const int SZ = 80;
И объявлю массив символов str
.
char str[SZ];
В публичном блоке класса String
мне необходимы два конструктора, первый конструктор без аргументов будет отвечать за начальное состояние экземпляров этого класса созданных объявлениями без инициализации.
String() { str[0] = '\0'; }
Второй конструктор принимает один аргумент, с его помощью можно будет инициировать экземпляры этого класса.
String(const char * s) { strcpy(str, s); }
В этом конструкторе я использовал объявленную в заголовочном файле cstring стандартную функцию strcpy для копирования строки переданной аргументом в строку str
.
Хранящиеся в экземплярах класса данные необходимо будет выводить на экран в заданной форме, для этого определяю функцию-член display
.
void display() const { std::cout << str << std::endl; }
Объявленные без инициализации экземпляры класса необходимо будет каким-то образом инициировать с получением данных от пользователя программы, для этого потребуется ещё одна функция-член.
void getstr() { std::cin.get(str, SZ); }
По условиям задачи мне необходимо обеспечить возможность конкатенации двух строк, для этого декларирую в классе перегрузку оператора +
.
String operator + (String ss) const;
Оператор сравнения ==
тоже необходимо перегрузить.
bool operator == (String ss) const;
Как и в предыдущем примере обе функции operator
в данном случае имеют возвращаемое значение, и принимают один аргумент - экземпляр этого же класса. Рассмотрим возможную реализацию каждой из этих двух функций, начнём с operator +
:
String String::operator + (String ss) const { // Объявляю ещё один экземпляр класса String String res; // Проверяю суммарную длину участвующих в // операции конкатенации строк с помощью стандартной // функции strlen из файла cstring if ( strlen(str) + strlen(ss.str) < SZ) // Если условие выполняется { // Сначала копирую строку str в объект res c помощью // стандартной функции strcpy из файла cstring strcpy(res.str, str); // А затем осуществляю конкатенацию с помощью // стандартной функции strcat из файла cstring strcat(res.str, ss.str); } else // Если условие не выполняется { // Вывожу на экран предупреждение std::cout << "String overflow" << std::endl; // Прерываю исполнение программы // с помощью стандартной функции exit из файла cstdlib exit(1); } // Возвращаю переменную res return res; }
С оператором сравнения всё ещё проще.
bool String::operator == (String ss) const { // Сравниваю две строки с помощью стандартной // функции strcmp из файла cstring // и возвращаю полученное в результате сравнения значение return (strcmp(str, ss.str) == 0) ? true : false; }
В сущности, эту функцию-член можно было определить прямо в декларации класса.
Теперь мне всё это великолепие необходимо как-то протестировать, и делать это я опять буду в функции main
.
int main() { // Тестируем второй конструктор класса String String s1 = "Hello, "; String s2 = "world!"; // Тестируем первый конструктор класса String String s3; s1.display(); s2.display(); s3.display(); // Тестируем конкатенацию строк s3 = s1 + s2; s3.display(); // Готовимся к тесту оператора сравнения строк == String s4 = "yes"; String s5 = "no"; String s6; std::cout << "Enter 'yes' or 'no': "; s6.getstr(); // Тестируем оператор сравнения строк == if (s6 == s4) std::cout << "You typed yes.\n"; else if (s6 == s5) std::cout << "You typed no.\n"; else std::cout << "You didn't follow instructions.\n"; return 0; }
Очередной момент истины..? Сохраняю файл stringify.cpp
, компилирую программу и пробую её исполнить.
g++ -o exe -Wall stringify.cpp ./exe
Как видно на снимке экрана выше, компиляция завершилась успешно, без ошибок и предупреждений. Чтобы в полной мере протестировать работу оператора ==
для экземпляров класса String
нужно ещё пару раз запустить полученную программу и попробовать вводить различные значения.
Код программы доступен по ссылке. На этот раз все цели этой демонстрации достигнуты, и с перегрузкой бинарных операторов всё более или менее понятно. А тема (перегрузка операторов в C++) будет продолжена в следующих выпусках блога...
Метки: | gplusplus, cplusplus, newbie, operator_overloading |