Компания ALT Linux
Опубликован: 07.03.2015 | Доступ: свободный | Студентов: 2137 / 487 | Длительность: 24:14:00
Лекция 10:

Объектно-ориентированное программирование

10.2.7 Перегрузка операторов

Как уже упоминалось, перегрузка, т. е. возможность создавать функции (например, методы класса) с одинаковыми именами и разными наборами параметров, вызываемые в разных ситуациях для решения однотипных задач — это одно из ключевых проявлений полиморфизма в C++. Однако кроме перегрузки функций C++ позволяет проделывать то же самое с большинством стандартных операторов.

На самом деле, можно считать, что перегрузка операторов для стандартных типов данных в неявном виде присутствовала ещё в языке С. Например, оператор деления может выполнять разные действия в зависимости от того, какой тип имеют его аргументы: для целочисленных аргументов будет выполнено деление нацело, а для вещественных — деление чисел с плавающей точкой. С точки зрения процессора деление чисел с плавающей точкой кардинально отличается от деления нацело: задействована другая машинная команда, операнды должны быть загружены в совсем другие регистры (ячейки памяти процессора), после чего выполняется совсем другая микропрограмма. На более высоком уровне абстракции операции целочисленного и вещественного деления могут казаться одинаковыми; однако использование для них одного и того же оператора допускают далеко не все языки.

В C++ это явление довели до логического завершения, и теперь многие встроенные операторы можно перегрузить для работы с новыми типами данных. Чтобы перегрузить оператор, программист объявляет новую функцию, имя которой состоит из ключевого слова operator и знака операции. Например, перегрузим оператор + для сложения двух объектов класса spatial_vector. Объявление функции будет выглядеть следующим образом:

spatial_vector operator+ ( spatial_vector a,spatial_vector b )
{
......... .
}

Нам понадобится предусмотреть в классе spatial_vector геттеры и сеттеры для всех трёх координат, чтобы внешняя функция могла выполнить покоординатное сложение двух векторов (либо мы могли бы объявить функцию дружественной классу). Также мы предусмотрим в классе конструктор, инициализирующий координаты заданными значениями, и метод info, выводящий координаты вектора на экран.

#include <iostream>
#include <math.h>
using namespace std;
class spatial_vector
{
	double x, y, z;
public :
	spatial_vector ( double x, double y, double z ) { this->x=x; this->y=y; this->z=z; }
	double get_x ( ) { return x; }
	double get_y ( ) { return y; }
	double get_z ( ) { return z; }
	void set_x ( double x ) { this->x=x; }
	void set_y ( double y ) { this->y=y; }
	void set_z ( double z ) { this->z=z; }
	void info ( ) { cout << "Координаты вектора: "<<x<<", "<<y<<", "<<z<<endl; }
};
spatial_vector operator+ ( spatial_vector a,spatial_vector b )
{
	spatial_vector c ( 0, 0, 0 );
	c.set_x ( a.get_x ( ) + b.get_x ( ) );
	c.set_y ( a.get_y ( ) + b.get_y ( ) );
	c.set_z ( a.get_z ( ) + b.get_z ( ) );
	return c;
}
main ( )
{
	spatial_vector a(1, 2, 3 ), b(10, 20, 30 ), c ( 0, 0, 0 );
	c=a+b;
	c.info ( );
}
  • оператор должен уже существовать в языке (нельзя добавить в программу новые, не существовавшие ранее операторы);
  • нельзя изменить количество операндов, которое принимает перегружаемый оператор;
  • нельзя переопределять действия встроенных в C++ операторов при работе со встроенными типами данных: например, нельзя перегрузить оператор "+" для работы с целыми числами типа int (а если вдруг это зачем-то понадобится, можно создать класс-обёртку, например integer, и перегружать для него все что угодно);
  • нельзя перегружать операторы ".", ".*", "?:", "::";
  • по вполне очевидной причине нельзя перегружать знак директивы препроцессора "#".

10.2.8 Перегрузка членов класса

Члены класса можно перегружать так же, как любые другие функции. Особенно часто перегрузку используют для объявления нескольких конструкторов. Главный смысл перегрузки конструкторов состоит в том, чтобы предоставить программисту наиболее удобный для каждой конкретной ситуации способ инициализации объекта. Например, мы можем объявить два конструктора в классе spatial_vector: один конструктор по умолчанию, создающий вектор с нулевыми значениями, а другой — принимающий конкретные параметры:

