6.2.1 Правила наследования различных методов
Конструкторы не наследуются, поэтому производный класс должен иметь собственные конструкторы.
Если в конструкторе производного класса явный вызов конструктора ба- зового класса отсутствует, автоматически вызывается конструктор базового класса по умолчанию (то есть тот, который можно вызвать без параметров).
Это использовано в первом из конструкторов класса daemon
Для иерархии, состоящей из нескольких уровней, конструкторы базовых классов вызываются начиная с самого верхнего уровня. После этого выпол- няются конструкторы тех элементов класса, которые являются объектами, в порядке их объявления в классе, а затем исполняется конструктор класса.
В случае нескольких базовых классов их конструкторы вызываются в по- рядке объявления.
48
Если конструктор базового класса требует указания параметров, он дол- жен быть явным образом вызван в конструкторе производного класса в списке инициализации (это продемонстрировано в трех последних кон- структорах).
Не наследуется и операция присваивания, поэтому ее также требуется явно определить в классе daemon
. Обратите внимание на запись функции- операции: в ее теле применен явный вызов функции-операции присваивания из базового класса. Чтобы лучше представить себе синтаксис вызова, клю- чевое слово operator вместе со знаком операции можно интерпретировать как имя функции-операции.
Вызов функций базового класса предпочтительнее копирования фраг- ментов кода из функций базового класса в функции производного. Кроме сокращения объема кода, этим достигается упрощение модификации про- граммы: изменения требуется вносить только в одну точку программы, что сокращает количество возможных ошибок.
6.2.2 Правила для деструкторов при наследовании Деструкторы не наследуются, и если программист не описал в производ- ном классе деструктор, он формируется по умолчанию и вызывает деструк- торы всех базовых классов.
В отличие от конструкторов, при написании деструктора производного класса в нем не требуется явно вызывать деструкторы базовых классов, по- скольку это будет сделано автоматически.
Для иерархии классов, состоящей из нескольких уровней,
деструкторы вызываются в порядке, строго обратном вызову конструкторов: сначала вы- зывается деструктор класса, затем – деструкторы элементов класса, а потом деструктор базового класса.
Поля, унаследованные из класса monster
, недоступны функциям произ- водного класса, поскольку они определены в базовом классе как private
Если функциям, определенным в daemon
, требуется работать с этими по- лями, можно либо описать их в базовом классе как protected
, либо обра- щаться к ним с помощью функций из monster
, либо явно переопределить их в daemon так, как было показано в предыдущем разделе.
Добавляемые поля в наследнике могут совпадать и по имени, и по типу с полями базового класса. При этом поле предка будет скрыто.
Статические поля, объявленные в базовом классе, наследуются обычным образом. Все объекты базового класса и всех его наследников разделяют единственную копию статических полей базового класса.
49
Рассматривая наследование методов, обратите внимание на то, что в классе daemon описан метод draw
, переопределяющий метод с тем же име- нем в классе monster
(поскольку отрисовка различных персонажей, есте- ственно, выполняется по-разному). Таким образом, производный класс мо- жет не только дополнять, но и корректировать поведение базового класса.
Доступ к переопределенному методу базового класса для производного класса выполняется через уточненное с помощью операции доступа к обла- сти видимости имя.
Класс-потомок наследует все методы базового класса, кроме конструк- торов, деструктора и операции присваивания. Не наследуются ни друже- ственные функции, ни дружественные отношения классов.
В классе-наследнике можно определять новые методы. В них разреша- ется вызывать любые доступные методы базового класса. Если имя метода в наследнике совпадает с именем метода базового класса, то метод произ- водного класса скрывает все методы базового класса с таким именем. При этом прототипы методов могут не совпадать. Если в методе-наследнике тре- буется вызвать одноименный метод родительского класса, нужно задать его с префиксом класса. Это же касается и статических методов.
6.3 Виртуальные методы Работа с объектами чаще всего производится через указатели. Указателю на базовый класс можно присвоить значение адреса объекта любого произ- водного класса, например: monster *p; // Описывается указатель на базовый класс p = new daemon;
/* Указатель ссылается на объект производного класса */
Вызов методов объекта происходит в соответствии с типом указателя, а не
фактическим типом объекта, на который он ссылается, поэтому при вы- полнении оператора, например: p -> draw(1, 1, 1, 1);
В результате будет вызван метод класса monster
, а не класса daemon
, поскольку ссылки на методы разрешаются во время компоновки про- граммы. Этот процесс называется ранним связыванием. Чтобы вызвать ме- тод класса daemon
, можно использовать явное преобразование типа указа- теля:
((daemon * p)) -> draw(1, 1, 1, 1);
50
Это не всегда возможно, поскольку в разное время указатель может ссы- латься на объекты разных классов иерархии, и во время компиляции про- граммы конкретный класс может быть неизвестен.
В качестве примера можно привести функцию, параметром которой яв- ляется указатель на объект базового класса. На его место во время выполне- ния программы может быть передан указатель любого производного класса.
Другой пример – связный список указателей на различные объекты иерар- хии, с которым требуется работать единообразно.
В С++ реализован механизм позднего связывания, когда разрешение ссылок на функцию происходит на этапе выполнения программы в зависи- мости от конкретного типа объекта, вызвавшего функцию. Этот механизм реализован с помощью виртуальных методов.
Для определения виртуального метода используется спецификатор virtual
: virtual void draw(int x, int y, int scale, int position);
Рассмотрим правила использования виртуальных методов:
Если в базовом классе метод определен как виртуальный, метод, опре- деленный в производном классе с тем же именем и набором параметров, ав- томатически становится виртуальным, а с отличающимся набором парамет- ров – обычным.
Виртуальные методы наследуются, то есть переопределять их в про- изводном классе требуется только при необходимости задать отличающиеся действия. Права доступа при переопределении изменить нельзя.
Если виртуальный метод переопределен в производном классе, объ- екты этого класса могут получить доступ к методу базового класса с помо- щью операции доступа к области видимости.
Виртуальный метод не может объявляться с модификатором static
, но может быть объявлен как дружественная функция.
Если производный класс содержит виртуальные методы, они должны быть определены в базовом классе хотя бы как чисто виртуальные.
Чисто виртуальный метод содержит признак = 0 вместо тела, например: virtual void f(int) = 0;
Чисто виртуальный метод должен переопределяться в производном классе (возможно, опять как чисто виртуальный).
Если определить метод draw в классе monster как виртуальный, реше- ние о том, метод какого класса вызвать, будет приниматься в зависимости от типа объекта, на который ссылается указатель:
51 monster *r, *p; r = new monster; // Создается объект класса monster p = new daemon;
// Создается объект класса daemon r -> draw(1,1,1,1);
// Вызывается метод monster::draw p -> draw(1,1,1,1);
// Вызывается метод daemon::draw p -> monster::draw(1,1,1,1); // Обход механизма виртуальных методов
Если объект класса daemon будет вызывать метод draw не непосред- ственно, а косвенно (то есть из другого метода, который может быть опре- делен только в классе monster
), будет вызван метод draw класса daemon
Итак, виртуальным называется метод, ссылка на который разрешается на этапе выполнения программы (перевод красивого английского слова virtual
– всего-навсего «фактический», то есть ссылка разрешается по факту вызова).
6.3.1 Механизм позднего связывания
Для каждого класса (не объекта!), содержащего хотя бы один виртуаль- ный метод, компилятор создает таблицу виртуальных методов (
vtbl
), в ко- торой для каждого виртуального метода записан его адрес в памяти. Адреса методов содержатся в таблице в порядке их описания в классах. Адрес лю- бого виртуального метода имеет в vtbl одно и то же смещение для каждого класса в пределах иерархии.
Каждый объект содержит скрытое дополнительное поле ссылки на vtbl
, называемое vptr
. Оно заполняется конструктором при создании объекта
(для этого компилятор добавляет в начало тела конструктора соответствую- щие инструкции).
На этапе компиляции ссылки на виртуальные методы заменяются на об- ращения к vtbl через vptr объекта, а на этапе выполнения в момент обра- щения к методу его адрес выбирается из таблицы. Таким образом, вызов виртуального метода, в отличие от обычных методов и функций, выполня- ется через дополнительный этап получения адреса метода из таблицы. Это несколько замедляет выполнение программы, поэтому без необходимости делать методы виртуальными смысла не имеет.
Рекомендуется делать виртуальными деструкторы для того, чтобы га- рантировать правильное освобождение памяти из-под динамического объ- екта, поскольку в любой момент времени будет выбран деструктор, соответ- ствующий фактическому типу объекта.
Четкого правила, по которому метод следует делать виртуальным, не су- ществует. Можно только дать рекомендацию объявлять виртуальными ме-
52 тоды, для которых есть вероятность, что они будут переопределены в про- изводных классах. Методы, которые во всей иерархии останутся неизмен- ными или те, которыми производные классы пользоваться не будут, делать виртуальными нет смысла. С другой стороны, при проектировании иерар- хии не всегда можно предсказать, каким
образом будут расширяться базо- вые классы, особенно при проектировании библиотек классов, а объявление метода виртуальным обеспечивает гибкость и возможность расширения.
Для пояснения последнего тезиса представим себе, что вызов метода draw осуществляется из метода перемещения объекта. Если текст метода перемещения не зависит от типа перемещаемого объекта (поскольку прин- цип перемещения всех объектов одинаков, а для отрисовки вызывается кон- кретный метод), переопределять этот метод в производных классах нет необходимости, и он может быть описан как невиртуальный. Если метод draw виртуальный, метод перемещения сможет без перекомпиляции рабо- тать с объектами любых производных классов – даже тех, о которых при его написании ничего известно не было.
Виртуальный механизм работает только при использовании указателей или ссылок на объекты. Объект, определенный через указатель или ссылку и содержащий виртуальные методы, называется полиморфным. В данном случае полиморфизм состоит в том, что с помощью одного и того же обра- щения к методу выполняются различные действия в зависимости от типа, на который ссылается указатель в каждый момент времени.
6.4 Виртуальный деструктор В языке программирования C++ деструктор полиморфного базового класса должен объявляться виртуальным. Только так обеспечивается кор- ректное разрушение объекта производного класса через указатель на соот- ветствующий базовый класс.
Рассмотрим следующий пример:
#include
using namespace std;
// Вспомогательный класс class Object
{ public:
Object() { cout << "Object::ctor()" << endl; }
Object() { cout << "Object::dtor()" << endl; }
};
// Базовый класс class Base
{
53 public:
Base() { cout << "Base::ctor()" << endl; } virtual Base() { cout << "Base::dtor()" << endl; } virtual void print() = 0;
};
// Производный класс class Derived: public Base
{ public:
Derived() { cout << "Derived::ctor()" << endl; }
Derived() { cout << "Derived::dtor()" << endl; } void print() {}
Object obj;
}; int main ()
{
Base * p = new Derived; delete p; return 0;
}
В функции main указателю на базовый класс присваивается адрес дина- мически создаваемого объекта производного класса
Derived
. Затем через этот указатель объект разрушается. При этом наличие виртуального де- структора базового класса обеспечивает вызовы деструкторов всех классов в ожидаемом порядке, а именно, в порядке, обратном вызовам конструкто- ров соответствующих классов.
Вывод программы с использованием виртуального деструктора в базо- вом классе будет следующим:
Base::ctor()
Object::ctor()
Derived::ctor()
Derived::dtor()
Object::dtor()
Base::dtor()
Уничтожение объекта производного класса через указатель на базовый класс с невиртуальным деструктором дает неопределенный результат. На практике это выражается в том, что будет разрушена только часть объекта, соответствующая базовому классу. Если в коде выше убрать ключевое слово virtual перед деструктором базового класса, то вывод программы будет уже иным. Обратите внимание, что член данных obj класса
Derived также не разрушается.
Base::ctor()
Object::ctor()
Derived::ctor()
Base::dtor()
54
Когда же следует объявлять деструктор виртуальным? Cуществует пра- вило – если базовый класс предназначен для полиморфного использования, то его деструктор должен объявляться виртуальным. Для реализации меха- низма виртуальных функций каждый объект класса хранит указатель на таб- лицу виртуальных функций vptr
, что увеличивает его общий размер.
Обычно, при объявлении виртуального деструктора такой класс уже имеет виртуальные функции, и увеличения размера соответствующего объекта не происходит.
Если же базовый класс не предназначен для полиморфного использова- ния (не содержит виртуальных функций), то его деструктор не должен объ- являться виртуальным.
6.5 Абстрактные классы Класс, содержащий
хотя бы один чисто виртуальный метод, называется абстрактным. Абстрактные классы предназначены для представления об- щих понятий, которые предполагается конкретизировать в производных классах. Абстрактный класс может использоваться только в качестве базо- вого для других классов – объекты абстрактного класса создавать нельзя, поскольку прямой или косвенный вызов чисто виртуального метода приво- дит к ошибке при выполнении.
При определении абстрактного класса необходимо иметь в виду следу- ющее:
Абстрактный класс нельзя использовать при явном приведении типов, для описания типа параметра и типа возвращаемого функцией значения.
Допускается объявлять указатели и ссылки на абстрактный класс, если при инициализации не требуется создавать временный объект.
Если класс, производный от абстрактного, не определяет все чисто виртуальные функции, он также является абстрактным.
Таким образом, можно создать функцию, параметром которой является указатель на абстрактный класс. На место этого параметра при выполнении программы может передаваться указатель на объект любого производного класса. Это позволяет создавать полиморфные функции, работающие с объ- ектом любого типа в пределах одной иерархии.
55
7. МНОЖЕСТВЕННОЕ НАСЛЕДОВАНИЕ В ООП Множественное наследование означает, что класс имеет несколько базо- вых классов. При этом, если в базовых классах есть одноименные элементы, может произойти конфликт идентификаторов, который устраняется с помо- щью операции доступа к области видимости: class monster
{ public: int get_health();
}; class hero
{ public: int get_health();
}; class being: public monster, public hero{ ... }; int main(){ being A; cout << A.monster::get_health(); cout << A.hero::get_health();}
Использование конструкции
A.get_health()
приведет к ошибке, по- скольку компилятор не в состоянии разобраться, метод какого из базовых классов требуется вызвать.
Если у базовых классов есть общий предок, это приведет к тому, что про- изводный от этих базовых класс унаследует два экземпляра полей предка, что чаще всего является нежелательным. Чтобы избежать такой ситуации, требуется при наследовании общего предка определить его как виртуаль- ный класс: class monster{ ... }; class daemon: virtual public monster{ ... }; class lady: virtual public monster{ ... }; class god: public daemon, public lady{ ... };
Класс god содержит только один экземпляр полей класса monster
7.1 Альтернатива наследованию Наследование – это
очень сложная тема, и даже создатель языка С++
Б. Страуструп рекомендует везде, где это возможно, обходиться без него.
Альтернативой наследованию при проектировании классов является вложение, когда один класс включает в себя поля, являющиеся классами или указателями на них. Например, если есть класс «двигатель», а требуется
56 описать класс «самолет», логично сделать двигатель полем этого класса, а не его предком.
Вид вложения, когда в классе описано поле объектного типа, называют композицией. Если в классе описан указатель на объект другого класса, это обычно называют агрегацией. При композиции время жизни всех объектов
(и объемлющего, и его полей) одинаково. Агрегация представляет собой бо- лее слабую связь между объектами, потому что объекты, на которые ссыла- ются поля-указатели, могут появляться и исчезать в течение жизни содер- жащего их объекта, кроме того, один и тот же указатель может ссылаться на объекты разных классов в пределах одной иерархии. Поле-указатель может также ссылаться не на один объект, а на неопределенное количество объек- тов, например быть указателем на начало линейного списка.
7.2 Отличия структур и объединений от классов Структуры (
struct
) и объединения (
union
) представляют собой частные случаи классов. Структуры отличаются от классов тем, что
доступ к элемен- там, а также базовый класс при наследовании по умолчанию считаются public
. Структуры предпочтительнее использовать для объектов, все эле- менты которых доступны.
Доступ в объединениях также устанавливается public
, кроме того, в них вообще нельзя использовать спецификаторы доступа. Объединение не мо- жет участвовать в иерархии классов. Элементами объединения не могут быть объекты, содержащие конструкторы и деструкторы. Объединение мо- жет иметь конструктор и другие методы, только не статические. В аноним- ном объединении методы описывать нельзя.