Форум программистов, компьютерный форум, киберфорум
Evg
Войти
Регистрация
Восстановить пароль
Карта форума Блоги Сообщество Поиск Заказать работу  
Рейтинг: 3.67. Голосов: 6.

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

Запись от Evg размещена 15.02.2012 в 23:15
Обновил(-а) Evg 08.07.2015 в 21:16
Метки c++

ВНИМАНИЕ! Вопросы по существу обсуждаемого вопроса просьба задавать здесь или создать тему на форуме и кинуть на неё ссылку в блог или мне в личку.
Объясняю почему

Причин для этого несколько.

Я, как и любой другой автор, всегда могу упустить интересный момент обсуждаемой темы (что подтвердилось на практике). А потому задаваемый вопрос может закрывать пробел в статье. Ответ на конкретный вопрос, как правило, дать несложно. Сложнее его аккуратно сформулировать так, чтобы ответ являлся законченной частью статьи. Поэтому, как правило, на первых порах я ограничиваюсь конкретным ответом на конкретный вопрос, а в статью временно вставляю ссылку на пост, где был дан ответ. А когда дойдут руки, то вместо ссылки пишу нормальное пояснение. Технические возможности блога не позволяют в комментариях пользоваться широкими возможностями, доступными на форуме (то как выделение текста жирным, вставка фрагментов исходников в удобном для чтения виде и т.п.), поэтому будет удобнее, если вопрос и ответ будут опубликованы на форуме

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

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

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




  • 1. Предисловие
  • 2. Терминология и классификация
  • 3. Время жизни объекта и память, выделяемая под объект
  • 4. Конструкторы и деструкторы
    4.1. Автоматический объект
    4.2. Статический объект
    4.3. Динамический объект
  • 5. Неочевидные моменты при работе с конструкторами и деструкторами в Си++
    5.1. Конструкторы и деструкторы не являются процедурами
    5.2. Вызов деструкторов при исполнении return посередине процедуры
    5.3. Вызов деструкторов при исключительных ситуациях
    5.4. Вызов деструкторов при принудительном завершении потока
    5.5. Классы, содержащие указатели
    5.6. Отличия между new/delete и malloc/free
    5.7. Отличия между конструированием объекта и присваиванием
    5.8. Некоторые тонкости при наличии печати в стандартный вывод внутри деструктора
    5.9. Исключительные ситуации в процессе работы конструкторов и деструкторов
  • 6. Заключение
  • 7. Ссылки на темы, где обсуждался данный вопрос
  • 8. Внешние ссылки по данной тематике







1. Предисловие

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

Занудство сосредоточено в разделах 2 и 3. По большому счёту эти разделы читать необязательно - только посмотреть определения в конце этих разделов. В разделе 4 всё пояснено на примерах-аналогиях, по идее этого материала для понимания достаточно. Однако разделы 2 и 3 могут оказаться полезными для того, чтобы привести собственные знания в порядок и разложить по полочкам

Изначально я планировал ограничить статью лишь разделением выделения памяти и инициализации объекта. Однако в процессе написания вспомнились некоторые тонкие моменты по работе с конструкторами и деструкторами в Си++. Моменты вспомнились и тем, кто читал статью. Все эти тонкости выходят за изначально предполагаемые рамки данной статьи, но тем не менее решил написать о них и собрал их в разделе 5 - не пропадать же добру







2. Терминология и классификация

Большинство программистов привыкло к той терминологии, на которой построено большинство книг и учебников. Это так называемая "бытовая" терминология. Однако разработчики компиляторов пользуются немного другой терминологией. И мне хотелось бы в описании воспользоваться именно этой терминологией, т.к. она более точно описывает понятия. Ну и если кто будет читать техническую литературу на околокомпиляторные темы, он скорее всего увидит ту же самую терминологию. Поэтому на всякий случай для всех терминов напишу варианты на английском языке

