Классы в Си++
From Wiki.conus.info
Я преднамеренно расположил описание классов сразу за структурами здесь, потому что класс в Си++ это почти то же что и структура.
Давайте попробуем простой пример с парой переменных, парой конструкторов и одним методом:
#include <stdio.h> class c { private: int v1; int v2; public: c() // ctor { v1=667; v2=999; }; c(int a, int b) // ctor { v1=a; v2=b; }; void dump() { printf ("%d; %d\n", v1, v2); }; }; int main() { class c c1; class c c2(5,6); c1.dump(); c2.dump(); return 0; };
Вот как выглядит _main() на ассемблере:
_c2$ = -16 ; size = 8 _c1$ = -8 ; size = 8 _main PROC push ebp mov ebp, esp sub esp, 16 ; 00000010H lea ecx, DWORD PTR _c1$[ebp] call ??0c@@QAE@XZ ; c::c push 6 push 5 lea ecx, DWORD PTR _c2$[ebp] call ??0c@@QAE@HH@Z ; c::c lea ecx, DWORD PTR _c1$[ebp] call ?dump@c@@QAEXXZ ; c::dump lea ecx, DWORD PTR _c2$[ebp] call ?dump@c@@QAEXXZ ; c::dump xor eax, eax mov esp, ebp pop ebp ret 0 _main ENDP
Вот что происходит. Под каждый экземпляр класса c выделяется по 8 байт, столько, сколько им нужно для хранения пары переменных.
Для c1 вызывается дефолтный конструктор без аргументов ??0c@@QAE@XZ. Для c2 вызывается конструктор ??0c@@QAE@HH@Z и передаются два числа в качестве аргументов. А указатель на объект передается в регистре ECX. Это называется thiscall - метод передачи указателя на объект. В данном случае, MSVC делает это через ECX. Необходимо помнить, что это особо не стандартизировано, и другие компиляторы могут делать это иначе, например через первый аргумент (как GCC).
Вы спросите, почему у имен функций такие странные имена? Это name mangling:
http://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B_Name_Mangling
Как известно, в Си++, у класса, может иметься несколько методов с одинаковыми именами но аргументами разных типов. Ну и конечно, у разных классов могут быть методы с одинаковыми именами.
Name mangling позволяет закодировать имя класса + имя метода + типы всех аргументов в одной строке, которая затем используется как имя функции. Это все потому что ни линкер, ни загрузчик DLL операционной системы (мангленные имена могут быть экспортами/импортами в DLL), не знают ничего об ООП.
Далее вызывается два раза dump().
Теперь смотрим что там в конструкторах:
_this$ = -4 ; size = 4 ??0c@@QAE@XZ PROC ; c::c, COMDAT ; _this$ = ecx push ebp mov ebp, esp push ecx mov DWORD PTR _this$[ebp], ecx mov eax, DWORD PTR _this$[ebp] mov DWORD PTR [eax], 667 ; 0000029bH mov ecx, DWORD PTR _this$[ebp] mov DWORD PTR [ecx+4], 999 ; 000003e7H mov eax, DWORD PTR _this$[ebp] mov esp, ebp pop ebp ret 0 ??0c@@QAE@XZ ENDP ; c::c _this$ = -4 ; size = 4 _a$ = 8 ; size = 4 _b$ = 12 ; size = 4 ??0c@@QAE@HH@Z PROC ; c::c, COMDAT ; _this$ = ecx push ebp mov ebp, esp push ecx mov DWORD PTR _this$[ebp], ecx mov eax, DWORD PTR _this$[ebp] mov ecx, DWORD PTR _a$[ebp] mov DWORD PTR [eax], ecx mov edx, DWORD PTR _this$[ebp] mov eax, DWORD PTR _b$[ebp] mov DWORD PTR [edx+4], eax mov eax, DWORD PTR _this$[ebp] mov esp, ebp pop ebp ret 8 ??0c@@QAE@HH@Z ENDP ; c::c
Все вроде бы и понятно: конструкторы просто как функции, используют указатель на структуру в ECX, перекладывают его себе в локальную переменную, хотя это и не обязательно.
И еще метод dump():
_this$ = -4 ; size = 4 ?dump@c@@QAEXXZ PROC ; c::dump, COMDAT ; _this$ = ecx ; Line 22 push ebp mov ebp, esp push ecx mov DWORD PTR _this$[ebp], ecx ; Line 23 mov eax, DWORD PTR _this$[ebp] mov ecx, DWORD PTR [eax+4] push ecx mov edx, DWORD PTR _this$[ebp] mov eax, DWORD PTR [edx] push eax push OFFSET ??_C@_07NJBDCIEC@?$CFd?$DL?5?$CFd?6?$AA@ call _printf add esp, 12 ; 0000000cH ; Line 24 mov esp, ebp pop ebp ret 0 ?dump@c@@QAEXXZ ENDP ; c::dump
Все очень просто, dump() берет указатель на структуру состоящую из двух int через ECX, выдергивает оттуда две переменных и передает их в printf().
А если скомпилировать с оптимизацией (/Ox), то будет намного меньше всего:
??0c@@QAE@XZ PROC ; c::c, COMDAT ; _this$ = ecx mov eax, ecx mov DWORD PTR [eax], 667 ; 0000029bH mov DWORD PTR [eax+4], 999 ; 000003e7H ret 0 ??0c@@QAE@XZ ENDP ; c::c _a$ = 8 ; size = 4 _b$ = 12 ; size = 4 ??0c@@QAE@HH@Z PROC ; c::c, COMDAT ; _this$ = ecx mov edx, DWORD PTR _b$[esp-4] mov eax, ecx mov ecx, DWORD PTR _a$[esp-4] mov DWORD PTR [eax], ecx mov DWORD PTR [eax+4], edx ret 8 ??0c@@QAE@HH@Z ENDP ; c::c ?dump@c@@QAEXXZ PROC ; c::dump, COMDAT ; _this$ = ecx mov eax, DWORD PTR [ecx+4] mov ecx, DWORD PTR [ecx] push eax push ecx push OFFSET ??_C@_07NJBDCIEC@?$CFd?$DL?5?$CFd?6?$AA@ call _printf add esp, 12 ; 0000000cH ret 0 ?dump@c@@QAEXXZ ENDP ; c::dump
Вот и все. Единственное о чем еще нужно сказать, это о том что в функции _main, когда вызывался второй конструктор с двумя аргументами, за ним не корректировался стек при помощи "add esp, X". В то же время, у конструктора вместо ret 0 имеется ret 8. Это потому что здесь используется thiscall, который, вместе с stdcall (все это - методы передачи аргументов через стек), предлагает вызываемой функции корректировать стек. Инструкция ret X сначала прибавляет X к ESP, затем передает управление передаваемой функции.
Об этом читайте также: X86 calling conventions.
Еще, кстати, нужно отметить, что именно компилятор решает, когда вызывать конструктор и деструктор.
GCC
В GCC 4.4.1 все почти так же, за исключением некоторых различий.
public main main proc near ; DATA XREF: _start+17�o var_20 = dword ptr -20h var_1C = dword ptr -1Ch var_18 = dword ptr -18h var_10 = dword ptr -10h var_8 = dword ptr -8 push ebp mov ebp, esp and esp, 0FFFFFFF0h sub esp, 20h lea eax, [esp+20h+var_8] mov [esp+20h+var_20], eax call _ZN1cC1Ev mov [esp+20h+var_18], 6 mov [esp+20h+var_1C], 5 lea eax, [esp+20h+var_10] mov [esp+20h+var_20], eax call _ZN1cC1Eii lea eax, [esp+20h+var_8] mov [esp+20h+var_20], eax call _ZN1c4dumpEv lea eax, [esp+20h+var_10] mov [esp+20h+var_20], eax call _ZN1c4dumpEv mov eax, 0 leave retn main endp
Здесь мы видим что применяется иной name mangling (способ кодирования имен функций). Во-вторых, указатель на экземпляр передается как первый аргумент.
Это первый конструктор:
public _ZN1cC1Ev ; weak _ZN1cC1Ev proc near ; CODE XREF: main+10�p arg_0 = dword ptr 8 push ebp mov ebp, esp mov eax, [ebp+arg_0] mov dword ptr [eax], 667 mov eax, [ebp+arg_0] mov dword ptr [eax+4], 999 pop ebp retn _ZN1cC1Ev endp
Он просто записывает два числа по указателю переданному в первом (и единственном) аргументе.
Второй конструктор:
public _ZN1cC1Eii _ZN1cC1Eii proc near arg_0 = dword ptr 8 arg_4 = dword ptr 0Ch arg_8 = dword ptr 10h push ebp mov ebp, esp mov eax, [ebp+arg_0] mov edx, [ebp+arg_4] mov [eax], edx mov eax, [ebp+arg_0] mov edx, [ebp+arg_8] mov [eax+4], edx pop ebp retn _ZN1cC1Eii endp
Эта функция, аналог которой мог бы выглядеть так:
void ZN1cC1Eii (int *obj, int a, int b) { *obj=a; *(obj+1)=b; };
... что, в общем, логично.
И функция dump():
public _ZN1c4dumpEv _ZN1c4dumpEv proc near var_18 = dword ptr -18h var_14 = dword ptr -14h var_10 = dword ptr -10h arg_0 = dword ptr 8 push ebp mov ebp, esp sub esp, 18h mov eax, [ebp+arg_0] mov edx, [eax+4] mov eax, [ebp+arg_0] mov eax, [eax] mov [esp+18h+var_10], edx mov [esp+18h+var_14], eax mov [esp+18h+var_18], offset aDD ; "%d; %d\n" call _printf leave retn _ZN1c4dumpEv endp
Эта функция "во внутреннем представлении" имеет один аргумент, через который передается указатель на экземпляр класса.
Таким образом, если брать в учет только эти простые примеры, разница между MSVC и GCC в способе кодирования имен функций и передаче указателя на экземпляр класса (через ECX или через первый аргумент).
Еще о name mangling разных компиляторов: