Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
OOP-CPP-AIT-2005.doc
Скачиваний:
10
Добавлен:
16.08.2019
Размер:
477.18 Кб
Скачать
  1. Перегрузка операций

Перегружаемые операторы. Кроме перегрузки функций С++ позволяет организовать перегрузку операций. Механизм перегрузки операций позволяет обеспечить более традиционную и удобную запись действий над объектами. Для перегрузки встроенных операторов используется ключевое слова 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

операция вычисления размера в байтах;

#

препроцессорная операция.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]