Перегрузка операторов C++, часть вторая - бинарные операторы

newbie_

Опубликован:  2020-02-08T06:44:51.748471Z
Отредактирован:  2020-02-08T06:44:41.580589Z
800
Продолжаем изучать 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. Запускаю текстовый редактор.

b0mVcfooTF.png

И прямо во встроенном терминале создаю новый файл - distance.cpp.

XTeCv1PG8L.png

Исходный код разрабатываемой программы будет храниться в этом файле. Для разработки программы мне понадобится один модуль стандартной библиотеки.

// 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

70MMs2jJPC.png

Как видно на снимке экрана, программа выхлопнула на экран терминала следующий текст.

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

sflaGBK9Oc.png

Как видно на снимках экрана выше, операторы +, < и += с экземплярами класса Distance работают в полном соответствии с замыслом, а цели первой части этого обзора полностью достигнуты, код рассмотренного примера целиком доступен по ссылке.

3. Конкатенация и сравнение строк

Как известно, некоторые арифметические операторы могут иметь различный функционал в зависимости от контекста, в котором применяются. Например оператор + может выражать арифметическую операцию сложения двух чисел в одном контексте, в другом же контексте этот оператор может выражать конкатенацию строк - что в сути не является арифметикой.

Перегруженные операторы сравнения в свою очередь не всегда в своей реализации для сравнения двух объектов используют аналогичные операторы сравнения. Например в примере с классом Distance реализация функции operator < использовала для сравнения данных аналогичный перегружаемому оператор <.

В этой части обзора я покажу пример, в котором оператор + не является арифметическим, а оператор сравнения == реализуется использованием вспомогательных средств, и как уже стало понятно, в этом примере пойдёт речь о строках.

В C++ строки имеют две возможные реализации:

  1. Так называемые С-строки;
  2. Экземпляры базового класса string одноимённого модуля стандартной библиотеки.

О первой реализации пойдёт речь далее...

C-строка - это в сути массив символов (тип char), последним элементом которого следует символ \0. Манипуляция с такими строками осуществляется вспомогательными средствами, которые не всегда удобны. В следующем примере будет показан класс String призванный упростить некоторые манипуляции с C-строками. Создаю в текстовом редакторе новый файл - stringify.cpp.

YQrRzIrdhB.png

Для разработки очередной программы мне потребуется три заголовочных файла стандартной библиотеки.

// 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

q1A0nB4J6X.png

Как видно на снимке экрана выше, компиляция завершилась успешно, без ошибок и предупреждений. Чтобы в полной мере протестировать работу оператора == для экземпляров класса String нужно ещё пару раз запустить полученную программу и попробовать вводить различные значения.

Код программы доступен по ссылке. На этот раз все цели этой демонстрации достигнуты, и с перегрузкой бинарных операторов всё более или менее понятно. А тема (перегрузка операторов в C++) будет продолжена в следующих выпусках блога...

Комментарии: