Любой тип класса (объявленный через ключевое слово class или ключевое слово struct) может быть декларирован как производный (derived) от одного или большего количества базовых классов, которые, в свою очередь, могут быть производными от своих собственных базовых классов, образуя тем самым иерархию наследования.
Список базовых классов предоставляется точке базы синтаксиса декларации класса. Точка базы состоит из символа :, за которым идет список из одного или большего количества спецификаторов базовых классов, разделяемые запятой.
attr(опционально) access-specifier(опционально) virtual-specifier(опционально) декларация-класса-или-типа
Здесь назначение полей декларации следующее:
attr(C++11) - опциональная (её наличие не обязательно) последовательность любого количества атрибутов access-specifier - одно из ключевых слов: private, public или protected virtual-specifier - ключевое слово virtual [2].
virtual-specifier и access-specifier могут появляться в любом порядке.
Базовыми спецификаторами в определении базы предложении могут быть расширения пакетов (C++11). Класс или структура, объявленные final, не могут отображаться в определении базы.
Если access-specifier опущен, то по умолчанию он доступен как public для классов, декларированных с ключевым словом class, и защищен от доступа как private для классов, декларированных через struct.
struct Base
{
int a, b, c;
};
// Каждый объект типа Derived включает Base как суб-объект.
struct Derived : Base
{
int b;
};
// Каждый объект типа Derived2 включает Derived и Base как суб-объекты.
struct Derived2 : Derived
{
int c;
};
Классы, перечисленные в определении базы, являются прямыми базовыми классами (direct base classes). Их базы являются косвенными базовыми классами (indirect base classes). Один и тот же класс не может быть указан как прямой базовый класс более одного раза, но один и тот же класс может быть прямым и косвенным базовым классом.
Каждый прямой и косвенный базовый класс присутствует в качестве суб-объекта базового класса в представлении объекта производного класса при смещении, определенном реализацией. Пустые базовые классы обычно не увеличивают размер производного объекта благодаря оптимизации пустой базы. Конструкторы суб-объектов базового класса вызываются конструктором производного класса: аргументы для этих конструкторов могут быть предоставлены в списке инициализаторов элементов (member initializer list).
[Виртуальные базовые классы]
Для каждого отдельного базового класса, указанного как virtual, самый ближний производный (most derived) объект содержит только один суб-объект базового класса этого типа, даже если класс появляется много раз в иерархии наследования (при условии, что он каждый раз наследуется как virtual).
struct B { int n; };
class X : public virtual B {};
class Y : virtual public B {};
class Z : public B {};
// Каждый объект типа AA содержит один X, один Y, один Z, и два B:
// один это база Z, и один общий между X и Y:
struct AA : X, Y, Z
{
AA()
{
X::n = 1; // модифицирует член суб-объекта virtual B
Y::n = 2; // модифицирует тот же самый член суб-объекта virtual B
Z::n = 3; // модифицирует член суб-объекта non-virtual B
std::cout << X::n << Y::n << Z::n << '\n'; // печатает 223
}
};
Примером иерархии наследования с базовыми классами virtual служит иерархия потоков ввода/вывода стандартной библиотеки: std::istream и std::ostream произведены от std::ios с использованием virtual-наследования. std::iostream произведен из обоих std::istream и std::ostream, так что каждый экземпляр std::iostream содержит суб-объект std::ostream, суб-объект std::istream и только один суб-объект std::iost (и, как следствие, один std::ios_base).
Все базовые суб-объекты virtual инициализируются перед любым базовым суб-объектом non-virtual, так что только самый ближний по наследованию (most derived) класс вызывает конструкторы virtual базовых классов в своем списке инициализаторов членов:
struct B
{
int n;
B(int x) : n(x) {}
};
struct X : virtual B { X() : B(1) {} };
struct Y : virtual B { Y() : B(2) {} };
struct AA : X, Y { AA() : B(3), X(), Y() {} };
// Конструктор по умолчанию AA вызывает конструкторы X и Y,
// однако эти конструкторы не вызывают конструктор B, потому
// что B это виртуальный базовый класс.
AA a; // a.n == 3// Конструктор по умолчанию X вызовет конструктор B
X x; // x.n == 1
Существуют специальные правила для неквалифицированного разрешения имен (unqualified name lookup) для членов класса при использовании virtual наследования (иногда называемые правилами доминирования), см. "Member function definition" статьи [4].
[Публичное наследование]
Когда класс использует спецификатор доступа членов public для наследования из базового класса, все public-члены базового класса доступны как public-члены в производном классе, и все protected-члены базового класса доступны как protected-члены в производном классе (private-члены базового класса всегда недоступны, если не объявлены дружественными).
Публичное наследование моделирует связь суб-типов объектно-ориентированного программирования: объект производного класса является объектом базового IS-A класса. Ожидается, что ссылки (references) и указатели (pointers) на производный объект можно использовать в любом коде, предполагающем нормальное использование ссылок или указателей на любой из public базовых классов (см. LSP [5]) или, в терминах DbC [6], производный класс должен поддерживать инварианты классов своих public базовых классов, не усиливая любое предварительное условие и не ослабляя любое пост-условие функции-члена, которого он переопределяет.
struct MenuOption { std::string title; };
// Класс Menu это вектор MenuOption: опции можно вставлять, удалять,
// переупорядочивать... И Menu имеет заголовок.
class Menu : public std::vector< MenuOption>
{
public:
std::string title;
void print() const
{
std::cout << title << ":\n";
for (std::size_t i = 0, s = size(); i < s; ++i)
std::cout << " " << (i+1) << ". " << at(i).title << '\n';
}
};
// Замечание: Menu::title не проблематично, потому что его роль
// не зависит от базового класса.
enum class Color { WHITE, RED, BLUE, GREEN };
void apply_terminal_color(Color c) { /* Специфика OS */ }
// ЭТО НЕПРАВИЛЬНО!
// ColorMenu это Menu где каждая опция имеет пользовательский цвет.
class ColorMenu : public Menu
{
public:
std::vector< Color> colors;
void print() const
{
std::cout << title << ":\n";
for (std::size_t i = 0, s = size(); i < s; ++i) {
std::cout << " " << (i+1) << ". ";
apply_terminal_color(colors[i]);
std::cout << at(i).title << '\n';
apply_terminal_color(Color::WHITE);
}
}
};
// ColorMenu нуждается в следующих инвариантах, которые не могут быть
// удовлетворены публичным наследованием от Menu, например:
// - ColorMenu::colors и Menu должны иметь одинаковое количество элементов
// - Чтобы был смысл, вызов erase() должен удалить также элементы из цветов,
// чтобы позволить опциям сохранить их цвета
// В основном каждый не-const вызов метода std::vector нарушит инвариант
// ColorMenu и потребует исправления пользователем путем корректного
// управления цветами.
int main()
{
ColorMenu color_menu;
// Большая проблема этого класса в том, что мы должны сохранять ColorMenu::Color
// в синхронным с Menu.
color_menu.push_back(MenuOption{"Some choice"});
// color_menu.print(); // Ошибка! colors[i] в print() вне допустимых пределов
color_menu.colors.push_back(Color::RED);
color_menu.print(); //OK: цвета и Menu имеют одинаковое количество элементов
return 0;
}
[Защищенное наследование]
Когда класс использует спецификатор доступа членов protected для наследования из базового класса, все public-члены базового класса доступны как protected-члены в производном классе (private-члены базового класса всегда недоступны, если не объявлены дружественными).
Protected-наследование может использоваться для "управляемого полиморфизма": внутри членов производного класса Derived, как и внутри членов всех последующих производных классов, производный класс ведет себя как IS-A базовый: ссылки и указатели на Derived могут использоваться там, где ожидаются ссылки и указатели на базовый класс Base.
[Приватное наследование]
Когда класс использует спецификатор доступа членов private для наследования из базового класса, все public-члены и protected-члены базового класса доступны как private-члены в производном классе (private-члены базового класса всегда недоступны, если не объявлены дружественными).
Private-наследование обычно используется в основанном на политике дизайне, где политики обычно это пустые классы, и использование их как базовых классов разрешает статический полиморфизм, и одновременно использует оптимизацию пустых базовых классов.
Private-наследование также может использоваться для реализации взаимоотношения композиции (суб-объект базового класса является детализацией объекта производного класса). Использование члена предоставляет лучшую инкапсуляцию и обычно является предпочтительным, если только производный класс не требует доступа к protected-членам (включая конструкторы) базового класса. Это потребует переопределить virtual-член базового класса, при этом базовый класс должен быть сконструирован перед и уничтожен после некоторого другого базового суб-объекта, что нуждается в совместном использовании virtual-базового класса или нуждается в управлении конструированием virtual-базового класса. Использование членов для реализации композиции также не применимо в случае множественного наследования от пакета параметров, или когда идентификаторы базовых классов определяются во время компиляции посредством мета-программирования шаблона.
Подобно protected-наследованию, private-наследование может также использоваться для управляемого полиморфизма: внутри членов производного класса (но не внутри членов последующих производных классов), производных от базового IS-A.
template< typename Transport>
class service : private Transport // private-наследование от политики Transport
{
public:
void transmit()
{
this->send(...); // отправка через любой доступный транспорт
}
};
// Политика транспорта TCP
class tcp
{
public:
void send(...);
};
// Политика транспорта UDP
class udp
{
public:
void send(...);
};
service< tcp> service(host, port);
service.transmit(...); // Отправка через TCP
[Разрешение имен членов (member name lookup)]
Правила разрешения unqualified-имен и qualified-имен для членов класса см. в статье [7].
[Ссылки]
1. Derived classes site:cppreference.com. 2. C++: ключевое слово virtual. 3. C++: абстрактные классы. 4. Unqualified name lookup site:cppreference.com. 5. Liskov substitution principle site:wikipedia.org. 6. Design by contract site:wikipedia.org. 7. C++: разрешение имен. |