- стр 202 -
void manager::print()
{
employee::print(); // печатает информацию о служащем
// ... // печатает информацию о менеджере
}
Заметьте, что надо использовать ::, потому что print() была
переопределена в manager. Такое повторное использование имен
типично. Неосторожный мог бы написать так:
void manager::print()
{
print(); // печатает информацию о служащем
// ... // печатает информацию о менеджере
}
и обнаружить, что программа после вызова manager::print()
неожиданно попадает в последовательность рекурсивных вызовов.
7.2.3 Видимость
Класс employee стал открытым (public) базовым классом класса
manager в результате описания:
class manager : public employee {
// ...
};
Это означает, что открытый член класса employee является также и
открытым членом класса manager. Например:
void clear(manager* p)
{
p->next = 0;
}
будет компилироваться, так как next - открытый член и employee и
manager'а. Альтернатива - можно определить закрытый (private)
класс, просто опустив в описании класса слово public:
class manager : employee {
// ...
};
Это означает, что открытый член класса employee является закрытым
членом класса manager. То есть, функции члены класса manager могут
как и раньше использовать открытые члены класса employee, но для
пользователей класса manager эти члены недоступны. В частности, при
таком описании класса manager функция clear() компилироваться не
будет. Друзья производного класса имеют к членам базового класса
такой же доступ, как и функции члены.
Поскольку, как оказывается, описание открытых базовых классов
встречается чаще описания закрытых, жалко, что описание открытого
базового класса длиннее описания закрытого. Это, кроме того, служит
источником запутывающих ошибок у начинающих.
- стр 203 -
Когда описывается производная struct, ее базовый класс по
умолчанию является public базовым классом. То есть,
struct D : B { ...
означает
class D : public B { public: ...
Отсюда следует, что если вы не сочли полезным то скрытие данных,
которое дают class, public и friend, вы можете просто не
использовать эти ключевые слова и придерживаться struct. Такие
средства языка, как функции члены, конструкторы и перегрузка
операций, не зависят от механизма скрытия данных.
Можно также объявить некоторые, но не все, открытые $ члены
базового класса открытыми членами производного класса. Например:
class manager : employee {
// ...
public:
// ...
employee::name;
employee::department;
};
Запись
имя_класса :: имя_члена ;
не вводит новый член, а просто делает открытый член базового класса
открытым для производного класса. Теперь name и department могут
использоваться для manager'а, а salary и age - нет. Естественно,
сделать сделать закрытый член базового класса открытым членом
производного класса невозможно. Невозможно с помощью этой записи
также сделать открытыми перегруженные имена.
Подытоживая, можно сказать, что вместе с предоставлением средств
дополнительно к имющимся в базовом классе, производный класс можно
использовать для того, чтобы сделать средства (имена) недоступными
для пользователя. Другими словами, с помощью производного класса
можно обеспечивать прозрачный, полупрозрачный и непрозрачный доступ
к его базовому классу.
7.2.4 Указатели
Если производный класс derived имеет открытый базовый класс base,
то указатель на derived можно присваивать переменной типа указатель
на base не используя явное преобразование типа. Обратное
преобразование, указателя на base в указатель на derived, должно
быть явным. Например:
- стр 204 -
class base { /* ... */ };
class derived : public base { /* ... */ };
derived m;
base* pb = &m; // неявное преобразование
derived* pd = pb; // ошибка: base* не является derived*
pd = (derived*)pb; // явное преобразование
Иначе говоря, объект производного класса при работе с ним через
указател иможно рассматривать как объект его базового класса.
Обратное неверно.
Будь base закрытым базовым классом класса derived, неявное
преобразование derived* в base* не делалось бы. Неявное
преобразование не может в этом случае быть выполнено, потому что к
открытому члкну класса base можно обращаться через указатель на
base, но нельзя через указатель на derived:
class base {
int m1;
public:
int m2; // m2 - открытый член base
};
class derived : base {
// m2 НЕ открытый член derived
};
derived d;
d.m2 = 2; // ошибка: m2 из закрытой части класса
base* pb = &d; // ошибка: (закрытый base)
pb->m2 = 2; // ok
pb = (base*)&d; // ok: явное преобразование
pb->m2 = 2; // ok
Помимо всего прочего, этот пример показывает, что используя явное
приведение к типу можно сломать правила защиты. Ясно, делать это не
рекомендуется, и это приносит программисту заслуженую "награду". К
несчасть , недисциплинированное использование явного преобразования
может создать адские условия для невинных жертв, которые
эксплуатируют программу, где это делается. Но, к счастью, нет
способа воспользоваться приведением для получения доступа к
закрытому имени m1. Закрытый член класса может использоваться
только членами и друзьями этого класса.
7.2.5 Иерархия Типов
Производный класс сам может быть базовым классом. Например:
- стр 205 -
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 { ... };
// не C++:
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 Конструкторы и Деструкторы
Для некоторых производных классов нужны конструкторы. Если у
базового класса есть конструктор, он должен вызываться, и если для
этого конструктора нужны параметры, их надо предоставить. Например:
- стр 206 -
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. Пример со служащими и менеджерами можно было бы
переопределить так:
- стр 207 -
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() часто бывают организованы так, чтобы ползоваться
общность классов, с которыми они работают. Например:
- стр 208 -
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, скрытых внутри большой функции,