Наследуются ли конструкторы и деструкторы

Обновлено: 04.05.2024

Когда-то давным давно я собирался и даже обещал написать про механизм виртуальных функций относительно деструкторов. Теперь у меня наконец появилось свободное время и я решил воплотить эту затею в жизнь. На самом деле эта мини-статья служит «прологом» к моей следующей статье. Но я постарался изложить доходчиво и понятно основные моменты по текущей теме. Если вы чувствуете, что еще недостаточно разобрались в механизме виртуальных вызовов, то, возможно, вам следует для начала прочитать мою предыдущую статью.

Сразу же, как обычно, оговорюсь, что: 1) статья моя не претендует на полноту изложения материала; 2) мегапрограммеры ничего нового здесь не узнают; 3) материал не новый и давно описан во многих книгах, но если явно об этом не прочитать и самому специально не задумываться, то можно о некоторых моментах даже не подозревать (до поры, до времени). Также прошу прощения за надуманные примеры :)

Виртуальные деструкторы

Если вы уже знаете и умеете использовать виртуальные функции, то просто обязаны знать, когда и зачем нужны виртуальные деструкторы. Иначе нижеследующий текст был написан именно для вас.

Основное правило: если у вас в классе присутствует хотя бы одна виртуальная функция, деструктор также следует сделать виртуальным. При этом не следует забывать, что деструктор по умолчанию виртуальным не будует, поэтому следует объявить его явно. Если этого не сделать, у вас в программе почти наверняка будут утечки памяти (memory leaks). Чтобы понять почему, опять же много ума не надо. Рассмотрим несколько примеров.

В первом случае создадим объект производного класса в стеке:

using std :: cout ;
using std :: endl ;

Всем ясно, что вывод программы будет следующим:

потому что сначала конструируется базовая часть класса, затем производная, а при разрушении наоборот — сначала вызывается деструктор производного класса, который по окончании своей работы вызывает по цепочке деструктор базового. Это правильно и так должно быть.

Попробуем теперь создать тот же объект в динамической памяти, используя при этом указатель на объект базового класса (код классов не изменился, поэтому привожу только код функции main()):

На сей раз конструируется объект так, как и надо, а при разрушении происходит утечка памяти, потому как деструктор производного класса не вызывается:

Происходит это потому, что удаление производится через указатель на базовый класс и для вызова деструктора компилятор использует раннее связывание. Деструктор базового класса не может вызвать деструктор производного, потому что он о нем ничего не знает. В итоге часть памяти, выделенная под производный класс, безвозвратно теряется.

Чтобы этого избежать, деструктор в базовом классе должен быть объявлен как виртуальный:

using std :: cout ;
using std :: endl ;

int main ( )
<
A * pA = new B ;
delete pA ;
return EXIT_SUCCESS ;
>

Теперь-то мы получим желаемый порядок вызовов:

Происходит так потому, что отныне для вызова деструктора используется позднее связывание, то есть при разрушении объекта берется указатель на класс, затем из таблицы виртуальных функций определяется адрес нужного нам деструктора, а это деструктор производного класса, который после своей работы, как и полагается, вызывает деструктор базового. Итог: объект разрушен, память освобождена.

Виртуальные функции в деструкторах

Давайте для начала рассмотрим ситуацию с вызовом виртуальных функций внутри класса. Предположим, что у нас есть Кот, который просит покушать мяуканьем, а затем приступает к процессу :) Так поступают многие коты, но не Чеширский! Чеширский, как известно, мало того что вечно улыбается, так еще и довольно разговорчив, поэтому мы научим его говорить, переопределив метод speak():

using std :: cout ;
using std :: endl ;

class Cat
<
public :
void askForFood ( ) const
<
speak ( ) ;
eat ( ) ;
>
virtual void speak ( ) const < cout
virtual void eat ( ) const < cout
> ;

class CheshireCat : public Cat
<
public :
virtual void speak ( ) const < cout
> ;

delete cats [ 0 ] ; delete cats [ 1 ] ;
return EXIT_SUCCESS ;
>

Вывод этой программы будет следующим:

Рассмотрим код более подробно. Есть класс Cat с парой виртуальных методов, один из которых переопределен в производном CheshireCat. Но всё самое интересное происходит в методе askForFood() класса Cat.