Вместо слова "переменная" (variable) будем пользоваться словом "объект" (object). Объект - это сущность, которая описывает аппаратный ресурс - регистр или память. Внимание! Не путать с понятием "объект Си++ как экземпляр класса". Поскольку для пояснений нет принципиальной разницы между тем, будет объект распределён на регистр или в памяти, в дальнейшем я не буду использовать фразу "аппаратный ресурс", а буду просто говорить "память". Итак, объект - это сущность, которая описывает некоторый участок памяти в момент исполнения программы. Память может быть выделена динамически в процессе работы программы, а потому эту память описывать словом "переменная" было бы некорректным. Именно поэтому мы будем пользоваться термином "объект". Другими словами, объект - это переменная языка или динамически выделенный экземпляр (класса).

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

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

Переменная, описаная внутри функции (например, внутри функции func1), называется "локальная" (local variable). Когда говорят о локальности, то обычно в первую очередь подразумевают локальную область видимости. Т.е. к этой переменной по имени можно обращаться только из той функции или лексического блока, в котором переменная определена. Однако к значению переменной мы можем обратиться из другой функции. Например, если мы в функции func1 возьмём адрес на переменную и передадим его в другую функцию (например, func2), то из функции func2 мы сможем через указатель обратиться к значению переменной (несмотря на то, что обратиться к переменной по имени не можем). Именно поэтому компиляторщики вместо термина "локальная" используют термин "автоматическая". И, таким образом, то, что обычно называют "локальная переменная", мы будем называть "автоматический объект" (automatic object) - от понятия "автоматическое время жизни".

Переменная, описанная вне функции, называется "глобальная" (global variable). Здесь так же в первую очередь подразумевается глобальная область видимости. Т.е. к глобальной переменной можно обращаться по имени из всех функций (или из всех функций данного файла, если у переменной есть модификатор static). Для компиляторщиков с точки зрения обсуждаемого в теме вопроса нет разницы между глобальными переменными, которые описаны с модификатором static или без него. Поэтому всё то, что обычно называют "глобальными переменными" мы будем называть термином "статический объект" (static object) - от понятия "статическое (т.е. не автоматическое) время жизни".

Внимание! Слово "статический" никак не связано с модификатором static. Уходя немного в сторону, разработчики языка Си немного погорячились, когда придумали слово "static", ибо исторически (с прошлых языков программирования) словом "static" называли именно глобальные переменные. А то, что принято называть "глобальная переменная" и "глобальная переменная с модификатором static", у компиляторщиков (да и в технической литературе по компиляторам) называют словами "global" и "local" соответственно. Но данный аспект нам сейчас не важен. На важно лишь не перепутать понятия "статический объект" и "модификатор static"

Переменная языка Си\Си++, определённая внутри функции и описанная с модификатором static, с точки зрения времени жизни попадает под понятие "статический объект"

Всё то, что создано динамически (а поскольку мы говорим о конструкторах, то прежде всего всё то, что выделено оператором new), мы будем называть "динамический объект" (dynamic object).

В языке Pascal есть понятие "функции" (function) и "процедуры" (procedure) - они отличаются тем, что одно возвращает значение, а другое - нет. В языке Си порешили, что всё будет называться "функциями". С появлением Си++ пришло понятие "метод" (method). С точки зрения компилятора на низком уровне - все эти понятия являются просто набором кодов, а потому компиляторщики все эти понятия объединяют в одно и называют "процедура".

Итак, вкратце подведём итоги по терминологии:
  • Объект (object) - сущность, описывающая некоторый участок памяти. Другими словами, это переменная языка или динамически выделенная память (например, оператором new). Не путать с "объектом Си++". Наше понятии "объект" не завязано ни на какой конкретный язык программирования, несмотря на то, что пояснения ведётся в контексте языка Си++
  • Автоматический объект (automatic object) - локальная переменная функции или лексического блока
  • Статический объект (static object) - глобальная переменная (независимо от наличия модификатора static) или локальная переменная, описанная с модификатором static
  • Динамический объект (dynamic object) - динамически выделенная память (например, оператором new)
  • Процедура (procedure) - процедура, функция, метод







