Программирование ARM C++: производные классы (наследование) Thu, November 21 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


C++: производные классы (наследование) Печать
Добавил(а) microsin   

Любой тип класса (объявленный через ключевое слово 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++: разрешение имен.

 

Добавить комментарий


Защитный код
Обновить

Top of Page