Как видно, метод всего лишь содержит вызовы двух других методов, однако конструкция speak() в данном контексте эквивалента this->speak(), то есть вызов происходит через указатель, а значит — будет использовано позднее связывание. Вот почему при вызове метода askForFood() через указатель на CheshireCat мы видим то, что и хотели: механизм виртуальных функций работает исправно даже несмотря на то, что вызов непосредственно виртуального метода происходит внутри другого метода класса.

А теперь самое интересное: что будет, если попытаться воспользоваться этим в деструкторе? Модернизируем код так, чтобы при деструкции наши питомцы прощались, кто как умеет:

using std :: cout ;
using std :: endl ;

class Cat
<
public :
virtual ~Cat ( ) < sayGoodbye ( ) ; >
void askForFood ( ) const
<
speak ( ) ;
eat ( ) ;
>
virtual void speak ( ) const < cout
virtual void eat ( ) const < cout
virtual void sayGoodbye ( ) const < cout
> ;

class CheshireCat : public Cat
<
public :
virtual void speak ( ) const < cout
virtual void sayGoodbye ( ) const < cout
> ;

delete cats [ 0 ] ; delete cats [ 1 ] ;
return EXIT_SUCCESS ;
>

Можно ожидать, что, как и в случае с вызовом метода speak(), будет выполнено позднее связывание, однако это не так:

Ordinary Cat: Meow! *champing*
Cheshire Cat: WTF?! Where's my milk? =) *champing*
Meow-meow!
Meow-meow!

Почему? Да потому что при вызове виртуальных методов из деструктора компилятор использует не позднее, а раннее связывание. Если подумать, зачем он делает именно так, всё становится очевидным: нужно просто рассмотреть порядок конструирования и разрушения объектов. Все помнят, что конструирование объекта происходит, начиная с базового класса, а разрушение идет в строго обратном порядке. Таким образом, когда мы создаем объект типа CheshireCat, порядок вызовов конструкторов/деструкторов будет таким:

Если же мы захотим внутри деструктора ~Cat() совершить виртуальный вызов метода sayGoodbye(), то фактически попытаемся обратиться к той части объекта, которая уже была разрушена.

Мораль: если в вашей голове витают помыслы выделить какой-то алгоритм «зачистки» в отдельный метод, переопределяемый в производных классах, а затем виртуально вызывать его в деструкторе, у вас ничего не выйдет.

В этой статье наследование описано на трех уровнях: beginner, intermediate и advanced. Expert нет. И ни слова про SOLID. Честно.

Что такое наследование?

Наследование является одним из основополагающих принципов ООП. В соответствии с ним, класс может использовать переменные и методы другого класса как свои собственные.

Класс, который наследует данные, называется подклассом (subclass), производным классом (derived class) или дочерним классом (child). Класс, от которого наследуются данные или методы, называется суперклассом (super class), базовым классом (base class) или родительским классом (parent). Термины “родительский” и “дочерний” чрезвычайно полезны для понимания наследования. Как ребенок получает характеристики своих родителей, производный класс получает методы и переменные базового класса.

Наследование полезно, поскольку оно позволяет структурировать и повторно использовать код, что, в свою очередь, может значительно ускорить процесс разработки. Несмотря на это, наследование следует использовать с осторожностью, поскольку большинство изменений в суперклассе затронут все подклассы, что может привести к непредвиденным последствиям.

В этом примере, метод turn_on() и переменная serial_number не были объявлены или определены в подклассе Computer . Однако их можно использовать, поскольку они унаследованы от базового класса.

Важное примечание: приватные переменные и методы не могут быть унаследованы.

Типы наследования

В C ++ есть несколько типов наследования:

  • публичный ( public )- публичные ( public ) и защищенные ( protected ) данные наследуются без изменения уровня доступа к ним;
  • защищенный ( protected ) — все унаследованные данные становятся защищенными;
  • приватный ( private ) — все унаследованные данные становятся приватными.

Для базового класса Device , уровень доступа к данным не изменяется, но поскольку производный класс Computer наследует данные как приватные, данные становятся приватными для класса Computer .

Класс Computer теперь использует метод turn_on() как и любой приватный метод: turn_on() может быть вызван изнутри класса, но попытка вызвать его напрямую из main приведет к ошибке во время компиляции. Для базового класса Device , метод turn_on() остался публичным, и может быть вызван из main .

Конструкторы и деструкторы

