
- •Конспект лекций по объектно-ориентированному программированию
- •Оглавление
- •Литература
- •Концепция объектно-ориентированного программирования
- •Расширение языка c
- •Абстрактные типы данных
- •Дружественные функции
- •Перегрузка операций
- •Производные классы
- •Параметрический полиморфизм
- •Библиотека ввода-вывода iostream.H
Перегрузка операций
Перегружаемые операторы. Кроме перегрузки функций С++ позволяет организовать перегрузку операций. Механизм перегрузки операций позволяет обеспечить более традиционную и удобную запись действий над объектами. Для перегрузки встроенных операторов используется ключевое слова operator. Синтаксически перегрузка операций осуществляется следующим образом:
тип operator @ (список_параметров-операндов)
{// ... тело функции ...}
@ — знак перегружаемой операции (-, +, * и т. д.),
тип — тип возвращаемого значения.
Тип возвращаемого значения должен быть отличным от void, если необходимо использовать перегруженную операцию внутри другого выражения.
Например, функция перемножения матрицы и вектора может быть записана следующим образом:
vect operator *(const vect &v, const matrix &m)
{.....................}
Если
vect r, s;
matrix t;
то функцию умножения будет вызывать оператор
r = s * t;
Любой перегруженный оператор можно вызвать с использование функциональной формы записи (функции-операции):
r = operator* (s, t);
Функция-операция описывается и может вызываться так же, как любая другая функция. Использование операции – это лишь сокращенная запись явного вызова функции операции. Например:
void f(complex a, complex b)
{
complex c = a + b; // сокращенная запись
complex d = operator+(a,b); // явный вызов
}
Имеется два способа описания функции, соответствующей переопределяемой операции:
если функция задается как обычная функция-элемент класса, то первым операндом операции является объект класса, указатель на который передается неявным параметром this;
если первый операнд переопределяемой операции не является объектом некоторого класса, либо требуется передавать в качестве операнда не указатель, а сам объект (значение), то соответствующая функция должна быть определена как дружественная классу с полным списком аргументов.
Можно описывать функции, определяющие значения следующих операций:
-
+
-
*
/
%
^
&
|
~
!
=
<
>
+=
-=
*=
/=
%=
^=
&=
|=
<<
>>
>>=
<<=
==
!=
<=
>=
&&
||
++
--
[]
()
new
delete
Правила перегрузки операций. Перегрузка операций в языке С++ подчиняется следующему ряду правил:
Язык не допускает определения для операций нового лексического символа, кроме уже определенных в языке. Например, нельзя определить в качестве знака операции @.
Не допускается перегрузка операций для встроенных типов данных. Нельзя, например, переопределить операцию сложения целых чисел:
int operator *(int i, int j);
Нельзя переопределить приоритет операции.
Нельзя изменить синтаксис операции в выражении. Например, если некоторая операция определена как унарная, то ее нельзя определить как бинарную. Если для операции используется префиксная форма записи, то ее нельзя переопределить в постфиксную. Например, !а нельзя переопределить как а!
Так как перегружать можно только операции, для которых хотя бы один аргумент представляет тип данных, определенный пользователем, то функция-операция должна быть определена либо как функция-член класса, либо как внешняя функция, но дружественная классу.
Функция-член класса |
Дружественная функция |
class string {............... public: string operator + (const string &); ............... }; |
class string {............... public: friend string operator +(string &, string &); ............... }; |
Перегрузка унарной операции. Если унарная операция перегружается как функция-член, то она не должна иметь аргументов, так как в этом случае ей передается неявный аргумент-указатель this на текущий объект. Если унарная операция перегружается дружественной функцией, то она должна иметь один аргумент –объект, для которого она выполняется:
Функция-член класса |
Дружественная функция |
class A {............... public: A operator ! (); ............... }; |
class A {............... friend A operator ! (A); ............... }; |
Таким образом, для любой унарной операции @ aa@ или @aa может интерпретироваться или как aa.operator@(), или как operator@(aa). Если определена и та, и другая, то и aa@ и @aa являются ошибками.
Перегрузка бинарной операции. Если бинарная операция перегружается с использованием функции-члена, то в качестве своего первого аргумента она получает неявно переданную переменную класса (указатель this на объект), а в качестве второго — аргумент из списка параметров. То есть, фактически, бинарная операция, перегружаемая функцией-членом, имеет один аргумент (правый операнд), а левый передается неявно через указатель this.
class complex
{ double real;
double imag;
public:
...............
complex operator +(const complex &);
...............
};
complex complex :: operator +(complex &c)
{ complex temp;
temp.real=this->real+c.real;
temp.imag=this->imag+c.imag;
return(temp);
}
Если бинарная операция перегружается дружественной функцией, то в списке параметров она должна иметь оба аргумента:
class complex
{ double real;
double imag;
public:
friend complex operator + (const complex &c1,
const complex &c2);
};
complex operator + (const complex &c1, const complex &c2)
{ complex temp;
temp.real=c1.real+c2.real;
temp.imag=c1.imag+c2.imag;
return(temp);
}
# include <iostream.h>
# include “complex.h”
void main(void)
{ complex a(3.1, 4.5), b(2.3, 6.7); // инициализация
complex c;
c=a+b;
...............
}
Таким образом, для любой бинарной операции @ aa@bb может интерпретироваться или как aa.operator@(bb), или как operator@(aa,bb). Если определены обе, то aa@bb является ошибкой.
Необходимо отметить также и тот факт, что для каждой комбинации типов операндов в переопределяемой операции необходимо ввести отдельную функцию, то есть транслятор не может производить перестановку операндов местами, даже если базовая операция допускает это. Например, если необходима операция сложения комплексных и вещественных чисел:
complex a;
double b;
a + b;
b + a;
то необходимо переопределить операцию сложения дважды:
friend complex operator + (complex, float);
friend complex operator + (float, complex);
Перегрузка операции индексирования и вызова функций. Переопределение операции () позволяет использовать синтаксис вызова функции применительно к объекту класса (имя объекта с круглыми скобками). Количество операндов в скобках может быть любым. Переопределение операции [] позволяет использовать синтаксис доступа к элементам массива (имя объекта с квадратными скобками). Возможность переопределения операций индексирования и вызова функции рассмотрим на примере.
//------ Переопределение операций [] и ()
#include <iostream.h>
#include <string.h> // Прототип ф-ции strncpy()
class string // Строка переменной длины
{
char *str; // Динамический массив символов
int size; // Длина строки
public:
string& operator()(int,int); // Операция выделения подстроки
char operator[](int); // Операция выделения символа
void print(){cout << "Str = " << str << endl;}
string (char* s = ""){
if((size = strlen(s)))
{ str = new char [size+1];
strcpy(str, s);}
else str = 0;}
string (string& r){
if(str)delete[] str;
str = new char [r.size];
strcpy(str, r.str);
size=r.size;}
~string(){if(str){delete[] str;size = 0;}}
};
//------ Операция выделения подстроки -------------------
string& string::operator()(int n1, int n2)
{
char *tmp = new char [n2-n1+2];
strncpy(tmp, (str+n1), n2-n1+1);
delete [] str;
str = new char [n2-n1+2];
strcpy(str, tmp);
delete [] tmp;
return (*this);
}
//------ Операция выделения символа -------------------
char string::operator[](int index)
{
return (str[index]);
}
void main(void)
{
string s1("abcdefghi");
s1.print();
string s2=s1(2,4);
s2.print();
s1.print();
char ch = s2[1];
cout << “ch = ” << ch << endl;
}
Перегрузка операции присваивания. Чуть ранее, говоря о конструкторах класса, мы говорили о таком понятии как конструктор копии. Вообще любой конструктор вызывается явно либо неявно в том случае, если необходимо создать новый объект какого-либо класса. Рассматривая на примере создание нового объекта и его инициализацию, мы использовали конструктор копии. До проведения инициализации существовал только один объект. Второй был создан и инициализирован в результате работы конструктора копии. Однако, если бы существовало несколько объектов одного типа, и была бы необходимость в присвоении значений одного объекта элемента другого, то в этом случае никакой из конструкторов не вызывается, так как объект уже были созданы. При выполнении операции присваивания по умолчанию копирование значений происходит с семантикой «поверхностного копирования». Но мы уже знаем, что такое не всегда допустимо, например недопустимо копирование массивов или указателей и пр. Если необходимо осуществить присваивание, но поведение операции присваивания по умолчанию не устраивает, то она (операция) может быть перегружена. Дополним приведенный выше пример реализации класса string перегруженным оператором присваивания. Для чего в объявление класса поместим объявление перегружаемой функции-оператора
string& operator=(const string&);
А в контексте файла – определение
string& string::operator=(const string& s)
{
if(size!=s.size)
{
size = s.size;
delete[] str;
str = new char [size];
}
strcpy(str, s.str);
return (*this);
}
Преобразование типов. При работе со стандартными типами данных в C имеют место явные и неявные преобразования их типов. По аналогии для классов также могут быть определены такие операции – они ассоциируются с конструированием объектов класса. Так, если в программе встречается преобразование типа (класса) "YYY" к типу (классу) "XXX", то для его осуществления в классе "XXX" необходим конструктор вида XXX(YYY &); То есть фактически необходим конструктор с одним параметром.
Сами преобразования типов происходят в тех же самых случаях, что и обычные преобразования базовых типов данных:
при использовании операции явного преобразования типов;
при выполнении операции присваивания, если она не переопределена в виде "XXX=YYY" (транслятором создается временный объект класса "XXX", для которого вызывается указанный конструктор и который затем используется в правой части операции присваивания);
при неявном преобразовании типа формального параметра функции при передаче его по значению (вместо конструктора копирования);
при неявном преобразовании типа результата функции при передаче его по значению (вместо конструктора копирования);
при определении объекта класса "XXX" одновременно с его инициализацией объектом класса "YYY" (вместо конструктора копирования)
YYY b;
XXX a = b;
При конструировании объекта класса "XXX" с использованием объекта класса "YYY" естественно должна быть обеспечена доступность необходимых данных последнего (например, через дружественность).
В качестве примера рассмотрим преобразование базового типа char* к типу string.
class string
{
int size;
char *str;
public:
.......
string (const char *s)
{
size = strlen (s);
str = new char [size+1];
strcpy(str, s);
}
}
Такое преобразование типов доступно как явно, так и неявно.
string str1;
char *strr2 = “Добрый день”;
str1 = string (str2); // явное преобразование
При выполнении такой программы, вызывается конструктор, который создает временный объект, а затем выполняется поэлементное копирование этого временного объекта в объект str1. Заметим также, что последняя строка – это не что иное, как функциональная форма записи операции приведения типа
тип(выражение)
Таким образом, string(str2) – это явный вызов операции приведения типа char* к типу string. Возможен и неявный вызов преобразования
str1 = str2; // неявный вызов
Следовательно, пользователь может определить преобразование из любого типа (встроенного, либо класса) в тип класса, для которого определен конструктор.
Однако, для встроенных типов пользователь не может писать конструкторы, но ему может понадобиться преобразование, например из типа string к типу char*.
Такое преобразование может быть выполнено с помощью переопределения операторов приведения типа. Общий вид функции-операции приведения типа:
operator тип(){тело функции}
Операция приведения – это специальная функция-член класса без возвращаемого значения, с пустым списком параметров и именем, совпадающим с типом данных к которому необходимо произвести преобразование.
Таким образом, преобразование от типа string к типу char*может быть записано следующим образом:
operator char*(){return(str);}
Преобразование, определенное такой функцией происходит неявно в выражениях присвоения, при передаче параметров функции, в возвращаемых значениях функций.
string s;
char *str = s; // неявный вызов преобразования
Возможен и явный вызов
char *str1 = (char*) s;
typedef *char ptr_ch;
char *str2 = ptch(s);
Таким образом, можно определить преобразование для любого класса из его собственного типа в любой другой тип.
Перегрузка new, delete. Операции создания и уничтожения объектов в динамической памяти могут быть переопределены следующим образом
void *operator new(size_t size);
void operator delete (void *);
где void * – указатель на область памяти, выделяемую под объект, size – размер объекта в байтах, size_t – тип размерности области памяти, int или long.
Переопределение этих операций позволяет написать собственное распределение памяти для объектов класса.
Операции, не допускающие перегрузки. В С++ существует несколько операций, не допускающих перегрузки:
-
.
прямой выбор члена объекта класса;
.*
обращение к члену через указатель на него;
?:
условная операция;
::
операция указания области видимости;
sizeof
операция вычисления размера в байтах;
#
препроцессорная операция.