Lection16

advertisement
Лекция 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
Download