
7. Перегрузка (переопределение) операций
С++ позволяет переопределить действие большинства операций, так чтобы при использовании с объектами конкретного класса они выполняли заданные действия. Операции могут быть перегружены глобально или в пределах класса. Как и в случае перегруженных функций С++ в целом, компилятор определяет различия в операциях по контексту вызова: по числу и типам аргументов операндов.
Переопределенные операции реализуются как функции с помощью ключевого слова operator. Имя перегруженной функции должно быть operatorХ, Х – перегруженный оператор.
Ключевое слово operator, за которым следует символ оператора, называется именем операторной функции.
Переопределение может быть выполнено для целого ряда операций, некоторые из которых для примера приведены в таблице:
Оператор |
Название |
Тип |
, |
запятая |
Бинарный |
! |
Логическое не |
Унарный |
!= |
Не равно |
Бинарный |
& |
Адрес |
Унарный |
* |
Умножение |
Бинарный |
+ |
Сложение |
Унарный |
- |
Вычитание |
Унарный |
++ |
Инкремент |
Унарный |
= |
Присваивание |
Бинарный |
< |
Меньше |
Бинарный |
<< |
Сдвиг влево |
Бинарный |
== |
Равно |
Бинарный |
delete |
Удаление |
- |
Например, чтобы перегрузить оператор сложения, нужно определить функцию с именем operator+, а чтобы перегрузить оператор сложения с присваиванием, нужно определить функцию operator+=.
Обычно компилятор вызывает эти функции неявно, когда перегруженные операторы встречаются в коде программы. Тем не менее, их можно вызывать и непосредственно.
Однако есть операции, которые нельзя перегружать
. |
Выбор члена |
.* |
Выбор члена по указателю |
:: |
Оператор расширения области видимости |
?: |
Оператор условия |
# |
Препроцессорный символ |
## |
Препроцессорный символ |
Операторные функции перегруженных операций, за исключением new и delete должны подчиняться следующим правилам:
Операторная функция должна быть либо нестатической функцией-членом класса, либо принимать аргумент типа класса или перечислимого типа, либо аргумент, который является ссылкой на тип класса или перечислимый тип.
Операторная функция не может изменять число аргументов или приоритеты операций и их порядок выполнения по сравнению с использованием соответствующего оператора для встроенных типов данных.
Операторная функция унарного оператора, объявленная как функция член, не должна иметь параметров. Если она объявлена как глобальная функция, она должна иметь один параметр.
Операторная функция бинарного оператора, объявленная как функция член, должна иметь один параметр. Если она объявлена как глобальная функция, она должна иметь два параметра.
Операторная функция не может иметь параметров по умолчанию.
Первый параметр операторной функции (если он есть) объявленной как функция-член, должна иметь тип класса объекта, для которого вызывается соответствующий оператор. Никакие преобразования типа для первого оператора не выполняются.
За исключением операторной функции оператора присваивания операторные функции класса наследуются его производными классами.
Операторные функции =, [], (), -> должны быть нестатическими функциями-членами и не могут быть глобальными.
Для переопределения операции используется особая форма элемента-функции с заголовком такого вида:
[friend] <тип> operator <операция> (<список параметров-операндов>)
При этом:
Имя функции состоит из ключевого слова operator и символа данной операции в синтаксисе языка Си {operator <операция> }.
Список формальных параметров функции является списком операндов (количество, типы, способы передачи) операции{список параметров-операндов }. .
Результат функции (тип, способ передачи) является результатом переопределяемой операции{ <тип> }.
Способ передачи и тип указывают на возможности использования результата в других выражениях.
Имеется два способа описания функции, соответствующей переопределяемой операции:
1. Если функция задается как обычная элемент-функция класса, то первым аргументом соответствующей операции является объект, ссылка на который передается неявным параметром this;
2. Если первым аргументом переопределяемой операции не является объект некоторого класса, либо функция получает на вход не ссылку на объект, а сам объект, тогда соответствующая элемент-функция должна быть определена как дружественная с полным списком аргументов. Естественно, что полное имя дружественной функции-оператора не содержит при этом имени класса.
Операция-функция, вызываемая с аргументами, ведет себя как операция, выполняющая определенные действия с операндами в выражении. Операция-функция изменяет число аргументов или правила приоритета и ассоциативности операции, сравнительно с ее нормальным использованием.
Рассмотрим небольшой пример. Пусть рассматривается комплексное число с двумя параметрами - действительная и мнимая части. И нужно сложить два комплексных числа. По известным уже правилам мы это сделаем следующим образом:
class TComplex
{
double real, imag;
public:
TComplex ()// встроенный конструктор
{ real = imag = 0; }
TComplex (double r=0, double i = 0)//еще один конструктор
{
real = r; imag = i;
}
}
Мы можем легко разработать функцию для сложения комплексных чисел, например,
TComplex Add(TComplex c1, TComplex c2);
Однако будет более естественным и удобным иметь возможность записать:
TComplex c1(0,1), c2(1,0), c3
c3 = c1 + c2;
вместо
c3 = AddComplex(c1, c2);
Заметим, что с помощью перегрузки можно совершенно изменить смысл операторов для некоторого класса. Однако рекомендуется не делать этого, т.к. это затрудняет понимание программы. Перегруженный оператор, как правило, должен следовать семантике его поведения для встроенных типов.
Перегрузка бинарных операторов.
Итак, операторная функция бинарного оператора может быть объявлена как нестандартная функция-член, и может быть объявлена глобальной т.е. с помощью дружественности.
Когда операторная функция бинарного оператора объявляется как нестандартная функция- член, она должна быть объявлена в виде:
<возв.тип> operator Х (<тип параметра> par);
где: <возв.тип> – тип возвращаемого функцией результата,
Х – перегружаемый оператор,
<тип параметра> и par – тип параметра и сам параметр, который передается в функцию. В этот параметр будет передан тот объект, который в самом выражении (в описании операторной функции) стоит справа от оператора. Объект, стоящий слева от оператора, передается неявно с помощью указателя this.
Рассмотрим пример, в котором в качестве объекта представляется точка с координатами x, y. Надо организовать операторную функцию сложения и вычитания для координат двух точек.
class Coord
{
int x, y;
public:
Coord()
{
x=0;y=0;
}
Coord(int _x, int _y )
{
x=_x; y=_y;
}
void Print()
{
cout <<"x="<<x<<"y="<<y<<endl;
}
Coord operator+(Coord& ob);
Coord operator-(Coord& ob);
};
//===============================================
Coord Coord::operator+(Coord& ob)
{
Coord temp_ob;
temp_ob.y=y+ob.y ;
temp_ob.x=x+ob.x ;
return temp_ob;
}
//===============================================
Coord Coord::operator-(Coord& ob)
{Coord temp_ob;
temp_ob.y=y-ob.y ;
temp_ob.x=x-ob.x ;
return temp_ob;
}
//================================
void main()
{
Coord a(4,6),b(7,8),c;
c=a+b;
c.Print();
c=a-b;
c.Print();
}
Когда операторная функция бинарного оператора объявляется как глобальная, она должна быть объявлена в виде:
<возв.тип> operator Х (<тип парам_1> par_1, <тип парам_2> par_2);
где: <возв.тип> – тип возвращаемого функцией результата,
Х – перегружаемый оператор,
<тип парам_1> par_1 и <тип парам_2> par_2 – типы параметров и сами параметры, которые передаются в функцию. По крайней мере один из этих параметров должен иметь тип класса, для которого перегружается оператор.
Хотя особых ограничений на тип возвращаемого параметра бинарных операций нет, большинство операторов возвращают либо тип класса, либо ссылку на тип класса.
Рассмотрим пример тот же пример, но операторная функция, которую надо организовать будет глобальной.
class Tpoint
{
int x,y;
public:
Tpoint()
{
x=0;
y=0;
printf("работал конструктор без параметров");
}
Tpoint(int xx, int yy)
{
x=xx;
y=yy;
printf(" работал конструктор с параметрами ");}
void pechat();
friend Tpoint operator+(Tpoint& P1, Tpoint& P2);
~Tpoint(){printf("\n работал деструктор");}
};
//===============================================
void Tpoint::pechat()
{
cout<<endl;
cout<< x <<" "<<y<<endl;
}
//===============================================
Tpoint operator+(Tpoint& P1, Tpoint& P2)
{
Tpoint g;
g.x=P1.x+P2.x;
g.y=P1.y+P2.y;
return g;
}
//===============================================
void main()
{
clrscr();
Tpoint a(1,2);
a.pechat();
Tpoint b(10,20);
b.pechat();
Tpoint c;
c=a+b;
c.pechat();
cout<<endl;
getch();
}
Рассмотрим еще один вариант примера, когда передаваемый параметр имеет тип, отличный от типа класса, в данном случае - целый.
class Tpoint
{
int x,y;
public:
Tpoint()
{x=0;
y=0;
printf(" konstryktor bez param");
}
Tpoint(int xx, int yy)
{
x=xx;
y=yy;
printf(" konstryktor c param ");
}
void pechat();
Tpoint operator+(int n);
~Tpoint(){printf("\n destryktor");}
};
//=======================================
void Tpoint::pechat()
{
cout<<endl;
cout<< x <<" "<<y<<endl;
}
//=======================================
Tpoint Tpoint::operator+(int n)
{
Tpoint g;
g.x=x+n;
g.y=y+n;
return g;
}
//=======================================
void main()
{clrscr();
Tpoint a(1,2);
a.pechat();
Tpoint c;
c=a+22;
c.pechat();
cout<<endl;
getch();
}
Перегрузка оператора присваивания.
Оператор присваивания также является бинарным, но процедура его перегрузки имеет ряд особенностей:
операторная функция оператора присваивания не может быть объявлена глобальной;
операторная функция оператора присваивания не наследуется;
компилятор может сгенерировать операторную функцию оператора присваивания, если она неопределенна в классе.
Оператор присваивания по умолчанию, сгенерированный компилятором выполняет по членное присваивание нестатических членов класса. Здесь такая же ситуация как и с генерируемым конструктором копирования по умолчанию: результат окажется неприемлемым, если класс содержит указатель
Левый операнд после выполнения оператора присваивания меняется, так как ему присваивается новое значение. Поэтому функция оператора присваивания должна возвращать ссылку на объект, для которого она вызвана (или которому присваивается значение), если мы хотим сохранить семантику оператора присваивания для встроенных типов данных. Проще всего это сделать, возвратив разыменованный указатель this. Это, кстати, позволяет использовать перегруженный оператор присваивания в выражениях вида: Р3=Р2=Р1.
class Tpoint
{
int x,y;
public:
Tpoint()
{
x=0;y=0;
printf("работал конструктор без параметров");
}
Tpoint(int xx, int yy)
{
x=xx; y=yy;
printf("работал конструктор с параметрами");
}
void pech();
Tpoint& operator=(Tpoint& P2);
~Tpoint()
{
printf("\n деструктор");
}
};
//=======================================
Tpoint& Tpoint::operator=(Tpoint& P2)
{
x=P2.x;
y=P2.y;
return *this;
}
//=======================================
void Tpoint::pech()
{
cout<<endl;
cout<< x <<" "<<y<<endl;
}
//=======================================
void main()
{
Tpoint a(1,2);
a.pech();
Tpoint b(10,20);
b.pech();
Tpoint c;
c=a;
c.pech();
cout<<endl;
c=b;
c.pech();
cout<<endl;
}
Рассмотрим пример, в котором объектами служат массивы и поля объявлены с помощью указателей. Учитывая вышеуказанные требования, в этом случае обязательно должен быть, использован конструктор копии.
В качестве задания рассмотрим организацию получения результата сложения поэлементно двух матриц.
class matr
{
float *x;
int n,m;
public:
matr(int, int);
matr (matr& f);
void vvod();
void ww();
friend matr operator+(matr& ob1, matr& ob2);
matr& operator=(matr& ob2);
~matr();
};
//==============================================
//конструктор с параметрами
matr::matr(int _n, int _m)
{ n=_n; m=_m;
x=new float [n*m];
puts("\n работал конструктор ");
}
//==============================================
//конструктор копии
matr::matr(matr& f)
{
n=f.n;
m=f.m;
x=new float [n*m];
for(int i=0; i<n; i++)
for(int j=0; j<m; j++)
*(x+i*m+j)=*(f.x+i*f.m+j);
cout <<" работал конструктор копии ";
}
//==============================================
//функция ввода численных данных