3. Время жизни объекта и память, выделяемая под объект

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

Временем жизни (lifetime) объекта называют тот период времени работы программы, в течение которого память, выделенная при создании объекта, закрепляется за этим объектом. Другими словами, если мы в течение времени жизни объекта будем обращаться к его памяти, то мы прочитаем корректное значение (при условии, что у нас в программе нет ошибок по записи в чужую память)

Время жизни автоматического объекта начинается в момент его объявления. Можно считать, что начиная с момента исполнения начала процедуры или лексического блока память под объект выделена. Я не буду заострять внимания на том, каким образом компилятор обеспечивает выделение памяти, важно лишь то, что начиная с этого момента к этой памяти можно обращаться на законных основаниях и эта память закреплена за этим объектом. Время жизни автоматического объекта заканчивается при выходе управления программы из процедуры или лексического блока, в котором определён объект. В этот момент можно считать, что память освобождается. Здесь опять-таки хочется упомянуть, что реально память не освобождается и к ней можно обратиться, но компилятор уже не гарантирует, что в этой памяти сохранится значение переменной, т.к. компилятор или система с этого момента имеют право использовать эту память по своему усмотрению.

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


Время жизни статического объекта условно можно считать равным времени жизни программного модуля, в котором находится объект. Программным модулем называют любую логически готовую к исполнению единицу, оформленную в виде отдельного файла. Другими словами, программный модуль - это исполняемый файл (для тех, кто под windows - это файл *.exe) или динамическая библиотека (для тех, кто под windows - это файл *.dll). Таким образом, если статический объект попал в исполняемый файл, то его временем жизни можно считать "вся программа": память под объект выделяется до того, как началось исполнение main; начиная с момента, когда мы вошли в main и заканчивая моментом, когда мы вышли из main (или вызвали exit или любой другой способ завершения программы), эта память закреплена за объектом. После того, как мы вышли из main (вызвали exit и т.п.) условно можно считать, что память освобождается. Аналогично случаю с автоматическими объектами, реального освобождения памяти не происходит, но нам это не важно, поскольку мы попросили программу завершиться, а значит, больше никаких действий выполнять не будем. А если статический объект попал в динамическую библиотеку, то время жизни объекта начинается в тот момент, когда динамическая библиотека загружена в память, и заканчивается в тот момент, когда динамическая библиотека выгружена из памяти. Некоторые забывают про эту особенность.

Время жизни динамического объекта начинается в момент явного (т.е. написанного программистом) создания динамической памяти (по сути дела в момент вызова new). Время жизни динамического объекта заканчивается в момент явного удаления динамической памяти (т.е. по сути вызов delete). Если программа написана таким образом, что delete не вызывается, то можно считать, что память от объекта не освобождается. Реально эта память освобождается, но уже на уровне процессов операционной системы, но вплоть до последней исполненной команды нашей программы эта память принадлежит динамическому объекту.

Итак, вкратце подведём итоги по выделению памяти для объекта и его времени жизни:
  • Автоматический объект. Время жизни начинается в момент входа в процедуру или лексический блок, в котором объявлен объект и заканчивается при выходе из процедуры или лексического блока.
  • Статический объект, попавший в исполняемый файл. Время жизни начинается до входа в процедуру main и заканчивается после выхода из процедуры main
  • Статический объект, попавший в динамическую библиотеку. Время жизни начинается с момента загрузки динамической библиотеки и заканчивается в момент выгрузки динамической библиотеки.
  • Динамический объект. Время жизни начинается в момент явного выделения памяти для объекта и заканчивается в момент явного освобождения памяти. Если память явным образом не освобождается, то данная память так и остаётся использованной и будет высвобождена средствами операционной системы







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

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

