Скачать презентацию Перегрузка операций для класса Лекция 24 02 2014 Скачать презентацию Перегрузка операций для класса Лекция 24 02 2014

Лекция-2.02.ppt

  • Количество слайдов: 31

Перегрузка операций для класса Лекция 24. 02. 2014 г. 1 Перегрузка операций для класса Лекция 24. 02. 2014 г. 1

Перегрузка операций для класса «Поведение» объектов новых (пользовательских) типов данных должно быть «ожидаемым» , Перегрузка операций для класса «Поведение» объектов новых (пользовательских) типов данных должно быть «ожидаемым» , похожим на поведение объектов встроенных типов. Например, если для объекта имеет смысл операция «сложить» , то для выполнения такой операции должен использоваться привычный знак «+» . Эту ситуацию можно охарактеризовать как совместимость с соглашениями для встроенных типов. Например: class My. Complex { … }; My. Complex a, b, c, d; a = b + c * d; Данное выражение должно вычисляться привычным для нас образом. Об этом нужно специально побеспокоиться, перегрузив для класса Complex операции «=» , «*» и «+» . class My. Complex { … My. Complex operator+( const My. Complex& arg 2) { return My. Complex( re + arg 2. re, im + arg 2. im); } }; Лекция 24. 02. 2014 г. 2

Перегружать можно большинство операций C++, включая унарные операции, операции new, delete, (), [], ->*, Перегружать можно большинство операций C++, включая унарные операции, операции new, delete, (), [], ->*, а также операцию "запятая". Нельзя перегружать следующие операции: операцию выбора члена класса или элемента структуры (точка); условную (тернарную) операцию; операцию sizeof; операцию разрешения области видимости (двойное двоеточие). Хотя операции при перегрузке могут приобретать новый смысл, их приоритет и порядок выполнения не зависят от типов операндов и регулируются правилами языка. Пишем в программе a b Вызывается функция – член класса a. operator ( b ) Вызывается внешняя функция operator ( a, b ) a * b a a. operator*( b ) a. operator ( ) operator*( a, b ) operator ( a ) !a a. operator!( ) operator!( a ) Лекция 24. 02. 2014 г. 3

При создании перегруженных операций необходимо следовать определённым правилам: выбор символа для перегруженной операции должен При создании перегруженных операций необходимо следовать определённым правилам: выбор символа для перегруженной операции должен позволять с большой долей вероятности правильно идентифицировать операцию; операция класса должна быть совместима с соглашениями относительно перегружаемой операции для встроенных типов, т. е. «поведение» этой операции должно соответствовать поведению операции для встроенных типов; операция класса должна поддерживать разумные автоматические преобразования типов, поэтому необходимо следовать правилу: «Объявляйте перегружаемую операцию внешней функцией, когда преобразование типа должно быть применимо ко всем параметрам» ; необходимо с предельной тщательностью подходить к выбору типов аргументов и типа возвращаемого значения. По каждому из них необходимо дать обоснованные ответы на два вопроса: 1) сам объект (т. е. значение) или ссылка? 2) константа или не константа? Лекция 24. 02. 2014 г. 4

Рекомендации по перегрузке операций Операция Все унарные операции = () [] -> ->* Рекомендуемая Рекомендации по перегрузке операций Операция Все унарные операции = () [] -> ->* Рекомендуемая форма Функция-член класса Обязательно функция-член класса Функция-член класса += -= *= ^= &= |= %= >>= <<= Остальные бинарные Внешняя функция операции Лекция 24. 02. 2014 г. 5

Правильные форматы перегрузки унарных операций: const C& operator+() const {return *this; } const C Правильные форматы перегрузки унарных операций: const C& operator+() const {return *this; } const C operator-() const {return C(…); } const C operator~() const {return C(…); } C operator!() const {return C(…); } C* operator&() const {return this; } const C& operator++() { … return *this; } //prefix const C operator++(int) {return …; } //postfix const C& operator--() { … return *this; } //prefix const C operator--(int) {return …; } //postfix Лекция 24. 02. 2014 г. 6

