uzluga.ru
добавить свой файл


Перегрузка операций

  • Перегрузка операций

  • Для каждого типа данных определен состав операций, которые разрешено выполнять над объектами данного типа

  • Например, для типа данных int определены операции

  • +, -, *, /, % и т.д.

  • В C++ существует возможность представления объектов вместе с множеством действий, которые могут над этими объектами выполняться, т.е. программист может переопределять смысл операций при их применении к объектам определенного класса.


Можно описывать функции, определяющие значения следующих операций:

  • Можно описывать функции, определяющие значения следующих операций:

  • + - * / % ^ & | ~ ! = < >

  • += -= *= /= %= ^= &= |=

  • << >> >>= <<= == != <= >=

  • && || ++ --

  • [ ] () new delete

  • Изменить приоритеты перечисленных операций невозможно, как невозможно изменить и синтаксис выражений

  • (нельзя, например, определить унарную операцию % или бинарную ! )



Программист задает смысл операций с помощью определения функции с определенным именем

  • Программист задает смысл операций с помощью определения функции с определенным именем

  • Имя формируется с помощью ключевого слова operator за которым следует сама операция, например, operator+



class complex

  • class complex

  • { double re, im;

  • public:

  • complex(double r, double i){ re=r; im=i; }

  • friend complex operator+(complex, complex);

  • friend complex operator*(complex, complex);

  • };

  • Обращение к перегруженной функции:

  • void f()

  • { complex a = complex(1, 3.1); // Конструктор

  • complex b = complex(1.2, 2);

  • complex с = complex(0., 0.);

  • с = a+b; // c = operator+(a,b);

  • c = c+b*a; // c = c + (b*a)

  • c = a*b + complex(1,2);

  • }



Относительно смысла операций, определяемых пользователем, не делается никаких предположений

  • Относительно смысла операций, определяемых пользователем, не делается никаких предположений

  • Определение operator+=( ) для типа complex не может быть выведено из определений complex::operator+( ) и complex::operator=( )



Бинарные операции

  • Бинарные операции

  • Бинарная операция может быть определена или как функция член, получающая один параметр, или как функция друг, получающая два параметра.

  • aa.operator@(bb) // Функция член

  • operator@(aa,bb) // Функция друг

  • Если определены обе, то aa@bb является ошибкой



Унарные операции

  • Унарные операции

  • Унарная операция, префиксная или постфиксная, может быть определена или как функция член, не получающая параметров, или как функция друг, получающая один параметр

  • aa.operator@() // Функция член

  • operator@(aa) // Функция друг

  • Если определена и то, и другое, то и aa@ и @aa являются ошибками

  • Префиксное и постфиксное использование различить невозможно



class X

  • class X

  • { // друзья

  • friend X operator-(X); // унарный минус

  • friend X operator-(X,X); // бинарный минус

  • friend X operator-(); // ошибка: нет операндов

  • friend X operator-(X,X,X); // ошибка: тернарная

  • // члены (с неявным первым параметром: this)

  • X* operator&(); // унарное & (взятие адреса)

  • X operator&(X); // бинарное & (операция И)

  • X operator&(X,X); // ошибка: тернарное

  • };



Функция операция должна или быть членом, или получать в качестве параметра по меньшей мере один объект класса (кроме переопределения операций new и delete)

  • Функция операция должна или быть членом, или получать в качестве параметра по меньшей мере один объект класса (кроме переопределения операций new и delete)



Предопределенные операции

  • Предопределенные операции

  • Операции

  • = и &

  • имеют предопределенный смысл для объектов классов



Операции над различными типами

  • Операции над различными типами

  • Операция, первым параметром которой предполагается основной тип (int, float, double …), не может быть функцией членом

  • complex c,a;

  • c=a+2; // a.operator+(2)

  • c=2+a; // 2.operator+(aa) ?

  • Так как компилятор не знает смысла +, определенного пользователем, то не может предполагать, что он коммутативен, и интерпретировать 2+aa как aa+2



Определяемое преобразование типа

  • Определяемое преобразование типа

  • class complex

  • { double re, im;

  • public:

  • complex(double r, double i){ re=r; im=i; }

  • friend complex operator+(complex, complex);

  • friend complex operator+(complex, double);

  • friend complex operator+(double, complex);

  • friend complex operator-(complex, complex);

  • friend complex operator-(complex, double);

  • friend complex operator-(double, complex);

  • complex operator-() // унарный –

  • friend complex operator*(complex, complex);

  • friend complex operator*(complex, double);

  • friend complex operator*(double, complex);

  • // ...

  • };



Теперь мы можем написать:

  • Теперь мы можем написать:

  • void f()

  • { complex a(1,1), b(2,2), c(3,3), d(4,4), e(5,5);

  • a = -b-c;

  • b = c*2.0*c;

  • c = (d+e)*a;

  • }

  • Но писать функции для каждого сочетания complex и double, как это делалось выше для operator+( ) , невыносимо нудно.



Конструкторы - Альтернативу использованию нескольких функций (перегруженных) составляет описание конструктора

  • Конструкторы - Альтернативу использованию нескольких функций (перегруженных) составляет описание конструктора

  • class complex

  • { // ...

  • complex(double r) { re=r; im=0; }

  • };

  • Конструктор, требующий только один параметр, необязательно вызывать явно:

  • complex z1 = complex(23);

  • complex z2 = 23;

  • z1, и z2 будут инициализированы вызовом complex(23)