Чтобы лучше понять, что на самом деле творится в программе, попробую пояснить на более низкоуровневых аналогах, описанных на языке Си. Все разъяснения оформлены в виде комментариев к соответствующим участкам программ.

4.1. Автоматический объект

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class T
{
  private:
    int x, y;
  public:
    T (int _x, int _y) { x = _x; y = _y; }
    /* Смысла в этом деструкторе нет, но пишу его в таком виде, чтобы он был не пустой */
    ~T () { x = 0; y = 0; }
}
...
void
func (void)
{
  T a(10,11), b(20,21);
 
  /* тело процедуры */
}
эквивалентном на языке Си будет служить вот такой код:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct T
{
  int x, y;
};
 
/* Эта процедура является отображением конструктора T::T(int,int) */
void
T_constr (struct T *this, int _x, int _y)
{
  this->x = _x;
  this->y = _y;
}
 
/* Эта процедура является отображением деструктора T::~T() */
void
T_destr (struct T *this)
{
  this->x = 0;
  this->y = 0;
}
...
void
func (void)
{
  struct T a; /* выделяем память для объекта "a" */
  struct T b; /* выделяем память для объекта "b" */
  T_constr (&a, 10, 11); /* вызываем конструктор для "a" */
  T_constr (&b, 20, 21); /* вызываем конструктор для "b" */
  
  /* тело процедуры */
 
  T_destr (&a); /* вызываем деструктор для "a" */
  T_destr (&b); /* вызываем деструктор для "b" */
  /* по достижении выхода из процедуры происходит удаление памяти для объектов "a" и "b" */
}
4.2. Статический объект

Здесь хочется обратить особое внимание на объект "c", потому что правила его инициализации для начинающих будут совсем не очевидными

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* Описание класса такое же, как и в предыдущем примере.
 * Я повторяю его, чтобы не нарушить целостность примера. */
class T
{
  private:
    int x, y;
  public:
    T (int _x, int _y) { x = _x; y = _y; }
    /* Смысла в этом деструкторе нет, но пишу его в таком виде, чтобы он был не пустой */
    ~T () { x = 0; y = 0; }
}
...
/* Статический объект, объявленный как глобал */
T a(10,11), b(20,21);
...
void
main (void)
{
  ...
  if (...)
  {
    /* Статический объект, объявленный как локал */
    static T c(30,31);
  }
  ...
}
эквивалентном на языке Си будет служить вот такой код:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
struct T
{
  int x, y;
};
 
/* Эта процедура является отображением конструктора T::T(int,int) */
void
T_constr (struct T *this, int _x, int _y)
{
  this->x = _x;
  this->y = _y;
}
 
/* Эта процедура является отображением деструктора T::~T() */
void
T_destr (struct T *this)
{
  this->x = 0;
  this->y = 0;
}
...
/* Объекты "a" и "b" являются статическими. Память для них выделяется в некоторый
 * момент до попадения в main, а освобождается после выхода из main */
struct T a;
struct T b;
 
/* Объект "c" является "локальным". Но, поскольку он статический, то память
 * под него выделяется наравне с прочими статическими объектами */
struct T c;
 
/* См. места использования */
int is_c_init = 0;
...
/* Эта процедура будет вызвана хитрыми системными способами до попадения
 * в main, но после того, как будет выделена память для объектов "a" и "b" */
void
_init (void)
{
  T_constr (&a, 10, 11); /* вызываем конструктор для "a" */
  T_constr (&b, 20, 21); /* вызываем конструктор для "b" */
 
  /* ВНИМАНИЕ! Конструктор для "c" здесь НЕ вызывается */
}
 
/* Эта процедура так же будет вызвана системными способами после выхода
 * из main, но до того, как будет освобождена память из-под объектов "a" и "b" */
