применения, знакомство с понятием производных классов делается в
три этапа. Вначале с помощью небольших примеров, которые не надо
воспринимать как реалистичные, будут описаны сами средства языка
(заись и семантика). После этого демонстрируются некоторые
неочевидные применения производных классов, и, наконец, приводится
законченная программа.
7.2.1 Построение Производного Класса
Рассмотрим построение программы, которая имеет дело с людьми,
служащими в некоторой фирме. Структура данных в этой программе
может быть например такой:
struct employee { // служащий
char* name; // имя
short age; // возраст
short department; // подразделение
int salary; //
employee* next;
// ...
};
Список аналогичных служащих будет свзяваться через поле next.
Теперь давайте определим менеджера:
struct manager { // менеджер
employee emp; // запись о менеджере как о служащем
employee* group; // подчиненные люди
// ...
};
Менеджер также является служащим; относящиеся к служащему employee
данные хранятся в члене emp объекта manager. Для читающего это
человека это, может быть, очевидно, но нет ничего выделяющего член
emp для компилятора. Указатель на менеджера (manager*) не является
указателем на служащего (employee*), поэтому просто использовать
один там, где требуется другой, нельзя. В частности, нельзя
поместить менеджера в список служащих, не написав для этого
специальную программу. Можно либо применить к manager* явное
преобразование типа, либо поместить в список служащих адрес члена
emp, но и то и другое мало элегантно и довольно неясно. Корректный
подход состоит в том, чтобы установить, что менеджер является
служащим с некоторой добавочной информацией:
- стр 200 -
struct manager : employee {
employee* group;
// ...
};
manager является производным от employee и, обратно, employee есть
базовый класс для manager. Класс manager дополнительно к члену
group имеет члены класса employee (name, age и т.д.).
Имея определения employee и manager мы можем теперь создать
список служащих, некоторые из которых являются менеджерами.
Например:
void f()
{
manager m1, m2;
employee e1, e2;
employee* elist;
elist = &m1; // поместить m1, e1, m2 и e2 в elist
m1.next = &e1;
e1.next = &m2;
m2.next = &e2;
e2.next = 0;
}
Поскольку менеджер является служащим, manager* может использоваться
как employee*. Однако служащий необязательно является менеджером,
поэтому использовать employee* как manager* нельзя.
7.2.2 Функции Члены
Просто структуры данных вроде employee и manager на самом деле не
столь интересны и часто не особенно полезны, поэтому рассмотрим,
как добавить к ним функции. Например:
class employee {
char* name;
// ...
public:
employee* next;
void print();
// ...
};
class manager : public employee {
// ...
public:
void print();
// ...
};
Надо ответить на некоторые вопросы. Как может функция член
производного класса manager использовать члены его базового класса
employee? Как члены базового класса employee могут использовать
функции члены производного класса manager? Какие члены базового
класса employee может использовать функция не член на объекте типа
- стр 201 -
manager? Каким образом программист может повлиять на ответы на эти
вопросы, чтобы удовлетворить требованиям приложения?
Рассмотрим:
void manager::print()
{
cout << " имя " << name << "\n";
// ...
}
Член производного класса может использовать открытое имя из своего
базового класса так же, как это могут делать другие члены
последнего, то есть без указания объекта. Предполагается, что на
объект указывает this, поэтому (корректной) ссылкой на имя name
является this->name. Однако функция manager::print компилироваться
не будет, член производного класса не имеет никакого особого права
доступа к закрытым членам его базового класса, поэтому для нее name
недоступно.
Это многим покажется удивительным, но представьте себе другой
вариант: что функция член могла бы обращаться к закрытым членам
своего базового класса. Возможность, позволяющая программисту
получать доступ к закрытой части класса просто с помощью вывода из
него другого класса, лишила бы понятие закрытого члена всякого
смысла. Более того, нельзя было бы узнать все использования
закрытого имени посмотрев на функции, описанные как члены и друзья
этого класса. Пришлось бы проверять каждый исходный файл во всей
программе на наличие в нем производных классов, потом исследовать
каждую функцию этих классов, потом искать все классы, производные
от этих классов, и т.д. Это по меньшей мере утомительно и скорее
всего нереально.
С другой стороны, можно ведь использовать механизм friend, чтобы
предоставить такой доступ или отдельным функциям, или всем функциям
отдельного класса (как описывается в #5.3). Например:
class employee {
friend void manager::print();
// ...
};
решило бы проблему с manager::print(), и
class employee {
friend class manager;
// ...
};
сделало бы доступным каждый член employee для всех функций класса
manager. В частности, это сделает name доступным для
manager::print().
Другое, иногда более прозрачное решение для производного класса,-
использовать только открытые члены его базового класса. Например:
- стр 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
Помимо всего прочего, этот пример показывает, что используя явное
приведение к типу можно сломать правила защиты. Ясно, делать это не
рекомендуется, и это приносит программисту заслуженую "награду". К