Друзья класса: их назначение, области применения. Определение и использование функции-друга класса. Различия между членами и друзьями класса. Функции и перегруженные операции - члены и друзья класса.
Назначение методов – создание интерфейса между внешним миром и закрытой (защищенной) частями класса. Еще один способ получить доступ к закрытой части класса – использование внешних функций, объявленных как друг класса. Функции-друзья, как и члены, являются интерфейсом класса.
Функция становится другом после ее объявления в классе с использованием спецификатора friend, например:
Определение | Реализация |
а) глобальная функция class X { ... friend void f(); public: void fx(); ... } | void f() { ... } |
б) функция – член класса class Y { ... friend void X::fx(); ... }; | void X::fx() { ... } |
в) класс class Z { ... friend class Y; ... }; |
Другом класса могут быть: глобальная функция (см. примеры, приведенные выше); функция-член другого класса; все функции-члены другого класса, т.е. сам класс (при этом функции-друзья одного класса не становятся автоматически друзьями другого класса). Почти любой метод может быть сделан другом. Исключения: конструкторы, деструктор, кое-что еще.
Расположение объявления friend в определении класса (в открытой, закрытой или защищенной частях класса) роли не играет – функция-друг класса является внешней по отношению к этому классу. По этой же причине функция-друг класса, не являясь методом класса, не имеет адресата и, следовательно, не имеет указателя this (все аргументы в функцию передаются через список параметров).
Различия между членами и друзьями класса:
Функция-член | Функция-друг |
class Rational{ public: void print(); ... }; void Rational::print() { cout << num; if(den != 1) cout << '/' << den; } ... Rational x(1,5); x.print(); | class Rational{ public: friend void print(Rational r); ... }; void print(Rational r) { cout << r.num; if(r.den != 1) cout << '/' << r.den; } ... Rational x(1,5); print(x); |
Перегруженные операторы – друзья класса:
Бинарный оператор | Унарный оператор (префиксный и постфиксный) |
Объявление | |
friend тип operator знак_оп(op1, op2) | friend тип operator знак_оп(op1) friend тип operator знак_оп(op1, int) |
Реализация | |
тип operator знак_оп(тип op1, тип op2) { ... } | тип operator знак_оп(тип op1) { ... } тип operator знак_оп(тип op1, int) { ... } |
Использование | |
op1 знак_оп op2 эквивалентно: operator знак_оп(op1, op2) | знак_оп op1 эквивалентно: operator знак_оп(op1) op1 знак_оп эквивалентно: operator знак_оп(op1, int) |
Пример перегрузки оператора записи в поток для класса Rational:
class Rational {
friend ostream& operator <<(ostream&, Rational);
... };
ostream& operator <<(ostream& os, Rational r)
{ os << r.num;
if(r.den == 1)
os << ‘/’ << r.den;
return os; }
Друзья или члены
Любой метод, представляющий интерфейс класса, за некоторыми (небольшими) исключениями (к ним относятся конструкторы, деструктор и кое-что еще), может быть реализован и функцией-членом, и функцией-другом класса.
Общее:
- имеют доступ к закрытой части класса,
- хотя бы один аргумент – экземпляр класса
Различие:
Член класса | Друг класса |
- из n параметров один (первый) параметр неявный, остальные – в списке параметров | - все n параметров в списке параметров |
- неявный параметр – адресат сообщения; доступен через this | - все параметры равноправны; адресата сообщения нет; this не определено |
- адресат сообщения (первый аргумент) – обязательно экземпляр класса | - порядок и типы аргументов определяются прототипом функции |
Rational x(1,3); x + 1 - все в порядке 1 + x - ошибка! | Rational x(1,3); x + 1 - все в порядке 1 + x - все в порядке |
Функции-члены класса:
- конструкторы, деструкторы, виртуальные функции;
- операции, требующие в качестве операндов основных типов lvalue (например, =, +=, ++ и т.д.)
- операции, изменяющие состояние объекта
Функции-друзья класса:
- операции, требующие неявного преобразования операндов (например, +, - и т.д.)
- операции, первый операнд которых не соответствует типу экземпляра класса (например, << и >>).
При прочих равных условиях лучше выбирать функции-члены класса.
7. Преобразования типа: назначение, использование. Правила преобразования типа. Возможные проблемы.
Возможны два способа преобразования типа для вновь создаваемого класса: преобразование из уже существующего типа в новый, и преобразование нового разрабатываемого типа в уже существующий.
Преобразование из существующего типа в новый выполняется с помощью одноаргументного конструктора.
Пример для класса Rational:
Rational x = Rational(23); // явный вызов
Rational y = 23; // неявный вызов
Возможны любые использования:
Rational a(1,2), b(1), c;
c = a + b;
c = a + 1; // эквивалентно c = a + Rational(1);
c = 2 + a; // эквивалентно c = Rational(2) + a;
Преобразование из нового типа в существующий выполняется с помощью перегрузки оператора преобразования типа.
Оператор преобразования типа обязательно перегружается как функция-член класса.
Прототип: operator имя_типа ();
Реализация: имя_класса::operator имя_типа() { ... }
Использование: неявно при вычислении выражений или явно с помощью обычного оператора преобразования типа: имя_типа(выражение).
Пример для класса Rational:
class Rational{
private:
int num, den;
...
public:
...
operator float() { return (float) num / den; }
... };
Использование:
Rational x(3,2);
float f = x;
Еще пример использования перегруженного преобразования типа – для потока вывода:
while(cin >> n) // здесь используется преобразование типа ostream к типу void *
cout << n;
Возможные неприятности:
Если в классе Rational есть одноаргументный конструктор (преобразование int в Rational) и в нем будет перегружен оператор преобразования типа для int (преобразование Rational в int), тогда конструкция:
Rational a(1,2);
... a + 1 ...
вызовет сообщение об ошибке: два преобразования типа, определенные пользователем; что выбрать: int + int или Rational + Rational?
Классы, использующие свободную память: определение и реализация, использование экземпляров класса, возникающие проблемы. Копирующий конструктор и деструктор, перегрузка операции присваивания: определение и использование.
Основные определения
При решении задач часто приходится сталкиваться с ситуацией, когда состояние класса определяется как массив элементов, конкретное количество которых оценить сложно. В таких ситуациях состояние класса определяется как указатель, а необходимый объем памяти выделяется во время выполнения программы. Пример такого класса: функция, заданная таблично. Состояние класса включает в себя информацию о размере таблицы (количество узлов в задании функции) и массивы для хранения значений аргумента (х) и соответствующих значений функции (y). Массивы определяются через соответствующие указатели, размер таблицы определяет требуемый объем памяти.
Возникает вопрос: кто будет выделять память под динамическую часть состояния экземпляра класса? – Ответ очевиден: конструктор.
Очевидно, что выделенную память после ее использования необходимо вернуть. Эта операция выполняется обычно тогда, когда разрушается соответствующий экземпляр класса. Как было сказано выше, при разрушении экземпляра класса работает деструктор класса. Следовательно, в определение класса должен быть включен и деструктор.
Пример разработки соответствующего класса.
class TableFun{
private:
float * xPtr, * yPtr; // динамические массивы для значений аргументов и функции
int size; // количество узлов функции
public:
Function(); // пустой конструктор
Function(int sz); // инициализирующий конструктор
~Function(); // деструктор
. . .
};
// пустой конструктор – память не выделяется
Function::TableFun ()
{
size = 0;
xPtr = yPtr = NULL;
}
// инициализирующий конструктор – выделяется память указанного размера
TableFun::TableFun(int sz)
{ size = sz;
xPtr = new float[size];
yPtr = new float[size]; }
// деструктор – освобождение памяти
TableFun::~ TableFun ()
{ delete [ ] xPtr;
delete [ ] yPtr; }
Возникающие проблемы
Пусть в прикладной программе в некотором внутреннем блоке создается переменная типа “Указатель на класс” и ей присваивается значение некоторой другой – глобальной переменной. Для классов (структур) определен оператор присваивания – он выполняет побайтное копирование. В результате два разных экземпляра класса будут содержать указатели на одни и те же динамические области памяти (рис. 4–1).
В момент выхода из блока переменная f2 разрушается – для нее работает деструктор класса. В результате будет разрушена и переменная f1!
Результат не изменится, если во внутреннем блоке вместо присваивания использовать инициализацию (в этом случае будет работать копирующий конструктор по умолчанию – также побайтное копирование):
{ TableFun f2 = f1;
. . . }
Рис. 4-1. Побайтное копирование при использовании присваивания по умолчанию
Решение проблемы
В таких классах обязательно нужно предусматривать копирующий деструктор и перегруженный оператор присваивания.
Копирующий конструктор (прототип в определении класса):
. . .
TableFun (const TableFun &); …
Копирующий конструктор должен:
- выделить новую память для создаваемого экземпляра класса,
- скопировать в нее состояние существующего экземпляра класса.
При этом необходимо проверить, что используемый для инициализации экземпляр класса действительно имеет собственное состояние (т.е. не использует пустые указатели).
Реализация:
TableFun:: TableFun (const TableFun &f)
{ xPtr = yPtr = NULL;
if((size = f.size) != 0){ // проверка ситуации, когда у объекта f нет памяти
xPtr = new float[size];
yPtr = new float[size];
for(int i = 0; i < size; i++){
xPtr[i] = f.xPtr[i];
yPtr[i] = f.yPtr[i];} } }
Перегруженный оператор присваивания (объявление в определении класса; функция-член класса):
. . .
TableFun & operator =(const TableFun &);
. . .
Перегруженный оператор присваивания должен:
- освободить память, занимаемую экземпляром класса – адресатом оператора присваивания (указанного слева от присваивания),
- выделить (если это необходимо) новую память для нового значения экземпляра класса – адресата,
- скопировать в нее значение экземпляра класса, указанного справа от присваивания, если они есть (параметр оператора присваивания),
- проверить возможность использования присваивания типа x = x.
Реализация:
TableFun & TableFun::operator =(const TableFun &f)
{ if(this != &f){ // проверка ситуации x = x
delete [] xPtr;
delete [] yPtr;
xPtr = yPtr = NULL;
if((size = f.size) != 0){ // проверка ситуации, когда у объекта f нет памяти
xPtr = new float[size];
yPtr = new float[size];
for(int i = 0; i < size; i++){
xPtr[i] = f.xPtr[i];
yPtr[i] = f.yPtr[i]; } } }
return *this; }