C++. Бархатный путь. Часть 2 - Друзья класса

ОГЛАВЛЕНИЕ

 

Друзья класса

Три спецификатора доступа обеспечивают в C++ управление доступом. Эти спецификаторы являются основанием принципа инкапсуляции - одного из трёх основных принципов объектно-ориентированного программирования. Соблюдение правил доступа повышает надёжность программного обеспечения.

Спецификаторы доступа способны обеспечить многоуровневую защиту функций и данных в наследуемых классах. Порождаемые на основе "инкапсулированных" классов объекты способны поддерживать жёсткий интерфейс. Они подобны "чёрным" ящикам с чётко обозначенными входами и выходами. Вместе с тем, следует признать, что система управления доступом, реализованная на основе трёх спецификаторов, не является гибкой. С её помощью может быть реализована защита по принципу "допускать ВСЕХ (члены класса, объявленные в секции public) или не допускать НИКОГО (члены класса, объявленные в секциях protected и private)". В C++ существует возможность организации более гибкой защиты. Здесь можно также объявлять функции, отдельные функции-члены классов и даже классы (в этом случае речь идёт о полном множестве функций-членов класса), которые получают доступ к защищённым и приватным членам данного класса. Что означает реализацию системы управления доступом принципу "не допускать НИКОГО, КРОМЕ". Такие функции и классы называют дружественными функциями и классами. Объявление дружественных классов и функций включается в объявление данного класса вместе со спецификатором объявления friend. Здесь нам потребуется всего одна форма Бэкуса-Наура для того, чтобы дополнить синтаксис объявления.

СпецификаторОбъявления ::= friend
::= *****

Рассмотрим небольшой пример использования дружественных функций и классов, а затем сформулируем основные правила работы с друзьями классов. В программе объявлены два класса, один из которых является другом другого класса и всеобщая дружественная функция.

#include <iostream.h>
class XXX;
/*
Неполное объявление класса. Оно необходимо для объявления типа
параметра функции-члена для следующего класса.
*/
class MMM
{
private:
int m1;
public:
MMM(int val);
void TypeVal(char *ObjectName, XXX& ClassParam);
};
MMM::MMM(int val)
{
m1 = val;
}
/*
Определение функции-члена TypeVal располагается после объявления
класса XXX. Только тогда транслятор узнаёт о структуре класса, к
которому должна получить доступ функция MMM::TypeVal.
*/
class XXX
{
friend class YYY;
friend void MMM::TypeVal(char *ObjectName, XXX& ClassParam);
friend void TypeVal(XXX& ClassParamX, YYY& ClassParamY);
/*
В классе объявляются три друга данного класса: класс YYY, функция-член
класса MMM, простая функция TypeVal. В класс XXX включаются лишь
объявления дружественных функций и классов. Все определения
располагаются в других местах - там, где им и положено быть - в своих
собственных областях видимости.
*/
private:
int x1;
public:
XXX(int val);
};
XXX::XXX(int val)
{
x1 = val;
}
void MMM::TypeVal(char *ObjectName, XXX& ClassParam)
{
cout << "Значение " << ObjectName << ": " << ClassParam.x1 << endl;
}
/*
Отложенное определение функции-члена MMM::TypeVal.
*/
class YYY
{
friend void TypeVal(XXX& ClassParamX, YYY& ClassParamY);
private:
int y1;
public:
YYY(int val);
void TypeVal(char *ObjectName, XXX& ClassParam);
};
YYY::YYY(int val)
{
y1 = val;
}
void YYY::TypeVal(char *ObjectName, XXX& ClassParam)
{
cout << "Значение " << ObjectName << ": " << ClassParam.x1 << endl;
}
void TypeVal(XXX& ClassParamX, YYY& ClassParamY);
void main()
{
XXX mem1(1);
XXX mem2(2);
XXX mem3(3);
YYY disp1(1);
YYY disp2(2);
MMM special(0);
disp1.TypeVal("mem1", mem1);
disp2.TypeVal("mem2", mem2);
disp2.TypeVal("mem3", mem3);
special.TypeVal("\n mem2 from special spy:", mem2);
TypeVal(mem1, disp2);
TypeVal(mem2, disp1);
}
void TypeVal(XXX& ClassParamX, YYY& ClassParamY)
{
cout << endl;
cout << "???.x1 == " << ClassParamX.x1 << endl;
cout << "???.y1 == " << ClassParamY.y1 << endl;
}

В этом примере все функции имеют одинаковые имена. Это не страшно. Это даже полезно, поскольку становится очевидным факт существования разных областей действия имён.

В заключение раздела перечислим основные правила пользования новыми средствами управления доступа - дружественной системой защиты.

  • Друзья класса не являются членами класса. Они должны определяться вне класса, для которого они объявляются друзьями, а об особых отношениях между ними и данным классом свидетельствует лишь специальное объявление(!) со спецификатором объявления friend. Объявления дружественного класса означает, что в дружественном классе доступны все компоненты объявляемого класса.
  • Дружественные данному классу функции не являются членами этого класса. Поэтому они не могут быть вызваны из объекта-представителя класса, для которого была объявлена другом данная функция, при помощи операции доступа к члену класса.
  • Дружественная функция может быть функцией-членом другого ранее объявленного класса. Правда, при этом само определение дружественной функции приходится располагать после объявления класса, другом которого была объявлена данная функция. Это не очень удобно и красиво, но зато работает.
  • Дружественная функция не имеет this указателя для работы с классом, содержащим её объявление в качестве дружественной функции. Дружба - это всего лишь дополнение принципа инкапсуляции и ничего более.
  • Дружественные отношения не наследуются. Дружественные функции не имеют доступа к членам производного класса, чьи базовые классы содержали объявления этих функций. Дети не отвечают за отношения своих родителей.