Правильный формат перегрузки операции = C& operator=(const C&) { … return *this; } Правильный Правильный формат перегрузки операции = C& operator=(const C&) { … return *this; } Правильный формат перегрузки бинарных операций: friend const C operator+(const C&, const C&); Правильный формат перегрузки операций потокового ввода и вывода: friend ostream& operator<<(ostream&, const C&); friend istream& operator>>(istream&, C&); Лекция 24. 02. 2014 г. 7

Пример: комплексные числа Лекция 24. 02. 2014 г. 8 Пример: комплексные числа Лекция 24. 02. 2014 г. 8

Модификация класса My. Complex //My. Complex. h #include <iostream> using namespace std; class My. Модификация класса My. Complex //My. Complex. h #include using namespace std; class My. Complex { double re; double im; public: My. Complex( double real =0. 0, double image =0. 0) { re = real; im = image; } My. Complex& operator=(const My. Complex&); const My. Complex operator~() const; //комплексно сопряженное число const double operator!() const; //квадрат модуля const My. Complex operator/(const double) const; // деление на число friend const My. Complex operator+(const My. Complex&, const My. Complex&); friend const My. Complex operator-(const My. Complex&, const My. Complex&); friend const My. Complex operator*(const My. Complex&, const My. Complex&); friend const My. Complex operator/(const My. Complex&, const My. Complex&); friend ostream& operator<<(ostream&, const My. Complex&); }; Лекция 24. 02. 2014 г. 9

Модификация класса My. Complex //My. Complex. cpp #include <iostream> #include Модификация класса My. Complex //My. Complex. cpp #include #include "My. Complex. h" using namespace std; My. Complex& My. Complex: : operator=(const My. Complex& z) {re = z. re; im = z. im; return *this; } const My. Complex: : operator/(const double r) const { return My. Complex(re / r, im / r); } const My. Complex: : operator~() const { return My. Complex(re, -im); } const double My. Complex: : operator!() const { return re*re + im*im; } const My. Complex operator+(const My. Complex& l, const My. Complex& r) { return My. Complex(l. re + r. re, l. im + r. im); } const My. Complex operator-(const My. Complex& l, const My. Complex& r) { return My. Complex(l. re - r. re, l. im - r. im); } const My. Complex operator*(const My. Complex& l, const My. Complex& r) { return My. Complex(l. re*r. re - l. im*r. im, l. re*r. im + l. im*r. re); } const My. Complex operator/(const My. Complex& l, const My. Complex& r) { return l * ~r / !r; } ostream& operator<<(ostream& s, const My. Complex& z) { s << "(" << z. re << ")+i*(" << z. im << ")"; return s; } Лекция 24. 02. 2014 г. 10

Тестирование класса My. Complex //Ex 070. cpp #include <cstdlib> #include <iostream> #include Тестирование класса My. Complex //Ex 070. cpp #include #include #include "My. Complex. h" using namespace std; int main() { My. Complex x(1. , 2. ), y(3. , 4), z(2, 1), t = x * y / z; cout << "x = " << x << "; y = " << y << endl; cout << "z = " << z << "; t = " << t << endl; cout << "x+1 = " << x+1 << endl; cout << "1+x = " << 1+x << endl; cout << "~x/2 = " << ~x / 2 << endl; cout << "!y = " << !y << endl; cout << « 10/z = " << 10 / z << endl; system("PAUSE"); return 0; } Лекция 24. 02. 2014 г. 11

