Деструкторы (C++)
Деструктор — это функция-член, которая вызывается автоматически, когда объект выходит из область или явно уничтожается вызовом delete . Деструктор имеет то же имя, что и класс, предшествующий тильде ( ~ ). Например, деструктор для класса String объявляется следующим образом: ~String() .
Если вы не определяете деструктор, компилятор предоставляет деструктор по умолчанию; для многих классов это достаточно. Необходимо определить только пользовательский деструктор, когда класс сохраняет обработку системных ресурсов, которые должны быть освобождены, или указатели, принадлежащие памяти, к которой они указывают.
Рассмотрим следующее объявление класса String :
// spec1_destructors.cpp #include class String < public: String( char *ch ); // Declare constructor ~String(); // and destructor. private: char *_text; size_t sizeOfText; >; // Define the constructor. String::String( char *ch ) < sizeOfText = strlen( ch ) + 1; // Dynamically allocate the correct amount of memory. _text = new char[ sizeOfText ]; // If the allocation succeeds, copy the initialization string. if( _text ) strcpy_s( _text, sizeOfText, ch ); >// Define the destructor. String::~String() < // Deallocate the memory that was previously reserved // for this string. delete[] _text; >int main()
В предыдущем примере деструктор String::~String использует delete оператор для освобождения места динамически выделенного для текстового хранилища.
Объявление деструкторов
Деструкторы — это функции с тем же именем, что и класс, но с добавленным в начало знаком тильды ( ~ ).
При объявлении деструкторов действуют несколько правил. Деструкторы:
- Не принимать аргументы.
- Не возвращайте значение (или void ).
- Невозможно объявить как const , volatile или static . Однако их можно вызвать для уничтожения объектов, объявленных как const , volatile или static .
- Можно объявить как virtual . С помощью виртуальных деструкторов можно уничтожить объекты, не зная их тип, — правильный деструктор для объекта вызывается с помощью механизма виртуальной функции. Деструкторы также можно объявить как чистые виртуальные функции для абстрактных классов.
Использование деструкторов
Деструкторы вызываются, когда происходит одно из следующих событий:
- Локальный (автоматический) объект с областью видимости блока выходит за пределы области видимости.
- Объект, выделенный оператором new , явно освобождается с помощью delete .
- Время существования временного объекта заканчивается.
- Программа заканчивается, глобальные или статические объекты продолжают существовать.
- Деструктор явно вызываться с использованием полного имени функции деструктора.
Деструкторы могут свободно вызывать функции-члена класса и осуществлять доступ к данным членов класса.
Существует два ограничения на использование деструкторов:
- Вы не можете взять свой адрес.
- Производные классы не наследуют деструктор базового класса.
Порядок уничтожения
Когда объект выходит за пределы области или удаляется, последовательность событий при его полном уничтожении выглядит следующим образом:
- Вызывается деструктор класса, и выполняется тело функции деструктора.
- Деструкторы для объектов нестатических членов вызываются в порядке, обратном порядку их появления в объявлении класса. Необязательный список инициализации элементов, используемый в строительстве этих элементов, не влияет на порядок строительства или уничтожения.
- Деструкторы для не виртуальных базовых классов вызываются в обратном порядке объявления.
- Деструкторы для виртуальных базовых классов вызываются в порядке, обратном порядку их объявления.
// order_of_destruction.cpp #include struct A1 < virtual ~A1() < printf("A1 dtor\n"); >>; struct A2 : A1 < virtual ~A2() < printf("A2 dtor\n"); >>; struct A3 : A2 < virtual ~A3() < printf("A3 dtor\n"); >>; struct B1 < ~B1() < printf("B1 dtor\n"); >>; struct B2 : B1 < ~B2() < printf("B2 dtor\n"); >>; struct B3 : B2 < ~B3() < printf("B3 dtor\n"); >>; int main() < A1 * a = new A3; delete a; printf("\n"); B1 * b = new B3; delete b; printf("\n"); B3 * b2 = new B3; delete b2; >Output: A3 dtor A2 dtor A1 dtor B1 dtor B3 dtor B2 dtor B1 dtor
Виртуальные базовые классы
Деструкторы для виртуальных базовых классов вызываются в порядке, обратном их указанию в направленном ациклическом графе (в глубину, слева направо, обход в обратном порядке). На следующем рисунке представлен граф наследования.
Пять классов, помеченные A до E, упорядочены в графе наследования. Класс E является базовым классом B, C и D. КлассЫ C и D являются базовым классом A и B.
Ниже перечислены заголовки классов, представленных на рисунке.
class A class B class C : virtual public A, virtual public B class D : virtual public A, virtual public B class E : public C, public D, virtual public B
Чтобы определить порядок удаления виртуальных базовых классов объекта типа E , компилятор выполняет сборку списка, применяя следующий алгоритм.
- Просмотрите левую часть графа, начиная с самой глубокой точки графа (в данном случае E ).
- Просматривайте граф справа налево, пока не будут пройдены все узлы. Запомните имя текущего узла.
- Пересмотрите предыдущий узел (вниз и вправо), чтобы определить, является ли рассматриваемый узел виртуальным базовым классом.
- Если рассматриваемый узел является виртуальным базовым классом, просмотрите список, чтобы проверить, был ли он введен ранее. Если он не является виртуальным базовым классом, игнорируйте его.
- Если запоминаемый узел еще не находится в списке, добавьте его в нижней части списка.
- Просмотрите граф вверх и вдоль следующего пути вправо.
- Перейдите к шагу 2.
- Если путь последний путь вверх исчерпан, запомните имя текущего узла.
- Перейдите к шагу 3.
- Выполняйте этот процесс, пока нижний узел снова не станет текущим узлом.
Таким образом, для класса E порядок удаления будет следующим.
- Не-виртуальный базовый класс E .
- Не-виртуальный базовый класс D .
- Не-виртуальный базовый класс C .
- Виртуальный базовый класс B .
- Виртуальный базовый класс A .
В ходе этого процесса создается упорядоченный список уникальных записей. Имя класса никогда не отображается дважды. После создания списка он выполняется в обратном порядке, а деструктор для каждого класса в списке от последнего до первого вызывается.
Порядок построения или уничтожения в первую очередь важен, если конструкторы или деструкторы в одном классе полагаются на другой компонент, созданный первым или сохраняющимся дольше, например, если деструктор для A (на рисунке, показанном ранее) опирался B на все еще присутствует при выполнении кода или наоборот.
Такие взаимозависимости между классами в графе наследования опасны, поскольку классы, наследуемые впоследствии, могут изменить крайний левый путь, тем самым изменив порядок построения и удаления.
Не виртуальные базовые классы
Деструкторы для не виртуальных базовых классов вызываются в обратном порядке, в котором объявляются имена базовых классов. Рассмотрим следующее объявление класса.
class MultInherit : public Base1, public Base2 .
В предыдущем примере деструктор Base2 вызывается перед деструктором Base1 .
Явные вызовы деструктора
Редко возникает необходимость в явном вызове деструктора. Однако может быть полезно выполнить удаление объектов, размещенных по абсолютным адресам. Эти объекты обычно выделяются с помощью определяемого пользователем new оператора, который принимает аргумент размещения. Оператор delete не может освободить эту память, так как он не выделяется из свободного хранилища (дополнительные сведения см. в разделе «Новые и удаленные операторы»). Вызов деструктора, однако, может выполнить соответствующую очистку. Для явного вызова деструктора для объекта ( s ) класса String воспользуйтесь одним из следующих операторов.
s.String::~String(); // non-virtual call ps->String::~String(); // non-virtual call s.~String(); // Virtual call ps->~String(); // Virtual call
Нотация для явных вызовов деструкторов, показанная в предыдущем примере, может использоваться независимо от того, определяет ли тип деструктор. Это позволяет выполнять такие явные вызовы, не зная, определен ли деструктор для типа. Явный вызов деструктора, если ни один из них не определен, не имеет никакого эффекта.
Отказоустойчивость
Классу требуется деструктор, если он получает ресурс и безопасно управлять ресурсом, который, вероятно, должен реализовать конструктор копирования и назначение копирования.
Если эти специальные функции не определены пользователем, они неявно определяются компилятором. Неявно созданные конструкторы и операторы назначения выполняют неглубокое копирование элементов, что почти наверняка неправильно, если объект управляет ресурсом.
В следующем примере неявно созданный конструктор копирования сделает указатели str1.text и str2.text ссылается на ту же память, и при возврате из copy_strings() нее память будет удалена дважды, что является неопределенным поведением:
void copy_strings() < String str1("I have a sense of impending disaster. "); String str2 = str1; // str1.text and str2.text now refer to the same object >// delete[] _text; deallocates the same memory twice // undefined behavior
Явное определение деструктора, конструктора копирования или оператора назначения копирования предотвращает неявное определение конструктора перемещения и оператора назначения перемещения. В этом случае обычно не удается предоставить операции перемещения, если копирование является дорогостоящим, отсутствует возможность оптимизации.
Как вызвать деструктор
Деструктор выполняет освобождение использованных объектом ресурсов и удаление нестатических переменных объекта. Деструктор автоматически вызывается, когда удаляется объект. Удаление объекта происходит в следующих случаях:
- когда завершается выполнение области видимости, внутри которой определены объекты
- когда удаляется контейнер (например, массив), который содержит объекты
- когда удаляется объект, в котором определены переменные, представляющие другие объекты
- динамически созданные объекты удаляются при применении к указателю на объект оператора delete
По сути деструктор — это функция, которая называется по имени класса (как и конструктор) и перед которой стоит тильда (~):
~имя_класса() < // код деструктора >
Деструктор не имеет возвращаемого значения и не принимает параметров. Каждый класс может иметь только один деструктор.
Обычно деструктор не так часто требуется и в основном используется для освобождения связанных ресурсов. Например, объект класса использует некоторый файл, и в деструкторе можно определить код закрытия файла. Или если в классе выделяется память с помощью оператора new , то в деструкторе можно освободить подобную память.
Сначала рассмотрим простейшее определение деструктора:
#include class Person < public: Person(std::string p_name) < name = p_name; std::cout ~Person() < std::cout private: std::string name; >; int main() < < Person tom; Person bob; > // объекты Tom и Bob уничтожаются Person sam; > // объект Sam уничтожается
В классе Person определен деструктор, который просто уведомляет об уничтожении объекта:
~Person()
В функции main создаются три объекта Person. Причем два из них создается во вложенном блоке кода.:
< Person tom; Person bob; >
Этот блок кода задается границы области видимости, в которой существуют эти объекты. И когда выполнение блока завершается, то уничтожаются обе переменных, и для обоих объектов вызываются деструкторы.
После этого создается третий объект — sam
int main() < < Person tom; Person bob; > // объекты Tom и Bob уничтожаются Person sam; > // объект Sam уничтожается
Поскольку он определен в контексте функции main, то и уничтожается при завершении этой функции. В итоге мы получим следующий консольный вывод:
Person Tom created Person Bob created Person Bob deleted Person Tom deleted Person Sam created Person Sam deleted
Чуть более практический пример. Допустим, у нас есть счетчик объектов Person в виде статической переменной. И если в конструкторе при создании каждого нового объекта счетчик увеличивается, то в деструкторе мы можем уменьшать счетчик:
#include class Person < public: Person(std::string p_name) < name = p_name; ++count; std::cout ~Person() < --count; std::cout private: std::string name; static inline unsigned count<>; // счетчик объектов >; int main() < < Person tom; Person bob; > // объекты Tom и Bob уничтожаются Person sam; > // объект Sam уничтожается
Консольный вывод программы:
Person Tom created. Count: 1 Person Bob created. Count: 2 Person Bob deleted. Count: 1 Person Tom deleted. Count: 0 Person Sam created. Count: 1 Person Sam deleted. Count: 0
При этом выполнение самого деструктора еще не удаляет сам объект. Непосредственно удаление объекта производится в ходе явной фазы удаления, которая следует после выполнения деструктора.
Стоит также отметить, что для любого класса, который не определяет собственный деструктор, компилятор сам создает деструктор. Например, если бы класс Person не имел бы явно определенного деструктора, то для него автоматически создавался бы следующий деструктор:
Как вызвать деструктор
Насколько след. код соответствует стандарту, и что должно на самом деле попасть в stdout?
#include class A < public: A() < std::cout "A()" ~A() < std::cout "~A()" void foo() < std::cout "foo()" >; int main(int argc, char *argv[]) < A a; a.~A(); a.foo(); return 0; >
И cl, и gcc, и Comeau код компилируют без проблем, однако вывод, например, у gcc отличается от cl:
Re: Явный вызов деструктора
От: | Ops | |
Дата: | 04.05.12 18:02 | |
Оценка: | 4 (1) |
Здравствуйте, PlusMyTwitterFace, Вы писали:
PMT>Насколько след. код соответствует стандарту, и что должно на самом деле попасть в stdout?
Once a destructor is invoked for an object, the object no longer exists; the behavior is undefined if the
destructor is invoked for an object whose lifetime has ended (3.8). [Example: if the destructor for an
automatic object is explicitly invoked, and the block is subsequently left in a manner that would ordinarily
invoke implicit destruction of the object, the behavior is undefined. —end example ]
Переубедить Вас, к сожалению, мне не удастся, поэтому сразу перейду к оскорблениям.
Re[2]: Явный вызов деструктора
От: | PlusMyTwitterFace |
Дата: | 04.05.12 18:18 |
Оценка: |
Т.е. явный вызов деструктора, по сути, можно без проблем использовать лишь для объектов, которые были размещены при помощи placement new?
Once a destructor is invoked for an object, the object no longer exists
Получается, что и обращение к любым полям класса после явного вызова деструктора — тоже UB?
Да, и ещё, вот в таком коде стандартом гарантируется, что временный объект будет уничтожен до 2?
// 1 A().foo(); // 2
Re[3]: Явный вызов деструктора
От: | Ops |
Дата: | 04.05.12 19:08 |
Оценка: |
Здравствуйте, PlusMyTwitterFace, Вы писали:
PMT>Т.е. явный вызов деструктора, по сути, можно без проблем использовать лишь для объектов, которые были размещены при помощи placement new?
По сути, выходит что так. Использовать по другому можно, если абсолютно точно знаешь, что делаешь, и уверен, что твой код не придется поддерживать маньяку-убийце.
PMT>
PMT>Once a destructor is invoked for an object, the object no longer exists
PMT>Получается, что и обращение к любым полям класса после явного вызова деструктора — тоже UB?
Вот этого явно не нашел, однако:
The properties ascribed to objects throughout this International Standard apply for a given object only
during its lifetime.
3.8.3
Там есть небольшие ремарки по поводу времени создания и уничтожения объектов, но они в этом контексте несущественные.
PMT>Да, и ещё, вот в таком коде стандартом гарантируется, что временный объект будет уничтожен до 2?
PMT>
PMT>// 1 PMT>A().foo(); PMT>// 2 PMT>
Здесь — да. Время жизни временного объекта продлевает только ссылка на него, с некоторыми исключениями, причем даже если в foo будет создана ссылка на этот объект, то объект проживет только до «;», это как раз одно из исключений.
Переубедить Вас, к сожалению, мне не удастся, поэтому сразу перейду к оскорблениям.
Re: Явный вызов деструктора
От: | Caracrist | https://1pwd.org/ |
Дата: | 04.05.12 20:06 | |
Оценка: |
Здравствуйте, PlusMyTwitterFace, Вы писали:
Два вопроса по cl
1. если написать это не в main ведёт себя так же?
2. что получается если сделать так:
A a; a.~A(); new (&a) A(); a.foo();
Тут всё логично и соответствует ожиданию
Re[2]: Явный вызов деструктора
От: | PlusMyTwitterFace |
Дата: | 04.05.12 20:20 |
Оценка: |
C>1. если написать это не в main ведёт себя так же?
Нет, ведёт себя, как gcc. А с чем это связано?
C>2. что получается если сделать так:
C>
C> A a; C> a.~A(); C> new (&a) A(); C> a.foo(); C>
C>Тут всё логично и соответствует ожиданию
По стандарту (цитаты ранее) получается, что любой явный вызов деструктора объекта класса, кроме случая с placement new, — UB в чистом виде, разве тут что-то вообще может быть «логично»?
Re[3]: Явный вызов деструктора
От: | Caracrist | https://1pwd.org/ |
Дата: | 04.05.12 20:41 | |
Оценка: |
Здравствуйте, PlusMyTwitterFace, Вы писали:
PMT>По стандарту (цитаты ранее) получается, что любой явный вызов деструктора объекта класса, кроме случая с placement new, — UB в чистом виде, разве тут что-то вообще может быть «логично»?
По стандарту.
А по сути, есть память в некотором состоянии судя по всему не валидном, однако вызов foo() это не виртуальный thiscall, там где ты это компилируешь этот вызов запишет указатель на обект в регистр и больше к нему не обратится (оптимизация скорее всего весь вызов уберёт и заинлайнит эту функцию).
Очень сложно придумать имплементацию, которая будет вести себя по другому.
Далее, вызов деструктора уже посложнее. Компилятор имеет право помечать объекты для которых вызван деструктор, например для того чтобы справиться со сложными случаями раскрутки(исключений в процессе раскрутки, к примеру). Тут не совсем тот случай, но поскольку существует переопределение операторов new/delete на уровне класса, то компилятор имеет право сохранять эту информацию на объекте и обращаться к ней при вызове delete, мало ли как там вызов деструктора с этой информацией общаются(а она у нас уже не валидна).
Также — выход из main это выход из программы, это не по стандарту, но компилятор может по другому процессить такой выход.
Вот по этому, нарушив стандарт различие мы видим только в вызове деструктора на выходе из функции.
А то, что я попросил проверить, мне это просто интересно
Re[3]: Явный вызов деструктора
От: | Masterkent |
Дата: | 04.05.12 21:40 |
Оценка: |
PlusMyTwitterFace:
PMT>Т.е. явный вызов деструктора, по сути, можно без проблем использовать лишь для объектов, которые были размещены при помощи placement new?
Необязательно. Объект классового типа с тривиальным конструктором по умолчанию можно создать просто выделив достаточный участок памяти с учётом выравнивания. Например,
#include struct X < int n; >; int main() < X *p = (X *)operator new(sizeof(X)); p->n = 123; std::cout n operator delete(p); >
Тут можно было бы вставить
после определения p, но в этом нет необходимости. Тривиальный деструктор тоже можно не звать. А вот насчёт того, можно ли вызывать тривиальный деструктор два раза подряд, в комитете по стандартизации консенсуса пока нет
PMT>Получается, что и обращение к любым полям класса после явного вызова деструктора — тоже UB?
После вызова нетривиального деструктора обращение к нестатическим членам влечёт undefined behavior — см. 3.8/1 и 3.8/5/2.
Re: Явный вызов деструктора
От: | PlusMyTwitterFace |
Дата: | 05.05.12 09:28 |
Оценка: |
А вызов деструктора из функции-члена класса при помощи this?
#include class A < public: A() < std::cout "A()" ~A() < std::cout "~A()" void foo() < std::cout "foo()" this->~A(); > >; int main(int argc, char *argv[]) < A a; a.foo(); return 0; >
Вызов деструктора в конструкторе
Кошеrно ли вызывать деструктор объекта из его же конструктора? Если нет, то как нужно поступить, если нужно создать объект, заставить его выполнить действия и по окончанию их убиться?
MyClass myObject = MyClass(someParam); // Тут объект уже должен быть мертвым, выполнив свои действия
inn
22.07.13 21:25:17 MSK
Забабахай статический метод, будь мужиком.
cdshines ★★★★★
( 22.07.13 21:26:20 MSK )
Ответ на: комментарий от cdshines 22.07.13 21:26:20 MSK
Эээ.. Сделать статическим деструктор или все остальные методы? Просто в конструкторе вызывается ряд методов, которые готовят данные. На любом из этапов может быть вызван exit(0) (ну или исключение, сейчас кидается фуллстоп для простоты). Если все «подготовительные этапы» прошли успешно, выполняется действие и объект должен самоуничтожиться.
inn
( 22.07.13 21:32:50 MSK ) автор топика
MyClass(someParam);
Begemoth ★★★★★
( 22.07.13 21:35:46 MSK )
Воспользуйся функцией вместо объекта.
anonymous
( 22.07.13 21:36:19 MSK )
void Foo() < < MyClass myObject = MyClass(someParam); >// some code >
Amp ★★★
( 22.07.13 21:36:55 MSK )
Ответ на: комментарий от inn 22.07.13 21:32:50 MSK
Если время жизни экземпляра не превышает время жизни его конструктора, задумайся над персмотром архитектуры. Если суть методов объекта сводится к выполнению какой-то работы один раз при конструировании, сделай их статическими и вызывай из конструктора (хотя это, опять жеЮ ниочинь подход, но я видел такие случаи — клас — просто обертка для набора функций). Тебе же объект не нужен по сути.
cdshines ★★★★★
( 22.07.13 21:38:17 MSK )
Последнее исправление: cdshines 22.07.13 21:39:35 MSK (всего исправлений: 1)
Ответ на: комментарий от anonymous 22.07.13 21:36:19 MSK
В комплекте идут хэш-данные, списки и рекурсивное создание таких же объектов внутри него. Так что просто функцией тут не обойтись.
inn
( 22.07.13 21:38:44 MSK ) автор топика
Ответ на: комментарий от inn 22.07.13 21:38:44 MSK
Если нужен доступ к внутренностям этого класса, то эту функцию можно зафрендить с этим классом.
anonymous
( 22.07.13 21:42:42 MSK )
Ответ на: комментарий от cdshines 22.07.13 21:38:17 MSK
Хорошо. О пересмотре архитертуры. Вот сейчас оно примерно так:
// xxx.h #include . class Xxx : public QObject
// xxx.h #include . Xxx::Xxx(QString FileName) < Func1(. ); // May be killed here Func2 . // Or here . // Or even here Func5 // But the most probably here run(void); >Xxx::run(void) < // cycle for commands // and if we've find the command we didn't load yet - load it: child = new Xxx("Path/To/Command/File/And/Its/File/Name.xxx"); // May be recursive for more than N levels. >
inn
( 22.07.13 21:50:31 MSK ) автор топика
Ответ на: комментарий от inn 22.07.13 21:50:31 MSK
Ты не показал не одной причины использовать объект вместо функции.
anonymous
( 22.07.13 22:11:05 MSK )
Ответ на: комментарий от anonymous 22.07.13 22:11:05 MSK
То, что для его работы потребуется
private: QList * commands;
А для каждого экземпляра объекта они свои. поэтому сделать глобальными — не айс.
inn
( 22.07.13 22:13:12 MSK ) автор топика
Ответ на: комментарий от inn 22.07.13 22:13:12 MSK
На стеке этот список определи в рекурсивной функции. Еще причины есть?
anonymous
( 22.07.13 22:19:20 MSK )
Ответ на: комментарий от anonymous 22.07.13 22:19:20 MSK
1) Стек не лопнет, если я её рекурсивно overдофига раз вызову?
2) В C++ НЕТ вложенных функций (это костыль), поэтому одиночную функцию придется сопровождать еще добрым десятком того, что сейчас в private.
inn
( 22.07.13 22:27:11 MSK ) автор топика
Ответ на: комментарий от inn 22.07.13 22:27:11 MSK
Стек не лопнет, потому что данные все равно будут в куче. Функции положи в файл реализации и сделай их static.
anonymous
( 22.07.13 22:30:55 MSK )
Ответ на: комментарий от anonymous 22.07.13 22:30:55 MSK
А суровые бородатые дяди в свитерах не будут тыкать меня вилами с надписью ООП за такую реализацию?
inn
( 22.07.13 22:34:23 MSK ) автор топика
Ответ на: комментарий от inn 22.07.13 22:34:23 MSK
Но если кто-то откроет рот, то сразу засунь их в какой нибудь namespace. 🙂
anonymous
( 22.07.13 22:37:00 MSK )
Ответ на: комментарий от inn 22.07.13 21:32:50 MSK
обьясните мне плз что здесь имеется в виду?