В C ++ конструкторы и деструкторы не наследуются. Однако они вызываются, когда дочерний класс инициализирует свой объект. Конструкторы вызываются один за другим иерархически, начиная с базового класса и заканчивая последним производным классом. Деструкторы вызываются в обратном порядке.

Важное примечание: в этой статье не освещены виртуальные десктрукторы. Дополнительный материал на эту тему можно найти к примеру в этой статье на хабре.

Конструкторы: Device -> Computer -> Laptop .
Деструкторы: Laptop -> Computer -> Device .

Множественное наследование

Множественное наследование происходит, когда подкласс имеет два или более суперкласса. В этом примере, класс Laptop наследует и Monitor и Computer одновременно.

Проблематика множественного наследования

Множественное наследование требует тщательного проектирования, так как может привести к непредвиденным последствиям. Большинство таких последствий вызваны неоднозначностью в наследовании. В данном примере Laptop наследует метод turn_on() от обоих родителей и неясно какой метод должен быть вызван.

Несмотря на то, что приватные данные не наследуются, разрешить неоднозначное наследование изменением уровня доступа к данным на приватный невозможно. При компиляции, сначала происходит поиск метода или переменной, а уже после — проверка уровня доступа к ним.

Проблема ромба

Проблема ромба (Diamond problem)- классическая проблема в языках, которые поддерживают возможность множественного наследования. Эта проблема возникает когда классы B и C наследуют A , а класс D наследует B и C .

К примеру, классы A , B и C определяют метод print_letter() . Если print_letter() будет вызываться классом D , неясно какой метод должен быть вызван — метод класса A , B или C . Разные языки по-разному подходят к решению ромбовидной проблем. В C ++ решение проблемы оставлено на усмотрение программиста.

Ромбовидная проблема — прежде всего проблема дизайна, и она должна быть предусмотрена на этапе проектирования. На этапе разработки ее можно разрешить следующим образом:

  • вызвать метод конкретного суперкласса;
  • обратиться к объекту подкласса как к объекту определенного суперкласса;
  • переопределить проблематичный метод в последнем дочернем классе (в коде — turn_on() в подклассе Laptop ).

Если метод turn_on() не был переопределен в Laptop, вызов Laptop_instance.turn_on() , приведет к ошибке при компиляции. Объект Laptop может получить доступ к двум определениям метода turn_on() одновременно: Device:Computer:Laptop.turn_on() и Device:Monitor:Laptop.turn_on() .

Проблема ромба: Конструкторы и деструкторы

Поскольку в С++ при инициализации объекта дочернего класса вызываются конструкторы всех родительских классов, возникает и другая проблема: конструктор базового класса Device будет вызван дважды.

Виртуальное наследование

Виртуальное наследование (virtual inheritance) предотвращает появление множественных объектов базового класса в иерархии наследования. Таким образом, конструктор базового класса Device будет вызван только единожды, а обращение к методу turn_on() без его переопределения в дочернем классе не будет вызывать ошибку при компиляции.

Примечание: виртуальное наследование в классах Computer и Monitor не разрешит ромбовидное наследование если дочерний класс Laptop будет наследовать класс Device не виртуально ( class Laptop: public Computer, public Monitor, public Device <>; ).

Абстрактный класс

В С++, класс в котором существует хотя бы один чистый виртуальный метод (pure virtual) принято считать абстрактным. Если виртуальный метод не переопределен в дочернем классе, код не скомпилируется. Также, в С++ создать объект абстрактного класса невозможно — попытка тоже вызовет ошибку при компиляции.

Интерфейс

С++, в отличии от некоторых ООП языков, не предоставляет отдельного ключевого слова для обозначения интерфейса (interface). Тем не менее, реализация интерфейса возможна путем создания чистого абстрактного класса (pure abstract class) — класса в котором присутствуют только декларации методов. Такие классы также часто называют абстрактными базовыми классами (Abstract Base Class — ABC).

Несмотря на то, что наследование — фундаментальный принцип ООП, его стоит использовать с осторожностью. Важно думать о том, что любой код который будет использоваться скорее всего будет изменен и может быть использован неочевидным для разработчика путем.

Наследование от реализованного или частично реализованного класса