Почему бинарные операции должны перегружаться внешними функциями? class My. Complex {. . . const Почему бинарные операции должны перегружаться внешними функциями? class My. Complex {. . . const My. Complex operator+(const My. Complex&); . . . }; const My. Complex: : operator+(const My. Complex& r) { return My. Complex(re + r. re, im + r. im); }. . . My. Complex x(1. , 2. ); cout << "x+1 = " << x+1 << endl; // будет вызван My. Complex(1) cout << "1+x = " << 1+x << endl; // ошибка, т. к. 1 – не объект! x+1 эквивалентно x. operator+(My. Complex(1)) – ошибок нет 1+x эквивалентно 1. operator+(x) – ошибка, т. к. 1 – не объект! Если же operator+ перегружена внешней функцией, то x+1 эквивалентно operator+(x, My. Complex(1)) – ошибок нет 1+x эквивалентно operator+(My. Complex(1), x) – ошибок нет Лекция 24. 02. 2014 г. 12

Перегрузка операции индексации массива [] Вернемся к классу Vector и усовершенствуем его, чтобы с Перегрузка операции индексации массива [] Вернемся к классу Vector и усовершенствуем его, чтобы с объектами этого класса можно было работать как с массивами. //Vector. h - объявление класса Vector #include using namespace std; class Vector { private: int sz; double* elem; int space; static const int DEFAULT_SPACE; static double DUMMY; void swap(int, int); void reserve(int); public: Vector(int = DEFAULT_SPACE); // Конструктор 1 Vector(double*, int); // Конструктор 2 ~Vector(); // Деструктор double& operator[](int); // Доступ по индексу int operator!(); // Размер вектора Vector& operator^(int); // Изменение размера Vector& operator<<(double); // Добавление в "хвост" void Qsort(int, int); // Сортировка friend ostream& operator<<(ostream&, Vector&); // Вывод в поток Лекция 24. 02. 2014 г. }; 13

Перегрузка операции индексации массива [] //Vector. cpp #include Перегрузка операции индексации массива [] //Vector. cpp #include "Vector. h" double Vector: : DUMMY = 0. ; double& Vector: : operator[](int i){ // Доступ по индексу if( (i < 0) || (i >= sz)) return DUMMY; return elem[i]; } Оператор [] возвращает double&, т. е. ссылку на элемент вектора. Это позволяет использовать возвращаемое значение как в левой, так и в правой части выражения присваивания. Если бы оператор [] возвращал просто double, то содержимое элемента вектора копировалось бы, а копия возвращалась вызывающей стороне. Возвращая double&, мы позволяем вызывающей стороне изменить содержимое элемента, а не только прочитать хранящееся в нем значение. Для индекса, выходящего за границы массива, возвращается адрес фиксированной переменной класса DUMMY, значение которой на самом деле нас не очень интересует. Лекция 24. 02. 2014 г. 14

Модификация класса Vector //Vector. cpp #include Модификация класса Vector //Vector. cpp #include "Vector. h“ int Vector: : operator!() {return sz; } // Размер вектора Vector& Vector: : operator^(int newsize) { // Изменение размера reserve(newsize); for(int i = sz; i < newsize; ++i) elem[i] = 0; sz = newsize; return *this; } Vector& Vector: : operator<<(double d) { // Добавление в "хвост" if (sz == 0) reserve(8); else if (sz == space) reserve(2*space); elem[sz++] = d; return *this; } ostream& operator<<(ostream& s, Vector& v) { // Вывод в поток if(!v == 0) s << "Vector is empty. "; else { s << "Vector = ["; for(int i = 0; i < !v; ++i) { s << v[i]; if(i == !v-1) s << "]n"; else s << ", "; } } return s; } Лекция 24. 02. 2014 г. 15

Тестирование класса Vector //Ex 060. cpp #include <cstdlib> #include <iostream> #include Тестирование класса Vector //Ex 060. cpp #include #include #include "Vector. h" using namespace std; int main() { Vector v(10); for(int i = 0; i < 10; ++i) v << (10 -i)*(10 -i); // 10 элементов v << 10 << 20 << 30; // Еще 3 элемента cout << v; cout << (v ^ 15); // Увеличиваем размер до 15 элементов v. Qsort(0, !v - 1); cout << v; // Сортируем cout << (v ^ 5); // Уменьшаем размер до 5 элементов v[0] = 200; v[!v-1] = 300; cout << v; // Изменяем первый и последний v. Qsort(0, !v - 1); cout << v; // Сортируем system("PAUSE"); return 0; } Лекция 24. 02. 2014 г. 16

Оператор [] с нецелыми аргументами Массив – это частный случай индексируемой коллекции, в которой Оператор [] с нецелыми аргументами Массив – это частный случай индексируемой коллекции, в которой один объект может использоваться в качестве индекса (ключа) для получения другого объекта. Пример – англо-русский словарь (класс Dictionary). //Dictionary. h #include #include using namespace std; struct Pair { string eng, rus; Pair(string e = "", string r = ""): eng(e), rus(r) {} friend ostream& operator<<(ostream&, Pair&); // Вывод в поток }; class Dictionary { Pair* dic; int sz; int space; static const int DEFAULT_SPACE; static Pair P_DUMMY; static string S_DUMMY; void reserve(int); public: Dictionary(int = DEFAULT_SPACE); // Конструктор ~Dictionary(); // Деструктор Pair& operator[](int i); string& operator[](const string& eng); int operator!(); // Размер словаря Dictionary& operator<<(Pair); // Добавление в "хвост" friend ostream& operator<<(ostream&, Dictionary&); // Вывод в поток }; Лекция 24. 02. 2014 г. 17

Реализация методов класса Dictionary //Dictionary. cpp #include <iostream> #include <string> #include Реализация методов класса Dictionary //Dictionary. cpp #include #include #include "Dictionary. h" using namespace std; const int Dictionary: : DEFAULT_SPACE = 10; Pair Dictionary: : P_DUMMY = Pair("", ""); string Dictionary: : S_DUMMY = ""; Dictionary: : Dictionary(int n ) { // конструктор 1 sz = 0; if(n > 0) {dic = new Pair[n]; space = n; } else {dic = NULL; space = 0; } } Dictionary: : ~Dictionary() { sz = space = 0; delete[] dic; } int Dictionary: : operator!() { return sz; } Pair& Dictionary: : operator[](int i){ // Доступ по индексу if( (i < 0) || (i >= sz)) return P_DUMMY; return dic[i]; } string& Dictionary: : operator[](const string& w) { // Переводчик if(sz == 0) { cout << "Dictionary is empty. "; return S_DUMMY; } for(int i = 0; i < sz; ++i) if(w == dic[i]. eng) return dic[i]. rus; else if(w == dic[i]. rus) return dic[i]. eng; cout << "Dictionary failure. "; return S_DUMMY; } Лекция 24. 02. 2014 г. 18

Реализация методов класса Dictionary (продолжение) //Dictionary. cpp Dictionary& Dictionary: : operator<<(Pair d) { // Реализация методов класса Dictionary (продолжение) //Dictionary. cpp Dictionary& Dictionary: : operator<<(Pair d) { // Пополнение словаря if (sz == 0) reserve(8); else if (sz == space) reserve(2*space); dic[sz++] = d; return *this; } void Dictionary: : reserve(int newalloc) { if (newalloc <= space) return; Pair* p = new Pair[newalloc]; for (int i=0; i " << p. rus; return s; } ostream& operator<<(ostream& s, Dictionary& v) { // Вывод словаря в поток if(!v == 0) s << "Dictionary is empty. "; else { s << "Dictionary: n"; for(int i = 0; i < !v; ++i) s << v[i] << endl; } return s; } Лекция 24. 02. 2014 г. 19

Тестирование класса Dictionary //Ex 080. cpp #include <cstdlib> #include <iostream> #include <string> #include <locale> Тестирование класса Dictionary //Ex 080. cpp #include #include #include #include #include "Dictionary. h" using namespace std; int main() { Dictionary D; setlocale(LC_ALL, "Russian_Russia. 1251"); D << Pair("cat", "кот") << Pair("tree", "дерево") << Pair("house", "дом"); D << Pair("man", "человек") << Pair("city", "город") << Pair("country", "страна"); cout << D << endl; cout << D[1] << endl; cout << "man : " << D["man"] << endl; D["man"] = "Человек"; cout << "man : " << D["man"] << endl; cout << "дом : " << D["дом"] << endl; cout << "dog : " << D["dog"] << endl; cout << "стол : " << D["стол"] << endl; system("PAUSE"); return 0; } Лекция 24. 02. 2014 г. 20

Имитация многомерных массивов В любом варианте перегрузки оператор [] может иметь только один аргумент Имитация многомерных массивов В любом варианте перегрузки оператор [] может иметь только один аргумент произвольного типа. Например, следующий фрагмент кода вызовет сообщение об ошибке: class Array. Of. Type { public: Type& operator[] (int x, int y); // Ошибка! }; Однако можно предложить «обходной путь» с использованием простой структуры Index, имитирующей набор индексов многомерного массива: struct Index { int x, y; Index(int x 0, int y 0) : x(x 0), y(y 0) {} bool operator==(const Index& i) { return x == i. x && у == i. y; } }; Лекция 24. 02. 2014 г. 21

Ошибка исчезает с использованием структуры Index, т. к. теперь функция operator[] имеет один аргумент: Ошибка исчезает с использованием структуры Index, т. к. теперь функция operator[] имеет один аргумент: class Array. Of. Type { public: Type& operator[] (Index i); // Нет ошибки }; . . . array[Index(17, 29)]. . . Выражение Index(17, 29) создает анонимный экземпляр структуры, который упаковывает два измерения массива в один аргумент. Лекция 24. 02. 2014 г. 22

Копирование объектов класса Проблемы копирования объектов и понятие поверхностного копирования Копирование объекта может потребоваться Копирование объектов класса Проблемы копирования объектов и понятие поверхностного копирования Копирование объекта может потребоваться как при его инициализации в момент объявления, так и при выполнении операции присваивания: My. Complex a( 5, 3); My. Complex b = a; // Инициализация копированием void Copy. Complex( My. Complex& a) { My. Complex b; b = a; // Копирование присваиванием } В обоих случаях подразумевается почленное копирование объекта (т. е. побитовое копирование областей памяти), иначе называемое поверхностным копированием. В том случае, когда некоторые поля объекта являются указателями, такая интерпретация копирования может не совпадать с пожеланиями программиста. Лекция 24. 02. 2014 г. 23

Пример: копирование объектов класса Vector v(10); Vector t(20); t = v; v Vector elem Пример: копирование объектов класса Vector v(10); Vector t(20); t = v; v Vector elem 10 t Vector elem В результате такого присваивания значение указателя elem объекта t будет указывать на ту же память, что и указатель elem объекта v, т. е. результатом поверхностного копирования стало копирование значения указателя. Любое последующее изменение состояния объекта v, будет воздействовать и на объект t. 20 потеряна! Лекция 24. 02. 2014 г. 24

Проблемы поверхностного копирование объектов класса Vector Помимо потери памяти, которую первоначально занимал объект t, Проблемы поверхностного копирование объектов класса Vector Помимо потери памяти, которую первоначально занимал объект t, существуют и другие, более серьезные проблемы поверхностного копирования: 1. Если выполнить операцию v ^ 15 (увеличение размера вектора v до 15), то вектор v «переедет» на новое место, а область памяти, которую он занимал, будет освобождена. Но вектор t, который «прикреплен» к той же памяти, ничего об этом не «знает» и будет пытаться использовать несуществующую память! 2. Даже если не изменять размеры векторов v и t, все равно останется проблема корректного выполнения деструкторов: после выхода из блока первый же вызов деструктора освободит «общую» память, и уже вызов деструктора для второго объекта приведет к ошибке! Лекция 24. 02. 2014 г. 25

Реализация корректной семантики копирования объектов Желательным поведением программы в приведенных примерах было бы копирование Реализация корректной семантики копирования объектов Желательным поведением программы в приведенных примерах было бы копирование содержимого памяти, связанного с указателем elem, a не копирование самого указателя. Для исправления семантики копирования, принятой по умолчанию, программист должен явно определить подходящий алгоритм для копирующей инициализации и копирующего присваивания применительно к конкретной ситуации. Достигается это явным определением специального конструктора (называемого копирующим конструктором) и перегрузки операции присваивания для объектов данного класса. Применительно к Vector, решение может выглядеть так: Лекция 24. 02. 2014 г. 26

Копирующий конструктор и перегрузка операции присваивания //Vector. h - объявление класса Vector class Vector Копирующий конструктор и перегрузка операции присваивания //Vector. h - объявление класса Vector class Vector {. . . public: Vector(const Vector&); // Копирующий конструктор Vector& operator=(const Vector&); // Операция присваивания. . . }; //Vector. cpp Vector: : Vector(const Vector& v) { // Копирующий конструктор sz = v. sz; space = v. space; elem = new double[space]; for(int i = 0; i < sz; i++) elem[i] = v. elem[i]; } Vector& Vector: : operator=(const Vector& v) { if( this != &v){ // Копируем, если не ссылается сам на себя delete [] elem; sz = v. sz; space = v. space; elem = new double[space]; for(int i = 0; i < sz; i++) elem[i] = v. elem[i]; } return *this; } Лекция 24. 02. 2014 г. 27

Тестирование модифицированного класса Vector //Ex 060_Vector. cpp #include <cstdlib> #include <iostream> #include Тестирование модифицированного класса Vector //Ex 060_Vector. cpp #include #include #include "Vector. h" using namespace std; int main() { Vector v(10); for(int i = 0; i < 10; ++i) v << (10 -i)*(10 -i); v << 10 << 20 << 30; cout << "v : " << v; Vector t = v; cout << "t : " << t; cout << (v ^ 15); v. Qsort(0, !v - 1); cout << v; cout << (v ^ 5); v[0] = 200; v[!v-1] = 300; cout << v; v. Qsort(0, !v - 1); cout << v; t = v; cout << "t : " << t; system("PAUSE"); return 0; } Лекция 24. 02. 2014 г. 28

Такой алгоритм реализации копирования объектов называют глубинным копированием, или рекурсивным копированием, имея в виду, Такой алгоритм реализации копирования объектов называют глубинным копированием, или рекурсивным копированием, имея в виду, что в некоторых случаях объекты могут указывать сами на себя, следовательно, явная реализация копирования может оказаться рекурсивной по своей природе. В большинстве случаев одновременно определяют и копирующий конструктор, и перегруженную операцию присваивания. Однако иногда встречаются ситуации, когда, например, объекты разрешается инициализировать копированием, но в дальнейшем запрещается изменять посредством присваивания. Это достигается помещением объявления перегруженной операции присваивания в закрытую часть определения класса. Если правильное поведение объекта вообще исключает инициализацию копированием и использование копирующего присваивания, то достаточно сделать закрытыми и копирующий конструктор, и операцию присваивания. Лекция 24. 02. 2014 г. 29

Особенности перегрузки операций приведения типов Ниже представлен пример класса A, в котором содержится две Особенности перегрузки операций приведения типов Ниже представлен пример класса A, в котором содержится две операции неявного приведения типов: float -> A и A-> float. class A { float f; public: A( float f 0 ): f(f 0) { } // конструктор приведения типа operator float() { return f; } // операция приведения }; int main(void) { A x = 5, x 1 = 20; float y = x + 5, y 1 = x 1 + 5; cout << y << " " << y 1 << 'n'; system("PAUSE"); return 0; } Лекция 24. 02. 2014 г. 30

Операции приведения между классами. Если требуется выполнять приведения между экземплярами классов A и B, Операции приведения между классами. Если требуется выполнять приведения между экземплярами классов A и B, необходимо определить соответствующие операции приведения, которые можно разместить в любом из классов (A или B). Нельзя определить приведение между классами, находящимися в одной цепочке наследования (т. к. приведения такого рода уже существуют). class A { float f; public: A(float f 0): f(f 0) { } }; class B { float f; public: B(float f 0): f(f 0) { } operator A() { return A(f); } }; Лекция 24. 02. 2014 г. 31