наследование
Весь смысл наследования заключается в том, чтобы совместно использовать общий интерфейс и протокол среди множества различных реализаций, так что экземпляр производного класса может обрабатываться идентично любому другому экземпляру из любого другого производного типа.
В C ++ наследование также приносит с собой детали реализации, помечая (или не помечая) деструктор как виртуальный - одна из таких деталей реализации.
Привязка функций
Теперь, когда вызывается функция или любой из ее особых случаев, таких как конструктор или деструктор, компилятор должен выбрать, какая реализация функции была предназначена. Затем он должен сгенерировать машинный код, который следует за этим намерением.
Самый простой способ для этого - выбрать функцию во время компиляции и выдать достаточно машинного кода, чтобы независимо от каких-либо значений, когда этот фрагмент кода выполнялся, он всегда выполнял код функции. Это прекрасно работает за исключением наследования.
Если у нас есть базовый класс с функцией (это может быть любая функция, включая конструктор или деструктор) и ваш код вызывает для него функцию, что это значит?
Исходя из вашего примера, если вы вызвали initialize_vector()
компилятор, он должен решить, действительно ли вы хотели вызвать реализацию, найденную в Base
, или реализацию, найденную в Derived
. Есть два способа решить это:
- Первый - решить, что, поскольку вы вызывали
Base
тип, вы имели в виду реализацию в Base
.
- Второй - решить, что, поскольку тип времени выполнения значения, хранящегося в
Base
типизированном значении, может быть Base
, или Derived
что решение о том, какой вызов сделать, должно быть принято во время выполнения при вызове (каждый раз, когда он вызывается).
Компилятор на этом этапе запутался, оба параметра одинаково действительны. Это когда virtual
входит в смесь. Когда это ключевое слово присутствует, компилятор выбирает вариант 2, откладывая решение между всеми возможными реализациями, пока код не будет запущен с реальным значением. Когда это ключевое слово отсутствует, компилятор выбирает вариант 1, потому что это нормальное поведение.
Компилятор может по-прежнему выбирать вариант 1 в случае вызова виртуальной функции. Но только если это может доказать, что это всегда так.
Конструкторы и деструкторы
Так почему бы нам не указать виртуальный конструктор?
Более интуитивно понятно, как компилятор будет выбирать между одинаковыми реализациями конструктора для Derived
и Derived2
? Это довольно просто, не может. Не существует заранее существующего значения, из которого компилятор может узнать, что на самом деле было задумано. Предварительно существующего значения не существует, потому что это работа конструктора.
Так зачем нам указывать виртуальный деструктор?
Более интуитивно понятно, как компилятор будет выбирать между реализациями для Base
и Derived
? Это просто вызовы функций, поэтому происходит поведение вызова функций. Без объявленного виртуального деструктора компилятор решит привязать его непосредственно к Base
деструктору независимо от значения типа времени выполнения.
Во многих компиляторах, если производный не объявляет никаких элементов данных и не наследует от других типов, поведение в ~Base()
будет подходящим, но это не гарантируется. Это сработало бы просто по случайности, как если бы вы стояли перед огнеметом, который еще не был зажжен. Вы в порядке на некоторое время.
Единственный правильный способ объявить любой базовый или интерфейсный тип в C ++ - это объявить виртуальный деструктор, чтобы правильный деструктор вызывался для любого данного экземпляра иерархии типов этого типа. Это позволяет функции с наибольшим знанием экземпляра правильно его очистить.
~derived()
делегату деструктору vec. В качестве альтернативы вы предполагаете,unique_ptr<base> pt
что знаете производный деструктор. Без виртуального метода этого не может быть. Хотя unique_ptr может быть предоставлена функция удаления, которая является параметром шаблона без какого-либо представления во время выполнения, и эта функция не используется для этого кода.