Если наследование происходит не от интерфейса (чистого абстрактного класса в контексте С++), а от класса в котором присутствуют какие-либо реализации, стоит учитывать то, что класс наследник связан с родительским классом наиболее тесной из возможных связью. Большинство изменений в классе родителя могут затронуть наследника что может привести к непредвиденному поведению. Такие изменения в поведении наследника не всегда очевидны — ошибка может возникнуть в уже оттестированом и рабочем коде. Данная ситуация усугубляется наличием сложной иерархии классов. Всегда стоит помнить о том, что код может изменяться не только человеком который его написал, и пути наследования очевидные для автора могут быть не учтены его коллегами.

В противовес этому стоит заметить что наследование от частично реализованных классов имеет неоспоримое преимущество. Библиотеки и фреймворки зачастую работают следующим образом: они предоставляют пользователю абстрактный класс с несколькими виртуальными и множеством реализованных методов. Таким образом, наибольшее количество работы уже проделано — сложная логика уже написана, а пользователю остается только кастомизировать готовое решение под свои нужды.

Интерфейс

Наследование от интерфейса (чистого абстрактного класса) преподносит наследование как возможность структурирования кода и защиту пользователя. Так как интерфейс описывает какую работу будет выполнять класс-реализация, но не описывает как именно, любой пользователь интерфейса огражден от изменений в классе который реализует этот интерфейс.

Интерфейс: Пример использования

Прежде всего стоит заметить, что пример тесно связан с понятием полиморфизма, но будет рассмотрен в контексте наследования от чистого абстрактного класса.

Приложение выполняющее абстрактную бизнес логику должно настраиваться из отдельного конфигурационного файла. На раннем этапе разработки, форматирование данного конфигурационного файла до конца сформировано не было. Вынесение парсинга файла за интерфейс предоставляет несколько преимуществ.

Отсутствие однозначности касательно форматирования конфигурационного файла не тормозит процесс разработки основной программы. Два разработчика могут работать параллельно — один над бизнес логикой, а другой над парсером. Поскольку они взаимодействуют через этот интерфейс, каждый из них может работать независимо. Данный подход облегчает покрытие кода юнит тестами, так как необходимые тесты могут быть написаны с использованием мока (mock) для этого интерфейса.

Также, при изменении формата конфигурационного файла, бизнес логика приложения не затрагивается. Единственное чего требует полный переход от одного форматирования к другому — написания новой реализации уже существующего абстрактного класса (класса-парсера). В дальнейшем, возврат к изначальному формату файла требует минимальной работы — подмены одного уже существующего парсера другим.

Наследование предоставляет множество преимуществ, но должно быть тщательно спроектировано во избежание проблем, возможность для которых оно открывает. В контексте наследования, С++ предоставляет широкий спектр инструментов который открывает массу возможностей для программиста.

Не так давно наткнулся на хабре на статью о виртуальных функциях в С++ (она находится сейчас тут). Был бы рад добавить комментарий, но, как оказалось, тут надо иметь регистрацию. Собственно поэтому я и написал этот пост-дополнение к вышеуказанной статье.

В данной статье я хочу затронуть вопрос виртуальности конструкторов, деструкторов, а также специфичные вопросы, так или иначе связанные с виртуальностью функций.

Статья расчитана на программистов средней и высокой квалификации. Приятного чтения.

Виртуальные конструкторы в C++

Итак, пожалуй начнем с конструкторов. Тут все очень просто — виртуальных конструкторов (а также похожих на них конструкторов) в C++ не существует. Просто потому что не бывает и всё тут (конкретно: это запрещено стандартом языка).

Вы, наверно, спросите: «а зачем такое вообще может понадобится?». На самом деле разговор про «виртуальный конструктор» немного некорректны. Конструктор не может быть виртуальным в смысле виртуальных функций, т.к. чтобы была виртуальность необходимо в конструкторе (а больше и негде особо) настроить указатель на ТВМ (таблицу виртуальных функций) для создаваемого объекта.

Замечание: обычно виртуальность реализуется через ТВМ и указатель на нее в объекте. Подробнее вы можете прочесть об этом тут

Так вот, иногда «виртуальным конструктором» называют механизм создания объект любого заранее неизвестного класса. Это может пригодится, например, при копировании массива объектов, унаследованных от общего предка (при этом нам бы очень хотелось чтобы вызывался конструктор копирования именно нужного нам класса, конечно же). В C++ для подобного, обычно, используют виртуальную функцию вроде virtual void assign (const object &o) , или подобную, однако, это не является повсеместным, и возможны другие реализации.

Виртуальный деструктор

