Классы в Си++

From Wiki.conus.info

Jump to: navigation, search

Я преднамеренно расположил описание классов сразу за структурами здесь, потому что класс в Си++ это почти то же что и структура.

Давайте попробуем простой пример с парой переменных, парой конструкторов и одним методом:

#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 разных компиляторов:

http://www.agner.org/optimize/calling_conventions.pdf

Personal tools
Namespaces
Variants
Actions
Navigation
Toolbox