void
_finish (void)
{
  T_destr (&a); /* вызываем деструктор для "a" */
  T_destr (&b); /* вызываем деструктор для "b" */
 
  /* Деструктор для "c" вызовем только в том случае, если был вызван конструктор для "c".
   * Несмотря на то, что конструктор для "c" вызывается не на одном уровне с "a" и "b",
   * деструкторы для всех статических объектов вызываются на одном уровне */
  if (is_c_init == 1)
    T_destr (&c);
}
...
/* С формальной точки зрения, казалось бы, вызовы псевдопроцедур _init и _finish
 * можно поставить в начало и конец main'а. Однако это не совсем верно. Программа
 * может завершить исполнение через функцию exit, т.е. до конца main управление
 * не дойдёт. Однако вызов _finish всё равно произойдёт. Т.е. при любом НЕаварийном
 * завершении программы компилятор обеспечит вызов деструкторов для статических
 * объектов */
void
main (void)
{
  ...
  if (...)
  {
    /* ВНИМАНИЕ! Конструктор для "c" вызываем только в том случае, если работа
     * программы достигла этой точки. Конструктор вызовется только один раз,
     * даже если через эту точку проходим несколько раз (в цикле). Если в данную
     * ветку исполнения не попали ни разу, то конструктор для "c" вызван НЕ будет */
    if (is_c_init == 0)
    {
      is_c_init = 1;
      T_constr (&c, 30, 31);
    }
  }
  ...
}
В данном примере я везде писал про то, что что-то делается "до входа в процедуру main", а что-то - "после выхода из main". Всё это в предположении, что мы работаем с исполняемым файлом. В случае же, если мы работаем с динамической библиотекой, то эти понятия следует читать как "в момент загрузки динамической библиотеки" и "в момент выгрузки динамической библиотеки". Если призадуматься, то для исполняемого файла понятия "до входа в процедуру main" и "в момент загрузки исполняемого файла" принципиально ничем друг от друга не отличаются, а потому всё это можно было бы описать обобщёнными понятиями "в момент загрузки программного модуля" и "в момент выгрузки программного модуля". Но, поскольку статья предназначена для начинающих, то я в примере привёл наиболее понятный для всех вариант - работу с исполняемым файлом, а всё остальное обговорил в данном абзаце.

4.3. Динамический объект

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* Описание класса такое же, как и в предыдущем примере.
 * Я повторяю его, чтобы не нарушить целостность примера. */
class T
{
  private:
    int x, y;
  public:
    T (int _x, int _y) { x = _x; y = _y; }
    /* Смысла в этом деструкторе нет, но пишу его в таком виде, чтобы он был не пустой */
    ~T () { x = 0; y = 0; }
}
...
/* Динамические объекты адресуются указателями и никак больше */
T *ta, *tb;
...
void
func1 (void)
{
  ...
  /* Создаём динамические объекты */
  ta = new T (10, 11);
  ...
  tb = new T (20, 21);
  ...
}
 
void
func2 (void)
{
  ...
  /* Удаляем динамические объекты */
  delete ta;
  ...
  delete tb;
  ...
}
эквивалентном на языке Си будет служить вот такой код:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
struct T
{
  int x, y;
};
 
/* Эта процедура является отображением конструктора T::T(int,int) */
void
T_constr (struct T *this, int _x, int _y)
{
  this->x = _x;
  this->y = _y;
}
 
/* Эта процедура является отображением деструктора T::~T() */
void
T_destr (struct T *this)
{
  this->x = 0;
  this->y = 0;
}
...
/* Указатель он и в Африке указатель*/
struct T *ta, *tb;
...
void
func1 (void)
{
  ...
  /* Оператор new одновременно выполняет две вещи:
   * выделение памяти и вызов конструктора */
  ta = (struct T*) malloc (sizeof (struct T));
  T_constr (ta, 10, 11);
  ...
  tb = (struct T*) malloc (sizeof (struct T));
  T_constr (tb, 20, 21);
  ...
}
 