class spatial_vector
{
	double x, y, z;
public :
	spatial_vector ( double x, double y, double z );
	spatial_vector ( );
... .
};

Однако заметим, что если код двух конструкторов практически идентичен и различается лишь использованием переданных значений в одном конструкторе и констант — в другом, то запись можно упростить. В такой ситуации можно оставить только конструктор с параметрами и задать для этих параметров (для всех или для нескольких, идущих последними в списке) значения по умолчанию:

spatial_vector ( double x=0, double y=0, double z=0);

Параметры, имеющие значение по умолчанию, можно не указывать при вызове.

Операторы тоже можно перегружать как члены класса, но с некоторыми интересными особенностями. Если мы вызываем в программе метод класса — его вызов будет указан после имени конкретного объекта. Как читатель безусловно помнит, при этом методу передаётся скрытый указатель на объект. Если перегруженный оператор объявлен как член класса, то компилятор, встретив его вызов, должен определить, для какого объекта вызвана перегружающая оператор функция, и тоже передать ей скрытый указатель на объект. Таким объектом всегда является левый операнд. По этой причине в объявлении перегруженного оператора внутри класса нет необходимости упоминать собственный объект — ведь он передаётся скрытым указателем this. Поэтому описание бинарного оператора, перегруженного как член класса, имеет всего один операнд (правый), а описание унарного оператора не имеет ни одного операнда.

Следует отметить, что нельзя объявить оператор как статический метод (поскольку статическим методам указатель this при вызове не передаётся) или использовать с оператором аргументы по умолчанию

Для одного и того же оператора можно объявить несколько перегруженных операторов-функций. Но, как и в случае перегрузки обычных функций, компилятор должен различать их по типу и количеству аргументов. Когда компилятор сталкивается с перегруженным оператором для класса X, он ищет подходящую функцию-оператор для класса Х, используя обычные для перегруженных функций правила сопоставления аргументов. Если поиск завершился неудачей, компилятор не пытается самостоятельно применить к аргументам перегруженных операторов преобразования типов.

Механизм дружественных функций часто используется при перегрузке операторов для работы с объектами, когда по какой-то причине перегруженный оператор невозможно или нецелесообразно объявлять членом класса. Для сравнения изменим фрагмент примера из п. 10.2.7, переопределив оператор "+" как функцию-член класса (слева) и как дружественную функцию (справа):

class spatial_vector
{
.....
spatial_vector operator+(
	spatial_vector b);
};
spatial_vector spatial_vector : :
	operator+( spatial_vector b )
{
	spatial_vector c;
	c.x = x + b.x;
	c.y = y + b.y;
	c.z = z + b.z;
	return c;
}
class spatial_vector
{
.....
friend spatial_vector operator+(
	spatial_vector a,spatial_vector b);
};
spatial_vector operator+(
	spatial_vector a,spatial_vector b )
{
	spatial_vector c;
	c.x = a.x + b.x;
	c.y = a.y + b.y;
	c.z = a.z + b.z;
	return c;
}

Классический пример перегрузки оператора как дружественной функции — средства стандартного ввода-вывода в C++. Как известно, операции << и >> выполняют ввод и вывод, если левым аргументом указан один из стандартных объектов ввода-вывода. Предопределённые объекты cin (клавиатура) и cout (дисплей) — экземпляры классов istream и ostream. Этим способом можно вывести любой базовый тип данных C++. Однако, на самом деле при выводе вызывается функция, перегружающая оператор. В частности, для объекта cout будет вызвана функция, имеющая приблизительно следующий вид:

ostream operator<<(ostream, int )

В результате, выражение

cout << "Значение переменной i равно " << i << " \n ";

благодаря такой подстановке будет заменено на

operator<<(operator<<(operator<<(cout, "Значение переменной i равно " ), i ), " \n " );

Понятно, что библиотека iostream содержит функции только для встроенных типов. Если требуется перегрузить операторы стандартного ввода-вывода для нового класса, чтобы программист мог вводить с консоли его информационное содержимое или выполнять его вывод на экран, необходимо перегрузить оператор для нового типа.