А вот деструктор, напротив может быть виртуальным. И даже более того — это часто встречается.
Обычным является использование вирт деструктора в классах, имеющих вирт функции. Более того, gcc, например, выдаст вам предупреждение, если вы не сделаете виртуальным деструктор, объявив виртуальную функцию.

Часто можно встретить миф: «вирт деструктор нужен лишь в том случае, когда на деструктор классов-потомков возлагаются какие-то нестандартные функции, если деструктор потомка не отличается по функционалу от родителя, то делать его виртуальным нет особого смысла». Это может и будет работать «сейчас», но может сыграть злую шутку в будущем, да и в общем-то не очень верно. Если деструктор не виртуальный, то будет вызван деструктор того типа, какой заявлен в указателе. В тоже время будет правильнее что для объектов потомков должны вызываться свои деструкторы. Просто стоит принять это как правило, иначе в будущем могут быть очень большие проблемы с отладкой непонятно почему текучих в плане памяти программ.

Другой миф: чисто виртуальных деструкторов не бывает. Ещё как бывают.

* This source code was highlighted with Source Code Highlighter .


Существует миф, что данный класс является абстрактным. И это верно.
Также распространено заблуждение, что налсденики этого класса будут полиморфными. Это неверно — деструкторы не наследуются.
Существут миф, что наследкника этого класса создать нельзя. Можно, вот пример:

class Sample public :
virtual ~Sample()=0<> //обратите особое внимание сюда, так писать по стандарту нельзя, но MS VC проглотит
>;

class DSample: public Sample

* This source code was highlighted with Source Code Highlighter .

Для вменяемых компиляторов класс Sample нужно писать так:

Sample::~Sample() >

* This source code was highlighted with Source Code Highlighter .

Сразу же замечание про нормальность компилятора, и заодно миф: по стандарту определять чисто вирт. функцию внутри определения класса нельзя. Но определенные корпорации говорят «если мы видим возможность улучшить стандарт, то мы незадумываясь делаем это».

Вы ниразу не видели чисто виртуальных деструкторов с определенными «телами»? Так вот, миф что они несуществуют, также неверен. Также определять можно и другие чисто виртуальные функции.

Почему надо писать именно с определением деструктора? Ответ на самом деле прост: из налсденика DSample в его деструкторе ~DSample будет вызываться деструктор ~Sample, и поэтому его необходимо определить, иначе у вас это даже не будет компилироваться.

Для чего нужен подобный чисто виртуальный деструктор? Это используется для того, чтобы сделать класс абстрактным, не создавая чисто виртуальных функций. Другого применения я найти не смог.

Замечания об устройстве указаталей на функцию-член

Казалось бы данная часть не имеет отношения к виртуальности. Если вы так думаете, то сильно заблуждаетесь. Вообще говоря указатели на функцию член в C++ используются не так часто. Отчасти это связано с мутными (как мне кажется) главами в стандарте языка, отчасти, потому что их реализация выливается в полный кошмар для программистов компиляторов. Мне не известно ни одного компилятора, который мог бы работать с этими «штуками» полностью по стандарту.

Заключение

Вот, наверно, и все что я хотел бы рассказать вам в дополнение. Конечно, часть вопросов, связанных с виртуальностью все еще требуют открытий, оставляю это на вашей совести 8)

Удачи в программировании.

P.S. перенести бы в CPP блог надо бы… думаю там оно будет более востребовано.

Наследование — это механизм создания нового класса на основе уже существующего. При этом к существующему классу могут быть добавлены новые элементы (данные и функции), либо существующие функции могут быть изменены. Основное назначение механизма наследования — повторное использование кодов, так как большинство используемых типов данных являются вариантами друг друга, и писать для каждого свой класс нецелесообразно.
Объекты разных классов и сами классы могут находиться в отношении наследования, при котором формируется иерархия объектов, соответствующая заранее предусмотренной иерархии классов.

Наследование

Иерархия классов позволяет определять новые классы на основе уже имеющихся. Имеющиеся классы обычно называют базовыми (иногда порождающими), а новые классы, формируемые на основе базовых, – производными (порожденными, классами-потомками или наследниками).


При наследовании некоторые имена методов (функций-членов) и полей (данных-членов) базового класса могут быть по-новому определены в производном классе. В этом случае соответствующие компоненты базового класса становятся недоступными из производного класса. Для доступа из производного класса к компонентам базового класса, имена которых повторно определены в производном, используется операция разрешения контекста ::

