Единый базовый класс
В
ряде языков программирования все классы явно или неявно наследуются от
некого базового класса. Smalltalk был одним из первых языков, в которых
использовалась эта концепция. К таким языкам относятся Objective-C
(NSObject), Eiffel (ANY), Java (java.lang.Object), C# (System.Object),
Delphi (TObject).
Производный класс можно
рассматривать как расширение существующего класса. Исходный класс
называется базовым или родительским, а производный – подклассом или
потомком. Производный класс представляет собой расширение или
модификацию базового класса. Имея родительский класс, можно использовать
единый интерфейс для получения нескольких производных классов.
Любой
класс может быть родительским, и любой производный класс будет
следовать его описанию. Класс-потомок может иметь дополнительные
возможности по сравнению с базовым классом. В производном классе можно
изменить права доступа, добавить новые элементы или перегрузить
имеющиеся методы.
Построение Производного Класса
Рассмотрим
построение программы, которая имеет дело с людьми, служащими в
некоторой фирме. Структура данных в этой программе может быть например
такой:
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, но и то и другое мало элегантно и довольно
неясно. Корректный подход состоит в том, чтобы установить, что менеджер
является служащим с некоторой добавочной информацией:
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* нельзя.
Функции Члены
Просто
структуры данных вроде employee и manager на самом деле не столь
интересны и часто не особенно полезны, поэтому рассмотрим, как добавить к
ним функции. Например:
class employee {
char* name;
// ...
public:
employee* next;
void print();
// ...
};
class manager : public employee {
// ...
public:
void print();
// ...
};
Надо
ответить на некоторые вопросы. Как может функция член производного
класса manager использовать члены его базового класса employee? Как
члены базового класса employee могут использовать функции члены
производного класса manager? Какие члены базового класса employee может
использовать функция не член на объекте типа manager? Каким образом
программист может повлиять на ответы на эти вопросы, чтобы удовлетворить
требованиям приложения?
Рассмотрим:
void manager::print()
{
cout << " имя " << name << "\n"; // ... }
Член
производного класса может использовать открытое имя из своего базового
класса так же, как это могут делать другие члены последнего, то есть без
указания объекта. Предполагается, что на объект указывает this, поэтому
(корректной) ссылкой на имя name является this->name. Однако функция
manager::print компилироваться не будет, член производного класса не
имеет никакого особого права доступа к закрытым членам его базового
класса, поэтому для нее name недоступно. Это многим покажется
удивительным, но представьте себе другой вариант: что функция член могла
бы обращаться к закрытым членам своего базового класса. Возможность,
позволяющая программисту получать доступ к закрытой части класса просто с
помощью вывода из него другого класса, лишила бы понятие закрытого
члена всякого смысла. Более того, нельзя было бы узнать все
использования закрытого имени посмотрев на функции, описанные как члены и
друзья этого класса. Пришлось бы проверять каждый исходный файл во всей
программе на наличие в нем производных классов, потом исследовать
каждую функцию этих классов, потом искать все классы, производные от
этих классов, и т.д. Это по меньшей мере утомительно и скорее всего
нереально. С другой стороны, можно ведь использовать механизм friend,
чтобы предоставить такой доступ или отдельным функциям, или всем
функциям отдельного класса. Например:
class employee {
friend void manager::print();
// ...
};
решило бы проблему с manager::print(), и
class employee {
friend class manager;
// ...
};
сделало
бы доступным каждый член employee для всех функций класса manager.
В частности, это сделает name доступным для
manager::print().
Другое, иногда более прозрачное решение для
производного класса, использовать только открытые члены его базового
класса. Например:
void manager::print()
{
employee::print(); // печатает информацию о служащем
// ... // печатает информацию о менеджере
}
Заметьте,
что надо использовать ::, потому что print() была
переопределена в manager. Такое повторное использование имен
типично. Неосторожный мог бы написать так:
void manager::print()
{
print(); // печатает информацию о служащем
// ... // печатает информацию о менеджере
}
и
обнаружить, что программа после вызова manager::print()
неожиданно попадает в последовательность рекурсивных вызовов.
Видимость
Класс 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() компилироваться не
будет. Друзья производного класса имеют к членам базового класса
такой
же доступ, как и функции члены. Поскольку, как оказывается, описание
открытых базовых классов встречается чаще описания закрытых, жалко,
что описание открытого базового класса длиннее описания закрытого. Это,
кроме того, служит источником запутывающих ошибок у начинающих.
Когда описывается производная 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 - нет. Естественно, сделать сделать закрытый член базового
класса открытым членом производного класса невозможно. Невозможно с
помощью этой записи также сделать открытыми перегруженные имена.
Подытоживая, можно сказать, что вместе с предоставлением средств
дополнительно к имеющимся в базовом классе, производный класс можно
использовать для того, чтобы сделать средства (имена) недоступными для
пользователя. Другими словами, с помощью производного класса можно
обеспечивать прозрачный, полупрозрачный и непрозрачный доступ к его
базовому классу.
Указатели
Если производный класс derived
имеет открытый базовый класс base, то указатель на derived можно
присваивать переменной типа указатель на base не используя явное
преобразование типа. Обратное преобразование, указателя на base в
указатель на derived, должно быть явным. Например:
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. Закрытый член класса может использоваться
только членами и друзьями этого класса.
Иерархия Типов
Производный класс сам может быть базовым классом. Например:
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), не написав специальной программы. Однако во многих
полезных программах этот метод успешно используется.