void
func2 (void)
{
  ...
  /* Оператор delete также одновременно выполняет две вещи:
   * вызов деструктора и удаление памяти */
  T_destr (ta);
  free (ta);
  ...
  T_destr (tb);
  free (tb);
  ...
}
Исходя из этого вытекает важное свойство динамического объекта. Если для автоматического и статического объекта удалением занимается компилятор, то динамический объект удалять должен программист. Если delete для автоматического объекта не было вызвано, то память всё равно освободится на уровне операционной системы, но деструктор вызван не будет. Это является критичным, когда в деструкторе выполняются некоторые полезные действия (например, запись в файл).







5. Неочевидные моменты при работе с конструкторами и деструкторами в Си++

Как я уже писал в предисловии, в этом разделе соберу тонкости, связанные с конструкторами и деструкторами Си++, которые изначально в статью писать не планировал

5.1. Конструкторы и деструкторы не являются процедурами

За основу взята тема Деструктор для массива матриц... (посты 10, 11, 15)

По синтаксису языка конструкторы и деструкторы выглядят как процедуры. Техническая их реализация (т.е. реализация в виде кода) ничем не отличается от реализации обычной процедуры. Однако с точки зрения языка Си++ конструктор функцией НЕ является, а потому нельзя вызывать конструктор "ручками" и нельзя брать адрес на конструктор. Логика такого поведения следующая: конструктор является механизмом инициализации объекта, а инициализация является составной частью процесса порождения объекта. И, таким образом, конструктор может вызываться только непосредственно в момент рождения объекта.

С деструкторами логика ровно такая же, однако по языку деструктор можно вызывать "ручками" и брать на него адрес. Такое поведени обусловлено тем, что в языке Си++ существует оператор new, который выделяет память не абы где, а использует ту память, которую ему навязал программист (так называемый, placement new)

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
 
class T
{
    int x;
  public:
    T() { std::cout << "constr\n"; }
    ~T() { std::cout << "destr\n"; }
};
 
int main (void)
{
  char *buf = new char[sizeof(T)];
  T *t = new(buf) T;
  
  /* Если бы "t" выделяли через простой new, то "delete t" по сути дела
   * выполнил бы те же самые два действия: вызвал деструктор и освободил память */
  t->~T();
  delete[] buf;
}
В этом случае в процессе удаления объекта компилятор не имеет никакой возможности корректно высвободить память: память выделял программист, а не компилятор, а поэтому компилятор не знает, как эту память освобождать (и нужно ли её освобождать вообще). В этом случае программист должен "ручками" вызвать деструктор и НЕ вызывать оператор delete. С точки зрения стандарта - это единственный случай, где допустимо вызывать деструктор. В любом другом случае поведение не определено и код считается некорректным.

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

5.2. Вызов деструкторов при исполнении return посередине процедуры

Глядя на примеры из раздела 4, может показаться, что деструктор вызывается только в момент завершения работы процедуры (или лексического блока) и что если где-то посередине процедуры содержался оператор return, то исполнени до вызова деструкторов не дойдёт. Это всё из-за того, что примеры я писал для разделения процесса выделения памяти и инициаализации, а потому не стал усложнять аналоги на Си с учётом возможностей "досрочного" завершения процедуры. Реально же в момент исполнения return'а компилятор обеспечит вызов деструкторов для всех живых автоматических объектов. Грубо говоря, можно сказать следующее: при завершении процедуры удаляется вся память, выделенная к моменту возврата под автоматические объекты процедуры. В процессе удаления вызовутся деструкторы. Это можно увидеть на следующем примере:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
 
class T
{
    int x;
  public:
    T (int _x) : x(_x) { std::cout << "constr " << x << std::endl; }
    ~T () { std::cout << "destr " << x << std::endl; }
};
 
void func (int x)
{
  std::cout << "Start func" << std::endl;
  T t(1);
 
  if (x == 4)
  {
    T t(2);
  } else
  {
    T t(3);
    static T tt(4);
    std::cout << "return from func" << std::endl;
    return;
  }
 
  T tt(5);
  std::cout << "Finish func" << std::endl;
}
 
