Сегодняшний шестой выпуск я планировал посвятить шаблонам, но вовремя вспомнил, что
остался нераскрыт вопрос друзей класса и вложенных классов, а без понимания этого материала
разговаривать о шаблонных классах как-то некорректно. Поэтому данный выпуск будет посвящен
раскрытию данных двух тем.
Друзья класса
Как Вы уже знаете, в классе существует механизм управления доступом, который продуцирует
три области доступа - открытую (public), защищенную (protected) и закрытую (private). К членам
класса открытой области доступ не ограничен, к членам защищенной имеют доступ только производные
классы, а к членам закрытой области могут получить доступ только функции-члены данного класса.
Все так и было, пока мы не знали о спецификаторе friend. Данный спецификатор открывает
объявленному классу или функции полный доступ ко всем членам данного класса. Принято записывать
friend-объявления сверху в теле класса, однако место объявления роли не играет - главное объявить
в теле класса. Рассмотрим пример объявления и использования классов-друзей:
class B;
class A
{
friend class B;
protected:
void func_protected(){}
private:
void func_private(){}
public:
void func_public(){}
};
class B
{
public:
void func_any(){
A a;
//без friend-объявления нижеприведенные функции будут недоступны
a.func_protected();
a.func_private();
}
};
Обратите внимание на первую строку примера: здесь мы объявляем класс B, иначе будет ошибка
компиляции с сообщением типа "класс B неизвестен".
Кроме классов-друзей, друзьями можно объявлять функции,- как обычные так и функции
члены классов. В этом случае полный доступ к членам класса получают только объявленные дружественными
функции. Например, продемонстрируем возможность получения доступа к закрытым членам:
class B
{
public:
void printValue(); //реализация доступа в A::value в *.cpp файле
};
class A
{
friend void printValue();
friend void B::printValue();
public:
A(int v=0):value(v){}
private:
int value;
};
void printValue()
{
A a(100);
a.value = 400;
}
int main()
{
B b;
b.printValue();
printValue();
return 0;
}
friend-объявления распространяются только на конкретный объявленный класс, т.е. если необходим
полный доступ к членам класса из наследников frind-класса, то необходимо объявлять друзьями всех
необходимых наследников. Подобное не пройдет:
class B;
class A
{
friend class B;
private:
void func_private(){}
};
class B
{
public:
//нормально
void exec_A_private(){ A a; a.func_private(); }
};
class BX : public B
{
public:
//уже не проходит
void exec_A_private(){ A a; a.func_private(); }
};
Нужно так:
class B;
class BX
class A
{
friend class B;
friend class BX;
private:
void func_private(){}
};
class B
{
public:
//нормально
void exec_A_private(){ A a; a.func_private(); }
};
class BX : public B
{
public:
//нормально
void exec_A_private(){ A a; a.func_private(); }
};
С другой стороны, при наследовании класса, в котором объявлены дружественные функции или
классы, действие объявления распространяется на всех наследников.
class A
{
friend class B;
private:
void func_private(){}
};
class AX : public A {};
class AXX : public AX {};
class B
{
public:
void exec_AXX_private(){ AXX a; a.func_private(); }
};
Теперь о применении.
Во-первых, самое главное о применении можно сказать так: если у Вас часто употребляются friend-
объявления, то это только говорит о неправильном проектировании. Предоставляя кому-либо полный
доступ к членам классу, Вы должны четко представлять механизм взаимодействия связки, т.к. в этом
случае практически полностью отключен механизм контроля доступа для объявленного класса (функции)
и соответственно, friend-класс может делать с данными делегирующего класса все что угодно.
Во-вторых, необходимость применения friend-объявлений возникает при написании, например итераторов
или подобных механизмов. Однако, по-моему, данные вещи красивее делать с помощью вложенных классов.
Вложенные классы
Вложенные классы - это обычные классы, члены которых имеют полный доступ ко всем членам родителя
(слово 'родитель' следует понимать не в плане наследования, а в плане непосредственной принадлежности).
Объявляются вложенные классы внутри тела родителя, при этом важна область видимости, в которой
класс был объявлен. Через объект родительского класса доступ к членам вложенного класса возможен
(при наявности допустимых прав) через оператор ::. Смотрим пример:
class AMain
{
public:
struct A
{
void printValue();
};
const A& getA();
protected:
struct B
{
void printValue();
};
const B& getB();
private:
class C
{
void printValue();
};
const C& getC();
A a;
B b;
C c;
};
void AMain::A::printValue()
{
}
void AMain::B::printValue()
{
}
void AMain::C::printValue()
{
}
const AMain::A& AMain::getA()
{
return a;
}
const AMain::B& AMain::getB()
{
return b;
}
const AMain::C& AMain::getC()
{
return c;
}
int main()
{
AMain a;
const AMain::A &aa = a.getA();
AMain::A aa1;
const AMain::B bb; //ошибка компиляции - данная структура в protected-области
return 0;
}
Т.к. вложенные классы не отличаются от обыкновенных невложенных, допустимо из обычное наследование
и т.п.:
class A
{
protected:
class AIn
{
public:
void print(){ printValue(); }
protected:
virtual void printValue(){ printf("A::AIn\n"); }
};
};
class B : public A
{
public:
void print(){ b.print(); }
protected:
class BIn : public AIn
{
protected:
virtual void printValue(){ printf("B::BIn\n"); }
};
private:
BIn b;
};
Главное преимущество вложенных классов заключается в том, что не засоряется глобальная область
видимости - если необходима структура или класс для внутреннего хранения данных, то лучшего, чем
вложенный класс трудно придумать. В связи с этим и главная рекомендация - вложенными делайте только
те классы, которые логически тесно связаны с родительским классом. Нерекомендуемо сильно развивать
иерархию вложенных классов, т.к. это ухудшает читаемость - Вы получите механизм обращения типа
A::A1::A2::, а это как правило не способствует упрощению понимания логики программы.
В заключение кратко рассмотрим механизм объявления и использования вложенного enum-типа:
class A
{
public:
enum Color{ red,black,green,cian };
void setColor(Color c);
Color getColor()const;
private:
Color col;
};
void A::setColor(A::Color c)
{
col=c;
}
A::Color A::getColor()const
{
return col;
}
int main()
{
A a;
a.setColor(A::red);
A::Color color = a.getColor();
return 0;
}