Для порождения нового класса на основе существующего используется следующая общая форма

При объявлении порождаемого класса МодификаторДоступа может принимать значения public , private , protected либо отсутствовать, по умолчанию используется значение private . В любом случае порожденный класс наследует все члены базового класса, но доступ имеет не ко всем. Ему доступны общие ( public ) члены базового класса и недоступны частные ( private ).

Для того, чтобы порожденный класс имел доступ к некоторым скрытым членам базового класса, в базовом классе их необходимо объявить со спецификацией доступа защищенные ( protected ).

Члены класса с доступом protected видимы в пределах класса и в любом классе, порожденном из этого класса.

Общее наследование

При общем наследовании порожденный класс имеет доступ к наследуемым членам базового класса с видимостью public и protected . Члены базового класса с видимостью private – недоступны.

Спецификация доступа внутри класса в порожденном классе вне класса
private +
protected + +
public + + +

Общее наследование означает, что порожденный класс – это подтип базового класса. Таким образом, порожденный класс представляет собой модификацию базового класса, которая наследует общие и защищенные члены базового класса.

Порожденный класс наследует все данные класса student (строка 13), имеет доступ к protected и public — членам базового класса. В новом классе добавлено два поля данных (строки 16, 17), и порожденный класс переопределяет функцию print() (строки 20, 39-43).

Конструктор для базового класса вызывается в списке инициализации (строка 29).

Но что происходит, когда мы присваиваем указателю класса student ссылку на объект класса grad_student (строка 55)? В этом случае происходит преобразование указателей, и в строке 56 вызывается уже функция print() класса student .


Указатель на порожденный класс может быть неявно передан в указатель на базовый класс. А указатель на порожденный класс может указывать только на объекты порожденного класса. То есть обратное преобразование недопустимо

Неявные преобразования между порожденным и базовым классами называются предопределенными стандартными преобразованиями :

  • объект порожденного класса неявно преобразуется к объекту базового класса.
  • ссылка на порожденный класс неявно преобразуется к ссылке на базовый класс.
  • указатель на порожденный класс неявно преобразуется к указателю на базовый класс.

Частное наследование

Порожденный класс может быть базовым для следующего порождения. При порождении private наследуемые члены базового класса, объявленные как protected и public , становятся членами порожденного класса с видимостью private . При этом члены базового класса с видимостью public и protected становятся недоступными для дальнейших порождений. Цель такого порождения — скрыть классы или элементы классов от использования их в дальнейших порождениях. При порождении private не выполняются предопределенные стандартные преобразования:

Однако порождение private позволяет отдельным элементам базового класса с видимостью public и protected сохранить свою видимость в порожденном классе. Для этого необходимо

  • в части protected порожденного класса указать те наследуемые члены базового класса с видимостью protected , уточненные именем базового класса, для которых необходимо оставить видимость protected и в порожденном классе;
  • в части public порожденного класса указать те наследуемые члены базового класса с видимостью public , уточненные именем базового класса, для которых необходимо оставить видимость public и в порожденном классе.

class X
private :
int n;
protected :
int m;
char s;
public :
void func( int );
>;
class Y : private X
private :
.
protected :
.
X::s;
public :
.
X::func();
>;

Возможен и третий вариант наследования – с использованием модификатора доступа protected .

Доступ к элементам базового класса из производного класса, в зависимости от модификатора наследования:

Конструкторы и деструкторы при наследовании

Как базовый, так и производный классы могут иметь конструкторы и деструкторы.

Если и у базового и у производного классов есть конструкторы и деструкторы, то конструкторы выполняются в порядке наследования, а деструкторы – в обратном порядке. То есть если А – базовый класс, В – производный из А , а С – производный из В ( А-В-С ), то при создании объекта класса С вызов конструкторов будет иметь следующий порядок:

  • конструктор класса А
  • конструктор класса В
  • конструктор класса С .

Вызов деструкторов при удалении этого объекта произойдет в обратном порядке:

  • деструктор класса С
  • деструктор класса В
  • деструктор класса А .

Поскольку базовый класс «не знает» о существовании производного класса, любая инициализация выполняется в нем независимо от производного класса, и, возможно, становится основой для инициализации, выполняемой в производном классе. Поскольку базовый класс лежит в основе производного, вызов деструктора базового класса раньше деструктора производного класса привел бы к преждевременному разрушению производного класса.

