Следующий код вызывает утечку памяти:
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class base
{
void virtual initialize_vector() = 0;
};
class derived : public base
{
private:
vector<int> vec;
public:
derived()
{
initialize_vector();
}
void initialize_vector()
{
for (int i = 0; i < 1000000; i++)
{
vec.push_back(i);
}
}
};
int main()
{
for (int i = 0; i < 100000; i++)
{
unique_ptr<base> pt = make_unique<derived>();
}
}
Для меня это не имело особого смысла, так как производный класс не выделяет необработанную динамическую память, а unique_ptr освобождает себя. Я понимаю, что неявный деструктор базы классов вызывается вместо производного, но я не понимаю, почему это проблема. Если бы я написал явный деструктор для производного, я бы ничего не написал для vec.
c++
inheritance
memory
allocation
Инерционное невежество
источник
источник
~derived()
делегату деструктору vec. В качестве альтернативы вы предполагаете,unique_ptr<base> pt
что знаете производный деструктор. Без виртуального метода этого не может быть. Хотя unique_ptr может быть предоставлена функция удаления, которая является параметром шаблона без какого-либо представления во время выполнения, и эта функция не используется для этого кода.Ответы:
Когда компилятор выполняет неявный
delete _ptr;
внутриunique_ptr
деструктора (где_ptr
хранится указательunique_ptr
), он точно знает две вещи:_ptr
есть. Поскольку указатель находится внутриunique_ptr<base>
, это означает, что_ptr
имеет типbase*
.Это все, что знает компилятор. Таким образом, учитывая, что он удаляет объект типа
base
, он будет вызываться~base()
.Итак ... где та часть, где он уничтожает
dervied
объект, на который он фактически указывает? Потому что, если компилятор не знает, что он уничтожаетderived
, тогда он вообще не знает,derived::vec
существует , не говоря уже о том, что он должен быть уничтожен. Итак, вы сломали объект, оставив половину его неуничтоженным.Компилятор не может предположить, что любое
base*
уничтожаемое на самом деле является aderived*
; в конце концов, может быть любое количество классов, полученных изbase
. Как бы он узнал, на какой тип конкретноbase*
указывает этот тип ?Компилятор должен
derived
найти правильный деструктор для вызова (да, у него есть деструктор. Если вы не= delete
деструктор, у каждого класса есть деструктор, независимо от того, пишете вы его или нет). Чтобы сделать это, он должен будет использовать некоторую информацию, хранящуюся в нем,base
чтобы получить правильный адрес вызываемого кода деструктора, информацию, которая устанавливается конструктором фактического класса. Затем он должен использовать эту информацию для преобразования вbase*
указатель на адрес соответствующегоderived
класса (который может быть или не быть по другому адресу. Да, действительно). И тогда он может вызвать этот деструктор.Этот механизм я только что описал? Обычно это называется «виртуальная диспетчеризация»: то есть то, что происходит каждый раз, когда вы вызываете функцию, помеченную,
virtual
когда у вас есть указатель / ссылка на базовый класс.Если вы хотите вызвать функцию производного класса, когда у вас есть только указатель / ссылка базового класса, эта функция должна быть объявлена
virtual
. Деструкторы принципиально не отличаются в этом отношении.источник
наследование
Весь смысл наследования заключается в том, чтобы совместно использовать общий интерфейс и протокол среди множества различных реализаций, так что экземпляр производного класса может обрабатываться идентично любому другому экземпляру из любого другого производного типа.
В C ++ наследование также приносит с собой детали реализации, помечая (или не помечая) деструктор как виртуальный - одна из таких деталей реализации.
Привязка функций
Теперь, когда вызывается функция или любой из ее особых случаев, таких как конструктор или деструктор, компилятор должен выбрать, какая реализация функции была предназначена. Затем он должен сгенерировать машинный код, который следует за этим намерением.
Самый простой способ для этого - выбрать функцию во время компиляции и выдать достаточно машинного кода, чтобы независимо от каких-либо значений, когда этот фрагмент кода выполнялся, он всегда выполнял код функции. Это прекрасно работает за исключением наследования.
Если у нас есть базовый класс с функцией (это может быть любая функция, включая конструктор или деструктор) и ваш код вызывает для него функцию, что это значит?
Исходя из вашего примера, если вы вызвали
initialize_vector()
компилятор, он должен решить, действительно ли вы хотели вызвать реализацию, найденную вBase
, или реализацию, найденную вDerived
. Есть два способа решить это:Base
тип, вы имели в виду реализацию вBase
.Base
типизированном значении, может бытьBase
, илиDerived
что решение о том, какой вызов сделать, должно быть принято во время выполнения при вызове (каждый раз, когда он вызывается).Компилятор на этом этапе запутался, оба параметра одинаково действительны. Это когда
virtual
входит в смесь. Когда это ключевое слово присутствует, компилятор выбирает вариант 2, откладывая решение между всеми возможными реализациями, пока код не будет запущен с реальным значением. Когда это ключевое слово отсутствует, компилятор выбирает вариант 1, потому что это нормальное поведение.Компилятор может по-прежнему выбирать вариант 1 в случае вызова виртуальной функции. Но только если это может доказать, что это всегда так.
Конструкторы и деструкторы
Так почему бы нам не указать виртуальный конструктор?
Более интуитивно понятно, как компилятор будет выбирать между одинаковыми реализациями конструктора для
Derived
иDerived2
? Это довольно просто, не может. Не существует заранее существующего значения, из которого компилятор может узнать, что на самом деле было задумано. Предварительно существующего значения не существует, потому что это работа конструктора.Так зачем нам указывать виртуальный деструктор?
Более интуитивно понятно, как компилятор будет выбирать между реализациями для
Base
иDerived
? Это просто вызовы функций, поэтому происходит поведение вызова функций. Без объявленного виртуального деструктора компилятор решит привязать его непосредственно кBase
деструктору независимо от значения типа времени выполнения.Во многих компиляторах, если производный не объявляет никаких элементов данных и не наследует от других типов, поведение в
~Base()
будет подходящим, но это не гарантируется. Это сработало бы просто по случайности, как если бы вы стояли перед огнеметом, который еще не был зажжен. Вы в порядке на некоторое время.Единственный правильный способ объявить любой базовый или интерфейсный тип в C ++ - это объявить виртуальный деструктор, чтобы правильный деструктор вызывался для любого данного экземпляра иерархии типов этого типа. Это позволяет функции с наибольшим знанием экземпляра правильно его очистить.
источник