class complex

  • class complex

  • { double re, im;

  • public:

  • complex(double r, double i = 0) { re=r; im=i; }

  • friend complex operator+(complex, complex);

  • friend complex operator*(complex, complex);

  • };

  • a=b*2; // a=operator*(b,complex(double(2),double(0)));

  • Объект, сконструированный с помощью явного или неявного вызова конструктора, является автоматическим и будет уничтожен при первой возможности



Использование конструктора для задания преобразования типа является удобным, но имеет следствия, которые могут оказаться нежелательными:

  • Использование конструктора для задания преобразования типа является удобным, но имеет следствия, которые могут оказаться нежелательными:

    • Не может быть неявного преобразования из определенного пользователем типа в основной тип (поскольку основные типы не являются классами)
    • Невозможно задать преобразование из нового типа в старый, не изменяя описание старого


Операции преобразования

  • Операции преобразования

  • Необходимо определить для исходного типа операцию преобразования

  • Функция член X::operator T( ) , где T - имя типа, определяет преобразование из X в T



Большие объекты

  • Большие объекты

  • При каждом применении для comlpex бинарных операций, описанных выше, в функцию, которая реализует операцию, как параметр передается копия каждого операнда

  • Чтобы избежать ненужного копирования, можно описать функции таким образом, чтобы они получали ссылочные параметры



class matrix

  • class matrix

  • { double m[4][4];

  • public:

  • matrix();

  • friend matrix operator+(matrix&, matrix&);

  • friend matrix operator*(matrix&, matrix&);

  • };

  • matrix operator+(matrix&, matrix&);

  • { matrix sum;

  • for (int i=0; i<4; i++)

  • for (int j=0; j<4; j++)

  • sum.m[i][j] = arg1.m[i][j] + arg2.m[i][j];

  • return sum; // Возвращает все же объект

  • }



class matrix

  • class matrix

  • { // ...

  • friend matrix& operator+(matrix&, matrix&);

  • friend matrix& operator*(matrix&, matrix&);

  • };

  • Это является допустимым, но приводит к сложности с выделением памяти. Поскольку ссылка на результат будет передаваться из функции как ссылка на возвращаемое значение, оно не может быть автоматической переменной

  • Как правило, его размещают в свободной памяти

  • Часто копирование возвращаемого значения оказывается дешевле (по времени выполнения, объему кода и объему данных) и проще программируется



Индексирование

  • Индексирование

  • Чтобы задать смысл индексов для объектов класса используется функция operator[ ]. Второй параметр (индекс) функции operator[ ] может быть любого типа.



Рассмотрим очень простой класс строк string:

  • Рассмотрим очень простой класс строк string:

  • struct string

  • { char* p;

  • int size; // размер вектора, на который указывает p

  • public:

  • string(int sz) { p = new char[size=sz]; }

  • ~string() { delete p; }

  • };

  • void f()

  • { string s1(10); string s2(20);

  • s1 = s2;

  • }

  • На выходе из f( ) для s1 и s2 будет вызываться деструктор и уничтожать один и тот же вектор с непредсказуемо разрушительными последствиями



struct string

  • struct string

  • { char* p;

  • int size; // размер вектора, на который указывает p

  • public:

  • string(int sz) { p = new char[size=sz]; }

  • ~string() { delete p; }

  • void operator=(string&)

  • };

  • void string::operator=(string& a)

  • { if (this == &a) return; // остерегаться s=s;

  • delete p;

  • p=new char[size=a.size];

  • strcpy(p,a.p);

  • }



void f()

  • void f()

  • {

  • string s1(10); s2 = s1;

  • }

  • Теперь создается только одна строка, а уничтожается две

  • Часто операция присваивания полагается на то, что ее аргументы инициализированы. Нужно определить другую, функцию, чтобы обрабатывать инициализацию



struct string

  • struct string

  • { char* p;

  • int size; // размер вектора, на который указывает p

  • public:

  • string(int sz) { p = new char[size=sz]; }

  • ~string() { delete p; }

  • void operator=(string&);

  • string(string&);

  • };

  • void string::string(string& a)

  • { p=new char[size=a.size]; strcpy(p,a.p); }

  • Для типа X инициализацию тем же типом X обрабатывает конструктор X(X&) .



Если класс X имеет конструктор, выполняющий нетривиальную работу вроде освобождения памяти, то скорее всего потребуется полный комплект функций, чтобы полностью избежать побитового копирования объектов:

  • Если класс X имеет конструктор, выполняющий нетривиальную работу вроде освобождения памяти, то скорее всего потребуется полный комплект функций, чтобы полностью избежать побитового копирования объектов:



Предостережение

  • Предостережение

  • Как и большая часть возможностей в языках программирования, перегрузка операций может применяться как правильно, так и неправильно.

  • В частности, можно так воспользоваться возможность определять новые значения старых операций, что они станут почти совсем непостижимы.

  • Представьте, например, с какими сложностями столкнется человек, читающий программу, в которой операция + была переопределена для обозначения вычитания