Друзья класса: их назначение, области применения. Определение и использование функции-друга класса. Различия между членами и друзьями класса. Функции и перегруженные операции - члены и друзья класса.

Назначение методов – создание интерфейса между внешним миром и закрытой (защищенной) частями класса. Еще один способ получить доступ к закрытой части класса – использование внешних функций, объявленных как друг класса. Функции-друзья, как и члены, являются интерфейсом класса.

Функция становится другом после ее объявления в классе с использованием спецификатора 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;

. . . }

Друзья класса: их назначение, области применения. Определение и использование функции-друга класса. Различия между членами и друзьями класса. Функции и перегруженные операции - члены и друзья класса. - student2.ru

Рис. 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; }

Наши рекомендации