int main (void)
{
  func (5);
  std::cout << "In main after func" << std::endl;
  return 0;
}
Вот выдача от данного теста:

Код:
Start func
constr 1
constr 3
constr 4
return from func
destr 3
destr 1
In main after func
destr 4
Мы выдим, что объект t(2) находится в ветке if'а, в которую в процессе исполнения функции func мы не заходили, а потому на момент возврата из func этот объект НЕ является живым (и потому в печати он не засветился). Объект tt(4) НЕ является автоматическим, а потому должен жить до конца программы, а не до завершения работы процедуры (или лексического блока). До точки рождения объекта t(5) мы вообще не дошла, а потому объект так же не засвечен в печати

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

В момент отработки исключительной ситуации (exception) происходит так называемый межпроцедурный переход. Т.е. происходит переход в некоторую точку, находящуюся в текущей процедуре (что редко) или в процедуре, активация которой располагается выше по стеку вызовов. На пальцах такой переход можно трактовать как принудительное исполнение return'а во всех процедурах по стеку вызовов. И, таким образом, происходит удаление памяти, выделенной под все автоматические объекты во всех процедурах, через которые происходит бросание исключительной ситуации. Т.е. то же самое, что и в предыдущем разделе, но уже по всей цепочке вызовов происходит вызов деструкторов для всех живых автоматических объектов. Это можно увидеть на следующем примере:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
 
class T
{
    int x;
  public:
    T (int _x) : x(_x) { std::cout << "constr " << x << std::endl; }
    ~T () { std::cout << "destr " << x << std::endl; }
};
 
void func1 (void)
{
  T t(20);
  std::cout << "Start exception" << std::endl;
  throw int();
}
 
void func2 (int x)
{
  T t(10);
 
  {
    T t(11);
  }
 
  if (x == 4)
  {
    T t(12);
    func1();
  } else
  {
    T t(13);
    static T tt(14);
    func1();
  }
}
 
int main (void)
{
  try
  {
    T t(1);
    func2 (5);
  } catch (...)
  {
    std::cout << "Finish exception" << std::endl;
  }
 
  return 0;
}
Вот выдача в процессе исполнения данного примера:

Код:
constr 1
constr 10
constr 11
destr 11
constr 13
constr 14
constr 20
Start exception
destr 20
destr 13
destr 10
destr 1
Finish exception
destr 14
Объект t(11) завершил своё время жизни на границе закрывающей фигурной скобки, а потому его деструктор был вызван до того, как начался процесс выбрасывания исключительной ситуации. Объект t(12) находится в ветке if'а, в которую в процессе исполнения функции func2 мы не заходили, а потому на момент выбрасывания исключительной ситуации этот объект НЕ является живым (и потому в печати он не засветился). Объект tt(14) НЕ является автоматическим, а потому как объект должен жить до конца программы, а не до завершения работы процедуры (или лексического блока)

Хочется отметить, что межпроцедурный переход через longjmp таким свойством не обладает. А точнее - не обязан обладать, ибо описан в стандарте языка Си, в котором понятия деструктора не было вообще.

5.4. Вызов деструкторов при принудительном завершении потока

FIXME дописать

5.5. Классы, содержащие указатели

За основу взят пост https://www.cyberforum.ru/post804953.html

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

5.6. Отличия между new/delete и malloc/free