Использовать перегрузку оператора как члена класса невозможно, т. к. левым аргументом должен быть не объект нового класса, а уже существующие объекты cin и cout. Таким образом, перегружать оператор приходится как внешнюю функцию. Однако поскольку эту функцию создаёт автор нового класса, он вполне может объявить её в структуре класса как дружественную, упростив ей доступ к закрытой части класса.

В общем виде операция вывода имеет следующую форму

ostream& operator << ( ostream& stream, имя_класса& obj )
{
	stream <<...//вывод элементов объекта obj в поток stream
	return stream;
}

Аналогичным образом может быть определена функция ввода:

istream& operator >> ( istream& stream, имя_класса& obj )
{
	stream >>...//ввод элементов объекта obj из потока stream
	return stream;
}

Знак "&&quot; в списке формальных параметров означает, что компилятор обеспечивает скрытую передачу параметра не по значению, а по ссылке (передача объектов по ссылке детально рассмотрена далее, в п. 10.3.2. Первый аргумент функций ввода и вывода определён как ссылка на поток, второй аргумент — ссылка на объект, выводящий или получающий информацию, а возвращаемое значение — тот же самый объект потока, который был передан в качестве первого аргумента.

Приведём пример с перегрузкой операторов стандартного ввода и вывода для уже знакомого нам класса spatial_vector.

#include <iostream>
using namespace std;
class spatial_vector
{
	double x, y, z;
public :
	spatial_vector ( ) { x=y=z =0; }
	friend ostream& operator<< ( ostream& stream, spatial_vector& b);
	friend istream& operator>> ( istream& stream, spatial_vector& b);
};
ostream& operator<< ( ostream& stream, spatial_vector& b )
{
	stream << " x = " << b.x << "; y = " << b.y << "; z = " << b.z << endl;
	return stream;
}
istream& operator>> ( istream& stream, spatial_vector& b )
{
	stream >> b.x >> b.y >> b.z;
	return stream;
}
main ( )
{
	spatial_vector a;
	cin >> a;
	cout << "Был введён вектор: " << a << endl;
}

10.2.9 Перегрузка постфиксных операторов

Большинство операций, поддерживаемых C++, являются префиксными, т. е. оператор применяется до вычисления выражения. Исключение составляют операторы инкремента и декремента ++ и --, которые могут быть как префиксными, так и постфиксными. При перегрузке постфиксных операций возникают определённые неудобства: например, программист должен иметь возможность как-то показать компилятору, что перегружает именно постфиксный оператор.

Объявление члена класса с именем operator++ без аргументов перегружает префиксный оператор инкремента. Чтобы перегрузить функцию-член класса как постфиксный оператор, его нужно объявить с одним аргументом типа int. Этот аргумент не несёт никакой полезной нагрузки и нужен только, чтобы можно было различить префиксные и постфиксные операторы. При выполнении этот аргумент будет иметь нулевое значение. Следующий пример показывает разницу в описаниях, и дополнительно выводит в консоль информацию о том, префиксный или постфиксный оператор был использован. В примере использован класс integer, являющийся обёрткой над переменной типа int, т. е. просто хранящий целое число:

#include <iostream>
using namespace std;
classin integer
{
	int value;
public :
	integer ( ) { value = 0; }
	integer& operator++(); //префиксный оператор
	integer& operator++(int ); //постфиксный оператор
};
integer& integer::operator++()
{
	value +=1;
	cout << "Использован префиксный оператор\n ";
	return *this;
}
integer& integer::operator++(int )
{
	value +=1;
	cout << "Использован постфиксный оператор\n ";
	return *this;
}
main ( )
{
	integer i;
	i ++; //используется постфиксный оператор
	++i; //используется префиксный оператор
}

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

Сергей Радыгин
Сергей Радыгин

Символы кириллицы выводит некорректно. Как сделать чтобы выводился читабельный текст на русском языке?

Тип приложения - не Qt,

Qt Creator 4.5.0 основан на Qt 5.10.0. Win7.

 

Юрий Герко
Юрий Герко

Кому удалось собрать пример из раздела 13.2 Компоновка (Layouts)? Если создавать проект по изложенному алгоритму, автоматически не создается  файл mainwindow.cpp. Если создавать этот файл вручную и добавлять в проект, сборка не получается - компилятор сообщает об отсутствии класса MainWindow. Как правильно выполнить пример?