C++: производные классы (наследование) |
![]() |
Добавил(а) microsin |
Любой тип класса (объявленный через ключевое слово class или ключевое слово struct) может быть декларирован как производный (derived) от одного или большего количества базовых классов, которые, в свою очередь, могут быть производными от своих собственных базовых классов, образуя тем самым иерархию наследования. Список базовых классов предоставляется точке базы синтаксиса декларации класса. Точка базы состоит из символа :, за которым идет список из одного или большего количества спецификаторов базовых классов, разделяемые запятой. attr(опционально) access-specifier(опционально) virtual-specifier(опционально) декларация-класса-или-типа Здесь назначение полей декларации следующее: attr(C++11) - опциональная (её наличие не обязательно) последовательность любого количества атрибутов 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. |