За основу взята тема Переменная + индекс (начиная с поста #17)

Ещё один из часто задаваемых вопросов является отличие между new/delete и malloc/free. Прежде всего хочется отметить, что это конструкции по сути дела из разных языков программирования. Язык Си является языком низкого уровня (фактически это ассемблер, только реализованный в виде языка программирования), а потому не имеет встроенных средств для работы с динамической памятью. Функции malloc/free являются внешними библиотечными функциями и не входят в состав языка (хотя и описаны в стандарте). А язык Си++ - это уже язык высокого уровня. И этот язык содержит встроенные средства для работы с динамической памятью - операторы new/delete, которые, к слову говоря, внутри себя реализуются через те же самые malloc и free (но необязательно именно так).

Принципиальные отличия между new/delete и malloc/free следующие:
  • Как следует из раздела 4.3 данной статьи, оператор new не только выделяет память, но и вызывает конструкторы, а delete, соответственно, помимо освобождения памяти вызывает деструкторы.
  • Оператор new кидает исключение при нехватке памяти, а malloc при нехватке памяти возвращает NULL. Из этого следует то, что при использовании malloc'а всегда нужно делать проверку возвращаемого значения. Обычно это делает процедура-обёртка над malloc'ом, которую в каждой программе реализуют по-своему. А при использовании new достаточно "наверху" написать обработчик исключительной ситуации и больше этим вопросом не заниматься. Более того, обработчик можно и не реализовывать, в этом случае сработает некий "системный" обработчик.
  • Операторы new и delete перегружаемы, в том числе только для каждого конкретного класса. Как и большинство конструкций Си++, это удобно при написании, но не удобно при чтении (особенно чужого кода).

5.7. Отличия между конструированием объекта и присваиванием

За основу взята тема Присваивание конструктором копирования

FIXME дописать

5.8. Некоторые тонкости при наличии печати в стандартный вывод внутри деструктора

За основу взята тема Деструктор (а конкретно пост #7)

FIXME дописать

5.9. Исключительные ситуации в процессе работы конструкторов и деструкторов

За основу взят пост https://www.cyberforum.ru/faq/... post872457







6. Заключение

FIXME дописать
Только забыл, что хотел :(







7. Ссылки на темы, где обсуждался данный вопрос







8. Внешние ссылки по данной тематике
Показов 129768 Комментарии 8
Всего комментариев 8
Комментарии
  1. Старый комментарий
    Аватар для AzaKendler
    хочу уточнить explicit - принуждает - (позволяет?) вызвать конструктор явно.
    Запись от AzaKendler размещена 16.02.2012 в 00:02 AzaKendler вне форума
  2. Старый комментарий
    Аватар для Evg
    Да фиг его знает Это в книгах надо читать. Я тут всего лишь описывал, как конструкторы и деструкторы на низком уровне устроены. Но точно знаю, что это высокоуровневая конструкция, которая НЕ влияет на принципы, описанные в данной статье
    Запись от Evg размещена 16.02.2012 в 08:19 Evg вне форума
  3. Старый комментарий
    Аватар для AzaKendler
    Цитата:
    Время жизни объекта
    есть временный объект класса. после вызова деструктора каково время жизни его полей данных, если специально они не очищались и не обнулялись?
    они "отъедут" после последующего вызова функции на стеке? после 3..,10 вызовов?
    Запись от AzaKendler размещена 16.02.2012 в 14:32 AzaKendler вне форума
  4. Старый комментарий
    Аватар для Evg
    Дальнейшее обсуждение вопроса уехало на форум: https://www.cyberforum.ru/cpp/thread447243.html
    Запись от Evg размещена 16.02.2012 в 19:15 Evg вне форума
  5. Старый комментарий
    Аватар для Evg
    В раздел 3 под cut'ом добавил "Только для продвинутых и особо любознательных"
    Запись от Evg размещена 17.02.2012 в 09:25 Evg вне форума
  6. Старый комментарий
    Аватар для #pragma

    Вот ещё древняя тема

    https://www.cyberforum.ru/cpp-... 06657.html
    Можно тоже добавить, полезно
    Запись от #pragma размещена 10.06.2012 в 21:38 #pragma вне форума
  7. Старый комментарий
    Аватар для Evg
    Добавил
    Запись от Evg размещена 10.06.2012 в 22:03 Evg вне форума
  8. Старый комментарий
    Аватар для Croessmah
    Еще немного особенностей деструкторов: https://www.cyberforum.ru/cpp-... st10019995
    Запись от Croessmah размещена 14.05.2019 в 21:45 Croessmah вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2024, CyberForum.ru