Конструкторы могут иметь параметры. При реализации наследования допускается передача параметров для конструкторов производного и базового класса. Если параметрами обладает только конструктор производного класса, то аргументы передаются обычным способом. При необходимости передать аргумент из производного класса конструктору родительского класса используется расширенная запись конструктора производного класса:

КонструкторПроизводногоКласса (СписокФормальныхАргументов)
: КонструкторБазовогоКласса (СписокФактическихАргументов)
< // тело конструктора производного класса >

Для базового и производного классов допустимо использование одних и тех же аргументов. Возможно, списки аргументов конструкторов производного и базового классов будут различны.

Конструктор производного класса не должен использовать все аргументы, часть предназначены для передачи в базовый класс (строка 29, см. код выше). В расширенной форме объявления конструктора производного класса описывается вызов конструктора базового класса.

Здравствуйте! а подскажите, пожалуйста, как я могу объявить неполный наследуемый класс? Например, у меня есть class a b class b, наследуемый откласса а. я хочу неполно объявить класс b перед классом а. как это синтаксически сделать? нигде не могу найти инфу об этом.

Если порожденный класс наследует элементы одного базового класса, то такое наследование называется одиночным . Однако, возможно и множественное наследование. Множественное наследование позволяет порожденному классу наследовать элементы более, чем от одного базового класса. Синтаксис заголовков классов расширяется так, чтобы разрешить создание списка базовых классов и обозначения их уровня доступа:

Множественное наследование

Класс А обобщенно наследует элементы всех трех основных классов.

Для доступа к членам порожденного класса, унаследованного от нескольких базовых классов, используются те же правила, что и при порождении из одного базового класса. Проблемы могут возникнуть в следующих случаях:

  • если в порожденном классе используется член с таким же именем, как в одном из базовых классов;
  • когда в нескольких базовых классах определены члены с одинаковыми именами.


В этих случаях необходимо использовать оператор разрешения контекста для уточнения элемента, к которому осуществляется доступ, именем базового класса.

Так как объект порожденного класса состоит из нескольких частей, унаследованных от базовых классов, то конструктор порожденного класса должен обеспечивать инициализацию всех наследуемых частей. В списке инициализации в заголовке конструктора порожденного класса должны быть перечислены конструкторы всех базовых классов. Порядок выполнения конструкторов при порождении из нескольких классов следующий:

  • конструкторы базовых классов в порядке их задания;
  • конструкторы членов, являющихся объектами класса;
  • конструктор порожденного класса.

Деструкторы вызываются в порядке обратном вызову конструкторов.

Виртуальные базовые классы

Иерархия наследования

Базовый класс может быть задан только один раз в списке порождения нового класса. Однако базовый класс может встретиться несколько раз в иерархии порождения.

Такая иерархия порождения несет двусмысленность при доступе к наследуемым членам класса X и может привести к ошибкам. В этом случае класс X будет дважды присутствовать в A . Хорошо это или плохо - зависит от решаемой задачи.

Если двойное вхождение объектов класса X в объект класса А не является допустимым, существует два выхода для разрешения такой ситуации:

    преобразование порождения из нескольких классов в порождение из одного класса и объявление дружественных классов.

Базовый класс определяется как виртуальный заданием ключевого слова virtual в списке порождения перед именем базового класса или указанием типа наследования


Если базовый класс объявляется как виртуальный при порождении нового класса, то каждый объект будет содержать свою собственную часть, а вместо базовой части будет содержать указатель на виртуальный базовый класс.

class A : public Y, public Z int key;
public :
A( int i = 0) : Y(i + 2), Z(i + 3)
key = Y::key + Z::key;
>
int getkey( void ) < return (key); >
>;
int main() A object(4);
cout cin.get();
return 0;
>

Конструкторы и деструкторы при использовании виртуальных базовых классов выполняются в следующем порядке:

  • конструкторы виртуальных базовых классов выполняются до конструкторов не виртуальных базовых классов, независимо от того, как эти классы заданы в списке порождения;
  • если класс имеет несколько виртуальных базовых классов, то конструкторы этих классов вызываются в порядке объявления виртуальных базовых классов в списке порождения;
  • деструкторы виртуальных базовых классов выполняются после деструкторов не виртуальных базовых классов.

При порождении с использованием виртуальных базовых классов сохраняются те же правила видимости, что и при порождении с не виртуальными классами.

Читайте также: