Идентификация конструктора и деструктора
"то, что не существует в одном тексте (одном возможном мире), может существовать в других текстах (возможных мирах)"
тезис семантики возможных миров
Конструктор, в силу своего автоматического вызова при создании нового экземпляра объекта, – первая по счету вызываемая функция объекта. Так какие сложности в его идентификации? Камень преткновения в том, что конструктор факультативен, т.е. может присутствовать в объекте, а может и не присутствовать. Поэтому, совсем не факт, что первая вызываемая функция – конструктор!
Заглянув в описание языка Си++, можно обнаружить, что конструктор не возвращает никакого значения, что нехарактерно для обычных функций, однако, все же не настолько редко встречается, чтобы однозначно идентифицировать конструктор. Как же тогда быть?
Выручает то обстоятельство, что по стандарту конструктор не должен автоматически вызывать исключения, даже если отвести память под объект не удалось. Реализовать это требование можно множеством различных способов, но все, виденные мной компиляторы, просто помещают перед вызовом конструктора проверку на нулевой указатель, передавая ему управление только при удачном выделении памяти для объекта. Напротив, все остальные функции объекта вызываются всегда – даже при неуспешном выделении памяти. Вернее, пытаются вызываться, но нулевой указатель (возращенный при ошибке отведения памяти) при первой же попытке обращения вызывает исключение, передавая "бразды правления" обработчику соответствующей исключительной ситуации.
Таким образом, функция, "окольцованная" проверкой нулевого указателя, и есть конструктор, а ни что иное. Теоретически, впрочем, подобная проверка может присутствовать и при вызове других функций, конструктором не являющихся, но… во всяком случае мне на практике с каким еще не приходилось встречаться.
Деструктор, как и конструктор факультативен, т.е. последняя вызываемая функция объекта не факт, что деструктор. Тем не менее, отличить деструктор от любой другой функции очень просто – он вызывается только при результативном создании объекта (т.е.
успешном выделении памяти) и игнорируется в противном случае. Это – документированное свойство языка, следовательно, обязательное к реализации всеми компиляторами. Таким образом, в код помещается такое же "кольцо", как и у конструктора, но никакой путаницы не возникает, т.к. конструктор вызывается всегда первым (если он есть), а деструктор – последним.
Особый случай представляет объект, целиком состоящий из одного конструктора (или деструктора) – попробуй, разберись, с чем мы имеем дело. И разобраться можно! За вызовом конструктора практически всегда присутствует код, обнуляющий this в случае неудалого выделения памяти, - а у деструктора этого нет! Далее – деструктор обычно вызывается не непосредственно из материнской процедуры, а из функции-обертки, вызывающей помимо деструктора и оператор delete, освобождающий занятую объектом память. Так, что отличить конструктор от деструктора вполне можно!
Давайте, для лучшего уяснения сказанного рассмотрим следующий пример:
#include <stdio.h>
class MyClass{
public:
MyClass(void);
void demo(void);
~MyClass(void);
};
MyClass::MyClass()
{
printf("Constructor\n");
}
MyClass::~MyClass()
{
printf("Destructor\n");
}
void MyClass::demo(void)
{
printf("MyClass\n");
}
main()
{
MyClass *zzz = new MyClass;
zzz->demo();
delete zzz;
}
Листинг 38 Демонстрация конструктора и деструктора
Результат его компиляции в общем случае должен выглядеть так:
Constructor proc near ; CODE XREF: main+11p
; функция конструктора. То, что это именно конструктор можно понять из реализации
; его вызова (см. main)
push esi
mov esi, ecx
push offset aConstructor ; "Constructor\n"
call printf
add esp, 4
mov eax, esi
pop esi
retn
Constructor endp
Destructor proc near ; CODE XREF: __destructor+6p
; функция деструктора. То, что это именно деструктор, можно понять из реализации
; его вызова (см. main)
push offset aDestructor ; "Destructor\n"
call printf
pop ecx
retn
Destructor endp
demo proc near ; CODE XREF: main+1Ep
; обычная
функия demo
push offset aMyclass ; "MyClass\n"
call printf
pop ecx
retn
demo endp
main proc near ; CODE XREF: start+AFp
push esi
push 1
call ??2@YAPAXI@Z ; operator new(uint)
add esp, 4
; выделяем память для нового объекта
; точнее, пытаемся это сделать
test eax, eax
jz short loc_0_40105A
; Проверка успешности выделения памяти для объекта.
; Обратите внимание: куда направлен jump.
; Он направлен на инструкцию XOR ESI,ESI, обнуляющую указатель на объект –
; при попытке использования нулевого указателя возникнет исключение,
; но конструктор не должен вызывать исключение даже если память под объект
; отвести не удалось.
; Поэтому, конструктор получает управление только при успешном отводе памяти!
; Следовательно, функция, находящаяся до XOR ESI,ESI, и есть конструктор!!!
; И мы сумели надежно идентифицировать ее.
mov ecx, eax
; готовим указатель this
call Constructor
; эта функция – конструктор, т.к. вызывается только при удачном отводе памяти
mov esi, eax
jmp short loc_0_40105C
loc_0_40105A: ; CODE XREF: main+Dj
xor esi, esi
; обнуляем указатель на объект, чтобы вызвать исключение при попытке его
; использования
; Внимание: конструктор никогда не вызывает исключения, поэтому,
; нижележащая функция гарантированно не является конструктором
loc_0_40105C: ; CODE XREF: main+18j
mov ecx, esi
; готовим указатель this
call demo
; вызываем обычную функцию объекта
test esi, esi
jz short loc_0_401070
; проверка указателя this на NULL. Деструктор вызываться только в том случае
; если память под объект была отведена (если же она не была отведена
; освобождать особо нечего)
; таким образом, следующая функция – именно деструктор, а не что-нибудь еще
push 1
; количество байт для освобождения (необходимо для delete)
mov ecx, esi
; готовим указатель this
call __destructor
; вызываем деструктор
loc_0_401070: ; CODE XREF: main+25j
pop esi
retn
main endp
__destructor proc near ; CODE XREF: main+2Bp
; функция деструктора. Обратите внимание, что деструктор обычно вызывается
; из той же функции, что и delete (хотя так бывает и не всегда, но очень часто)
arg_0 = byte ptr 8
push ebp
mov ebp, esp
push esi
mov esi, ecx
call Destructor
; вызываем функцию деструктора, определенную пользователем
test [ebp+arg_0], 1
jz short loc_0_40109A
push esi
call ??3@YAXPAX@Z ; operator delete(void *)
add esp, 4
; освобождаем память, ранее выделенную объекту
loc_0_40109A: ; CODE XREF: __destructor+Fj
mov eax, esi
pop esi
pop ebp
retn 4
__destructor endp
Листинг 39
::объекты в автоматической памяти или когда конструктор/деструктор идентифицировать невозможно. Если объект размещается в стеке (автоматической памяти), то никаких проверок успешности ее выделения не выполняется и вызов конструктора становится неотличим от вызова остальных функций. Аналогичная ситуация и с деструктором – стековая память автоматически освобождается по завершению функции, а вместе с ней умирает и сам объект безо всякого вызова delete (delete применяется только для удаления объектов из кучи).
Чтобы убедиться в этом, модифицируем функцию main нашего предыдущего примера следующим образом:
main()
{
MyClass zzz;
zzz.demo();
}
Листинг 40
Результат компиляции в общем случае должен выглядеть так:
main proc near ; CODE XREF: start+AFp
var_4 = byte ptr -4
; локальная переменная zzz – экземпляр объекта MyClass
push ebp
mov ebp, esp
push ecx
lea ecx, [ebp+var_4]
; подготавливаем указатель this
call constructor
; вызываем конструктор, как и обычную функцию!
; долгаться, что это конструктор можно разве что по его содержимому
; (обычно конструктор инициализирует объект), да и то неуверенно
lea ecx, [ebp+var_4]
call demo
; вызываем функцию demo, - обратите внимание, ее вызов ничем не отличается
; от вызова конструктора!
lea ecx, [ebp+var_4]
call destructor
; вызываем деструктор – его вызов, как мы уже поняли, ничем
; характерным не отмечен
mov esp, ebp
pop ebp
retn
main endp
Листинг 41
::идентификация конструктора/деструктора в глобальных объектах. Глобальные объекты (так же называемые статическими объектами) размешаются в сегменте данных еще на стадии компиляции. Стало быть, ошибки выделения памяти в принципе невозможны и, выходит, что по аналогии со стековыми объектами, надежно идентифицировать конструктор/деструктор и здесь нельзя? А вот и нет!
Глобальный объект, в силу свой глобальности, доступен из многих мест программы, но его конструктор должен вызываться лишь однажды. Как можно это обеспечить? Конечно, возможны самые различные варианты реализации, но большинство компиляторов идут по простейшему пути, используя для этой цели глобальную переменную-флаг, изначально равную нулю, а перед первым вызовом конструктора увеличивающуюся на единицу (в более общем случае устанавливающуюся в TRUE). При повторных итерациях остается проверить – равен ли флаг нулю, и если нет – пропустить вызов конструктора. Таким образом, конструктор вновь "окольцовывается" условным переходом, что позволяет его безошибочно отличить ото всех остальных функций.
С деструктором еще проще – раз объект глобальный, то он уничтожается только при завершении программы. А кто это может отследить кроме поддержки времени исполнения? Специальная функция, такая как _atexit, принимает на вход указатель на конструктор, запоминает его и затем вызывает при возникновении в этом необходимости.
Интересный момент - _atexit (или что там используется в вашем конкретном случае) должна быть вызвана лишь однократно (надеюсь, понятно почему?). И, чтобы не вводить еще один флаг, она вызывается сразу же после вызова конструктора! На первый взгляд объект может показаться состоящим из одних конструктора/деструктора, но это не так! Не забывайте, что _atexit не передает немедленно управление на код деструктора, а только запоминает его указатель для дальнейшего использования!
Таким образом, конструктор/деструктор глобального объекта очень просто идентифицировать, что и доказывает следующий пример:
main()
{
static MyClass zzz;
zzz.demo();
}
Листинг 42
Результат его компиляции в общем случае должен выглядеть так:
main proc near ; CODE XREF: start+AFp
mov cl, byte_0_4078E0 ; флаг инициализации экземпляра объекта
mov al, 1
test al, cl
; объект инициализирован?
jnz short loc_0_40106D
; --> да, инициализирован, - не вызываем конструктор
mov dl, cl
mov ecx, offset unk_0_4078E1 ; экземляр объекта
; готовим указатель this
or dl, al
; устанавливаем флаг инициализации в TRUE
; и вызываем конструктор
mov byte_0_4078E0, dl ; флаг инициализации экземпляра объекта
call constructor
; Вызов конструктора.
; Обратите внимание, что если экземпляр объекта уже инициализирован
; (см. проверку выше) конструктор не вызывается.
; Таким образом, его очень легко отождествить!
push offset thunk_destructo
call _atexit
add esp, 4
; Передаем функции _atexit указатель на деструктор,
; который она должна вызвать по завершении программы
loc_0_40106D: ; CODE XREF: main+Aj
mov ecx, offset unk_0_4078E1 ; экземпляр
объекта
; готовим указатель this
jmp demo
; вызываем demo
main endp
thunk_destructo: ; DATA XREF: main+20o
; переходник к функции-деструктору
mov ecx, offset unk_0_4078E1 ; экземпляр объекта
jmp destructor
byte_0_4078E0 db 0 ; DATA XREF: mainr main+15w
; флаг инициализации экземпляра объекта
unk_0_4078E1 db 0 ; ; DATA XREF: main+Eo main+2Do ...
; экземпляр объекта
Листинг 43
Аналогичный код генерирует и Borland C++. Единственное отличие – более хитрый вызов деструктора. Вызовы всех деструкторов помещены в специальную процедуру, которая выдает себя тем, что обычно располагается перед библиотечными функциями (или в непосредственной близости от них), так что идентифицировать ее очень легко. Смотрите сами:
_main proc near ; DATA XREF: DATA:00407044o
push ebp
mov ebp, esp
cmp ds:byte_0_407074, 0 ; флаг инициализации объекта
jnz short loc_0_4010EC
; Если объект уже инициализирован – конструктор не вызывается
mov eax, offset unk_0_4080B4 ; Экземпляр объекта
call constructor
inc ds:byte_0_407074 ; флаг инициализации объекта
; Увеличиваем флаг на единицу, возводя его в TRUE
loc_0_4010EC: ; CODE XREF: _main+Aj
mov eax, offset unk_0_4080B4 ; Экземляр
объекта
call demo
; Вызов
функции demo
xor eax, eax
pop ebp
retn
_main endp
call_destruct proc near ; DATA XREF: DATA:004080A4o
; Эта функция содержит в себе вызовы всех деструкторов глобальных объектов,
; поскольку, вызов каждого деструктора "окольцован" проверкой флага инициализации,
; эту функцию легко идентифицировать – только она содержит подобный "калечный код"
; (вызовы конструкторов обычно разбросаны по всей программе)
push ebp
mov ebp, esp
cmp ds:byte_0_407074, 0 ; флаг инициализации объекта
jz short loc_0_401117
; объект был инициализирован?
mov eax, offset unk_0_4080B4 ; Экземпляр объекта
; готовим указатель this
mov edx, 2
call destructor
; вызываем деструктор
loc_0_401117: ; CODE XREF: call_destruct+Aj
pop ebp
retn
call_destruct endp
Листинг 44
:: виртуальный деструктор. Деструктор тоже может быть виртуальным! А почему бы и нет? Это бывает полезно, когда экземпляр производного класса удаляется через указатель на базовый объект. Поскольку, виртуальные функции связаны с классом объекта, а не с классом указателя, то вызывается виртуальный деструктор, связанный с типом объекта, а не с типом указателя. Впрочем, эти тонкости относятся к непосредственному программированию, а исследователей в первую очередь интересует: как идентифицировать виртуальный деструктор. О, это просто – виртуальный деструктор совмещает в себе свойства обычного деструктора и виртуальной функции (см. "Идентификация виртуальных функций").
::виртуальный конструктор. Виртуальный конструктор?! А что, разве есть такой? Ничего подобного стандартный Си++ не поддерживает. Непосредственно не поддерживает. И, когда виртуальный конструктор позарез требуется программистом (впрочем, бывает это лишь в весьма экзотических случаях), они прибегают к ручной эмуляции некоторого его подобия. В специально выделенную для этих целей виртуальную функцию (не конструктор!) помещается приблизительно следующий код: "return new имя класса (*this)" или "return new имя класса (*this)". Этот трюк кривее, чем бумеранг, но… он работает. Разумеется, существуют и другие решения.
Подробное их обсуждение далеко выходит за рамки данной книги и требует глубоко знания Си++ (гораздо более глубокого, чем у рядового разработчика), к тому же это заняло бы слишком много места… но едва ли оказалось интересно рядовому читателю.
Итак, идентификация виртуального конструктора в силу отсутствия самого понятия – в принципе невозможна. Его эмуляция насчитывает десятки решений (если не больше), – попробуй-ка, перечисли их все! Впрочем, этого и не нужно делать – в большинстве случаев виртуальные конструкторы представляют собой виртуальные функции, принимающие в качестве аргумента указатель this и возвращающие указатель на новый объект.
Не слишком- то надежно для идентификации, но все же лучше, чем ничего.
::конструктор раз, конструктор два… Количество конструкторов объекта может быть и более одного (и очень часто не только может, но и бывает). Однако это никак не влияет на анализ. Сколько бы конструкторов ни присутствовало, – для каждого экземпляра объекта всегда вызывается только один, выбранный компилятором в зависимости от формы объявления объекта. Единственная деталь – различные экземпляры объекта могут вызывать различные конструкторы – будьте внимательны!
::а зачем козе баян или внимание: пустой конструктор. Некоторые ограничения конструктора (в частности, отсутствие возвращаемого значения) привели к появлению стиля программирования "пустой конструктор". Конструктор умышленно оставляется пустым, а весь код инициализации помещается в специальную функцию-член, как правило, называемую Init. Обсуждение сильных и слабых сторон такого стиля – предмет отдельного разговора, никаким боком не относящегося к данной книге. Исследователям достаточно знать – такой стиль есть и активно используется не только отдельными индивидуальными программистами, но и крупнейшими компаниями-гигантами (например, той же Microsoft). Поэтому, встретив вызов пустого конструктора, – не удивляйтесь, - это нормально, и ищите функцию инициализации среди обычных членов.