Лекция 16 Дружественные функции и классы Перегрузка операций 1 Дружественные функции и классы • Иногда желательно иметь непосредственный доступ извне к скрытым полям класса, то есть расширить интерфейс класса. • Для этого служат дружественные функции и дружественные классы. 2 Дружественная функция • Дружественные функции применяются для доступа к скрытым полям класса и представляют собой альтернативу методам. Метод, как правило, описывает свойство объекта, а в виде дружественных функций оформляются действия, не являющиеся свойствами класса, но входящие в его интерфейс и нуждающиеся в доступе к его скрытым полям, например, переопределенные операции вывода объектов 3 • Дружественная функция объявляется внутри класса, к элементам которого ей нужен доступ, с ключевым словом friend. В качестве параметра ей должен передаваться объект или ссылка на объект класса, поскольку указатель this ей не передается. • Дружественная функция может быть обычной функцией или методом другого ранее определенного класса. На нее не распространяется действие спецификаторов доступа, место размещения ее объявления в классе безразлично. • Одна функция может быть дружественной сразу нескольким классами. 4 class string{ char* str; int max_lenght; friend void reverse (string&); }; void reverse(string& obj){ strupr(obj.str); strrev(obj.str); } int main() { … string my(“Hello”); reverse(my); … } 5 • Использования дружественных функций нужно по возможности избегать, поскольку они нарушают принцип инкапсуляции и, таким образом, затрудняют отладку и модификацию программы. 6 Дружественный класс • Если все методы какого-либо класса должны иметь доступ к скрытым полям другого, весь класс объявляется дружественным с помощью ключевого слова friend. 7 class hero{ friend class mistress; } class mistress{ void f1(); void f2(); } Функции f1 и f2 являются дружественными по отношению к классу hero (хотя и описаны без ключевого слова friend) и имеют доступ ко всем его полям. Объявление friend не является спецификатором доступа и не наследуется. 8 • С++ позволяет переопределить действие большинства операций так, чтобы при использовании с объектами конкретного класса они выполняли заданные функции. • Это дает возможность использовать собственные типы данных точно так же, как стандартные. Обозначения собственных операций вводить нельзя. Можно перегружать любые операции, существующие в С++, за исключением: • . .* ?: :: # ## sizeof 9 • Перегрузка операций осуществляется с помощью методов специального вида (функций-операций) и подчиняется следующим правилам: • при перегрузке операций сохраняются количество аргументов, приоритеты операций и правила ассоциации (справа налево или слева направо), используемые в стандартных типах данных; • для стандартных типов данных переопределять операции нельзя; • функции-операции не могут иметь аргументов по умолчанию; • функции-операции наследуются (за исключением =); • функции-операции не могут определяться как static. 10 • Функцию-операцию можно определить тремя способами: она должна быть либо методом класса, либо дружественной функцией класса, либо обычной функцией. В двух последних случаях функция должна принимать хотя бы один аргумент, имеющий тип класса, указателя или ссылки на класс. • функция-операция, первый параметр которой имеет стандартный тип, не может определяться как метод класса. • Функция-операция содержит ключевое слово operator, за которым следует знак переопределяемой операции: • тип operator операция ( список параметров) { тело функции } 11 Перегрузка унарных операций • Унарная функция-операция, определяемая внутри класса, должна быть представлена с помощью нестатического метода без параметров, при этом операндом является вызвавший ее объект, например: class monstr{ monstr & operator ++() {++health; return *this;} } monstr Vasia; cout << (++Vasia).get_health(); 12 • Если операция может перегружаться как внешней функцией, так и функцией класса, какую из двух форм следует выбирать? Ответ: используйте перегрузку в форме метода класса, если нет каких-либо причин, препятствующих этому. Например, если первый аргумент (левый операнд) относится к одному из базовых типов (к примеру, int), то перегрузка операции возможна только в форме внешней функции. 13 Перегрузка операций инкремента • Операция инкремента имеет две формы: префиксную и постфиксную. Для первой формы сначала изменяется состояние объекта в соответствии с данной операцией, а затем он (объект) используется в том или ином выражении. Для второй формы объект используется в том состоянии, которое у него было до начала операции, а потом уже его состояние изменяется. • Чтобы компилятор смог различить эти две формы операции инкремента, для них используются разные сигнатуры, например: 14 Point& operator ++(); Point operator ++(int); // префиксный инкремент // постфиксный инкремент //Покажем реализацию данных операций на примере класса //Point: Point& Point::operator ++() { x++; y++; return *this; } Point Point::operator ++(int) { Point old = *this; x++; y++; return old; } 15 • Обратите внимание, что в префиксной операции осуществляется возврат результата по ссылке. • Это предотвращает вызов конструктора копирования для создания возвращаемого значения и последующего вызова деструктора. • В постфиксной операции инкремента возврат по ссылке не подходит, поскольку необходимо вернуть первоначальное состояние объекта, сохраненное в локальной переменной old. Таким образом, префиксный инкремент является более эффективной операцией, чем постфиксный инкремент. 16 • Заметим, что ранее во всех примерах использовалась постфиксная форма инкремента: • for (i = 0; i < n; i++); • Дело в том, что пока параметр i является переменной встроенного типа, форма инкремента безразлична: программа будет работать одинаково. Ситуация меняется, если параметр i есть объект некоторого класса — в этом случае префиксная форма инкремента оказывается более эффективной. 17 Перегрузка бинарных операций Бинарная функция-операция, определяемая внутри класса, должна быть представлена с помощью нестатического метода с параметрами, при этом вызвавший ее объект считается первым операндом: class Point{ bool operator >(const Point &P){ if( x > P.get_x() && y > P.get_y() ) return true; return false; } }; 18 Если функция определяется вне класса, она должна иметь два параметра типа класса: bool operator >(const Point &P1, const Point &P2){ if( P1.get_x() > P2.get_x() && P1.get_y() >P2.get_y() ) return true; return false; } 19 class Point { double x, y; public: //. . . friend Point operator +(Point&, Point&); }; Point operator +(Point& p1, Point& p2) { return Point(p1.x + p2.x, p1.y + p2.y); } 20 class Point { double x, y; public: //. . . Point operator +(Point&); }; Point Point::operator +(Point& p) { return Point(x + p.x, y + p.y); } 21 • Если не описывать функцию внутри класса как дружественную, нужно учитывать доступность изменяемых полей. 22 • Независимо от формы реализации операции «+» мы можем теперь написать: • Point p1(0, 2), p2(-1, 5); • Point p3 = p1 + p2; • Встретив выражение p1 + p2, компилятор в случае первой формы перегрузки вызовет метод p1.operator +(p2), а в случае второй формы перегрузки — глобальную функцию operator +(p1, p2); 23 • Результатом выполнения данных операторов будет точка p3 с координатами x = –1, y = 7. Заметим, что для инициализации объекта p3 будет вызван конструктор копирования по умолчанию, но он нас устраивает, поскольку в классе нет полейуказателей. 24 Перегрузка операции присваивания • Перегрузка этой операции имеет ряд особенностей. • Во-первых, если вы не определите эту операцию в некотором классе, то компилятор создаст операцию присваивания по умолчанию, которая выполняет поэлементное копирование объекта. В этом случае возможно появление тех же проблем, которые возникают при использовании конструктора копирования по умолчанию. • Поэтому существует правило: если в классе требуется определить конструктор копирования, то должна быть перегруженная операция присваивания, 25 и наоборот. • Во-вторых, операция присваивания может быть определена только в форме метода класса. • В-третьих, операция присваивания не наследуется (в отличие от всех остальных операций). 26 class Man { public: Man(char* Name, int by=1950, float p=1000) ; ~Man() { delete [] pName; } … Man& operator =(const Man&); … private: char* pName; int birth_year; float pay; }; 27 Man& Man::operator =(const Man& man) { if (this == &man) return *this; // проверка на //самоприсваивание delete [] pName; // уничтожить //предыдущее значение pName = new char[strlen(man.pName) + 1]; strcpy(pName, man.pName); birth_year = man. birth_year; pay = man.pay; return *this; } 28 • Необходимо обратить внимание на несколько простых, но важных моментов при реализации операции присваивания: 1. Убедиться, что не выполняется присваивание вида x = x;. Если левая и правая части ссылаются на один и тот же объект, то делать ничего не надо. Если не перехватить этот особый случай, то следующий шаг уничтожит значение, на которое указывает pName, еще до того, как оно будет скопировано; 2. Удалить предыдущие значения полей в динамически выделенной памяти; 3. Выделить память под новые значения полей; 4. Скопировать в нее новые значения всех полей; 5. Возвратить значение объекта, на которое указывает this (то есть *this). 29 • Возврат из функции указателя на объект делает возможной цепочку операций присваивания: • Man A(“Alpha”), B(“Bravo”), C(“Charlie”); • C = B = A; • Операцию присваивания можно определять только как метод класса. 30 Перегрузка операции индексирования • Операция индексирования [ ] обычно перегружается, когда тип класса представляет множество значений, для которого индексирование имеет смысл. Операция индексирования должна возвращать ссылку на элемент, содержащийся в множестве. • Покажем это на примере класса Vect, предназначенного для хранения и работы с безопасным массивом целых чисел: 31 #include <iostream.h> #include <stdlib.h> class Vect{ public: Vect(int n = 10); Vect(const int a[], int n); //инициализация //массивом ~Vect() { delete [] p; } int& operator [] (int i); void Print(); … 32 private: int* p; int size; }; Vect::Vect(int n) : size(n){ p = new int[size]; } Vect::Vect(const int a[], int n) : size(n){ p = new int[size]; for (int i = 0; i < size; i++) p[i] = a[i]; } 33 // Перегрузка операции индексирования: int& Vect::operator [] (int i){ if(i < 0 || i >= size){ cout << "Неверный индекс (i = " << i << ")" << endl; cout << "Завершение программы" << endl; exit(0); } return p[i]; } 34 void Vect::Print(){ for (int i = 0; i < size; i++) cout << p[i] << " "; cout << endl; } int main(){ int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; Vect a(arr, 10); a.Print(); cout << a[5] << endl; cout << a[12] << endl; return 0; } 35 Результат работы программы: 1 2 3 4 5 6 7 8 9 10 6 Неверный индекс (i = 12) Завершение программы Перегруженная операция индексирования получает целый аргумент и проверяет, лежит ли его значение в пределах диапазона массива. Если да, то возвращается адрес элемента, что соответствует семантике стандартной операции индексирования. 36