Страницы

Поиск по вопросам

вторник, 2 октября 2018 г.

Как работает деструктор

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


Ответ

Формально деструктор не имеет никакого отношения к освобождению памяти "своего" объекта. Освобождение памяти делается внешним кодом, который и вызывает деструктор перед освобождением. Поэтому с этой точки зрения ваш вопрос несколько бессмыслен. Деструктор - специальная функция. Даже если он имеет пустое тело, совсем не означает, что "ничего не делает". Например, деструктор всегда неявно вызывает деструкторы подобъектов данного класса, даже если у него внешне "пустое" тело.
Понятно, что деструктор может быть ответственен за явное освобождение посторонних ресурсов, которыми напрямую владеет ваш объект (в том числе память). Но в этом случае и тело деструктора не будет пустым. В "традиционных" реализациях, как ни странно, когда деструктор виртуален, функция освобождения динамической памяти объекта зачастую неявно вызывается изнутри деструктора (а не наружным кодом). Такой трюк необходим для того, чтобы обеспечить правильное поведение перегруженного operator delete для класса (если таковой имеется). Но это уже внутренние детали реализации.

Третий пункт ссылается на следующую ситуацию.
В языке С++ абстрактный алгоритм работы оператора delete сводится к последовательности из двух шагов:
Вызов правильного деструктора объекта Вызов правильной функции освобождения "сырой" памяти operator delete(void *)
Функция operator delete(void *), как известно, может замещаться/перегружаться пользователем. Разрешается как замещать глобальный ::operator delete(void *), так и перегружать статическую функцию operator delete(void *) в конкретных классах. При этом спецификация языка требует, чтобы выбор конкретного operator delete(void *) делался так, как будто его поиск (name lookup) делался из деструктора удаляемого объекта.
Например
#include
struct B { virtual ~B() { std::cout << "B::~B" << std::endl; }
void operator delete(void *p) { std::cout << "B::operator delete" << std::endl; ::operator delete(p); } };
struct D : B { virtual ~D() { std::cout << "D::~D" << std::endl; }
void operator delete(void *p) { std::cout << "D::operator delete" << std::endl; ::operator delete(p); } };
int main() { B* pb = new B; B* pd = new D; delete pb; delete pd; }
в таком коде при выполнении delete pb после выполнения деструктора B::~B должен вызваться B::operator delete, а при выполнении delete pd после выполнения деструктора D::~D должен вызваться D::operator delete. Другими словами, несмотря на то, что функция operator delete всегда является статическим членом класса, она должна вести себя фактически как виртуальная (!) функция.
Для того, чтобы удовлетворить этому требованию языка, большинство реализаций просто-напросто переносят вызов правильного operator delete внутрь деструктора. Таким образом требуемая "виртуальность" функции operator delete достигается бесплатно, за счет виртуальности деструктора.
При этом понятно, что operator delete(void *) нужно вызвать только для полных объектов, размещенных в динамической памяти, а для остальных объектов - не нужно (т.е. нельзя). Чтобы принять это во внимание, компиляторы снабжают деструктор неявным булевским параметром, говорящим деструктору, надо ли вызывать operator delete. Таким образом в вышеприведенном примере деструкторы на самом деле будут иметь следующий вид
B::~B(bool call_delete) // неявный параметр { std::cout << "B::~B" << std::endl;
if (call_delete) // неявно B::operator delete(this); // неявно }
~D::D(bool call_delete) // неявный параметр { std::cout << "D::~D" << std::endl;
B::~B(false); // неявно
if (call_delete) // неявно D::operator delete(this); // неявно }
Выражения delete pb и delete pd в такой ситуации превращаются просто в виртуальные вызовы pb->~B(true) и pd->~B(true). Первое попадает в B::~B, второе - в D::~D.
Компилятор GCC, кстати, в более ранних версиях реализовывал этот подход именно так, как описано выше - через скрытый булевский параметр, а в современных версиях этот же поход он реализует через генерацию двух отдельных деструкторов.

Комментариев нет:

Отправить комментарий