C++ - Страустрап Бьярн
Шрифт:
Интервал:
Закладка:
class derived : base (* // m2 – НЕ открытый член derived *);
derived d; d.m2 = 2; // ошибка: m2 из закрытой части класса base* pb = amp;d; // ошибка: (закрытый base) pb-»m2 = 2; // ok pb = (base*) amp;d; // ok: явное преобразование pb-»m2 = 2; // ok
Помимо всего прочего, этот пример показывает, что ипользуя явное приведение к типу можно сломать правила защиты. Ясно, делать это не рекомендуется, и это приносит программиту заслуженную «награду». К несчастью , недисциплинированное использование явного преобразования может создать адские уловия для невинных жертв, эксплуатирующих программу, в котрой это делается. Но, к счастью, нет способа воспользоваться приведением для получения доступа к закрытому имени m1. Зарытый член класса может использоваться только членами и друзьями этого класса.
7.2.5 Иерархия типов
Производный класс сам может быть базовым классом. Например:
class employee (* ... *); class secretary : employee (* ... *); class manager : employee (* ... *); class temporary : employee (* ... *); class consultant : temporary (* ... *); class director : manager (* ... *); class vice_president : manager (* ... *); class president : vice_president (* ... *);
Такое множество родственных классов принято называть ирархией классов. Поскольку можно выводить класс только из оного базового класса, такая иерархия является деревом и не может быть графом более общей структуры. Например:
class temporary (* ... *); class employee { ... *); class secretary : employee (* ... *);
// не С++: class temporary_secretary : temporary : secretary(* ... *); class consultant : temporary : employee (* ... *);
И этот факт вызывает сожаление, потому что направленный ациклический граф производных классов был бы очень полезен. Такие структуры описать нельзя, но можно смоделировать с пмощью членов соответствующих типов. Например:
class temporary (* ... *); class employee (* ... *); class secretary : employee (* ... *);
// Альтернатива: class temporary_secretary : secretary (* temporary temp; ... *); class consultant : employee (* temporary temp; ... *);
Это выглядит неэлегантно и страдает как раз от тех пролем, для преодоления которых были изобретены производные классы. Например, поскольку consultant не является произвоным от temporary, consultant'а нельзя помещать с список врменных служащих (temporary employee), не написав специальный код. Однако во многих полезных программах этот метод успешно используется.
7.2.6 Конструкторы и деструкторы
Для некоторых производных классов нужны конструкторы. Если у базового класса есть конструктор, он должен вызыватся, и если для этого конструктора нужны параметры, их надо предоставить. Например:
class base (* // ... public: base(char* n, short t); ~base(); *);
class derived : public base (* base m; public: derived(char* n); ~derived(); *);
Параметры конструктора базового класса специфицируются в определении конструктора производного класса. В этом смысле базовый класс работает точно также, как неименованный член производного класса (см. #5.5.4). Например:
derived::derived(char* n) : (n,10), m(«member»,123) (* // ... *)
Объекты класса конструируются снизу вверх: сначала базвый, потом члены, а потом сам производный класс. Уничтожаются они в обратном порядке: сначала сам производный класс, потом члены а потом базовый.
7.2.7 Поля типа
Чтобы использовать производные классы не просто как удобную сокращенную запись в описаниях, надо разрешить следющую проблему: Если задан указатель типа base*, какому проиводному типу в действительности принадлежит указываемый обект? Есть три основных способа решения этой проблемы:
1. Обеспечить, чтобы всегда указывались только объекты одного типа (#7.3.3),
2. Поместить в базовый класс поле типа, которое смогут просматривать функции и
3. Использовать виртуальные функции (#7.2.8).
Обыкновенно указатели на базовые классы используются при разработке контейнерных (или вмещающих) классов: множество, вектор, список и т.п. В этом случае решение 1 дает однородные списки, то есть списки объектов одного типа. Решения 2 и 3 можно использовать для построения неоднородных списков, то есть списков объектов (указателей на объекты) нескольких раличных типов. Решение 3 – это специальный вариант решения 2 с гарантией типа.
Давайте сначала исследуем простое решение с помощью поля типа, то есть решение 2. Пример со служащими и менеджерами можно было бы переопределить так:
enum empl_type (* M, E *);
struct employee (* empl_type type; employee* next; char* name; short department; // ... *);
struct manager : employee (* employee* group; short level; // уровень *);
Имея это, мы можем теперь написать функцию, которая пчатает информацию о каждом служащем:
void print_employee(employee* e) (* switch (e-»type) (* case E: cout «„ e-“name „„ „t“ „„ e-“department „„ „n“; // ... break; case M: cout „« e-“name «« «t“ «« e-“department «« «n“; // ... manager* p = (manager*)e; cout «« " уровень " «« p-“level «« «n“; // ... break;
*) *)
и воспользоваться ею для того, чтобы напечатать список служащих:
void f() (* for (; ll; ll=ll-»next) print_employee(ll); *)
Это прекрасно работает,особенно в небольшой программе, написанной одним человеком, но имеет тот коренной недостаток, что неконтролируемым компилятором образом зависит от того, как программист работает с типами. В больших программах это обычно приводит к ошибкам двух видов. Первый – это невыполнние проверки поля типа, второй – когда не все случаи case пмещаются в переключатель switch, как в предыдущем примере. Оба избежать достаточно легко , когда программу сначала пишут на бумаге, но при модификации нетривиальной программы, осбенно написанной другим человеком, очень трудно избежать как того, так и другого. Часто от этих сложностей становится труднее уберечься из-за того, что функции вроде print() часто бывают организованы так, чтобы пользоваться общностью класов, с которыми они работают. Например:
void print_employee(employee* e) (* cout «„ e-“name „„ „t“ „„ e-“department „« «n“; // ... if (e-“type == M) (* manager* p = (manager*)e; cout «« " уровень " «« p-“level «« «n“; // ... *) *)
Отыскание всех таких операторов if, скрытых внутри болшой функции, которая работает с большим числом производных классов, может оказаться сложной задачей, и даже когда все они найдены, бывает нелегко понять, что же в них делается.
7.2.8 Виртуальные функции
Виртуальные функции преодолевают сложности решения с пмощью полей типа, позволяя программисту описывать в базовом классе функции, которые можно переопределять в любом проиводном классе. Компилятор и загрузчик обеспечивают правильное соответствие между объектами и применяемыми к ним функциями. Например:
struct employee (* employee* next; char* name; short department; // ... virtual void print(); *);
Ключевое слово virtual указывает, что могут быть разлиные варианты функции print() для разных производных классов, и что поиск среди них подходящей для каждого вызова print() является задачей компилятора. Тип функции описывается в базвом классе и не может переописываться в производном классе. Виртуальная функция должна быть определена для класса, в ктором она описана впервые. Например:
void employee::print() (* cout «„ e-“name „„ „t“ «« e-“department «« «n“; // ... *)
Виртуальная функция может, таким образом, использоваться даже в том случае, когда нет производных классов от ее класа, и в производном классе, в котором не нужен специальный вариант виртуальной функции, ее задавать не обязательно. Просто при выводе класса соответствующая функция задается в том случае, если она нужна. Например:
struct manager : employee (* employee* group; short level; // ... void print(); *);