W C++ można zdefiniować kilka funkcji o tej samej nazwie
void f(); int f(int n); double f(double x, double y);
Co więc oznaczałoby:
call f
// Funkcja void f(); void f(int x); void f(double x); void f(int * x); double f(int x, double y); double f(int (*fun)(int), int); void Klasa::f(int i, double t); static void Klasa::f(int i); | ; Nazwa
_Z1fv
_Z1fi
_Z1fd
_Z1fPi
_Z1fid
_Z1fPFiiEi
_ZN5Klasa1fEid
_ZN5Klasa1fEi |
Nazwa jest zależna od systemu, kompilatora. Reguły są dość skomplikowane.
Zamiast zgadywać nazwę można zobaczyć jaką nazwę używa kompilator.
g++ -S plik.cpp -o plik.asm
Jeżeli chcemy format bardziej podobny do NASMa to należy dodać opcję -masm=intel.
g++ -masm=intel -S plik.cpp -o plik.asm
void f(); // 1 void f(int x); // 2 void f(double x); // 3 void f(int * x); // 4 double f(int x, double y); // 5 class Klasa { public: void f(int i, double t); // 6 static void f(int i); // 7 }; int main(){ int x=1; double y=1.1; f(); f(x); f(y); f(&x); f(x,y); Klasa k; k.f(x,y); Klasa::f(x); } | main: push ebp mov ebp, esp and esp, -16 sub esp, 32 mov DWORD PTR [esp+24], 1 fld QWORD PTR .LC0 fstp QWORD PTR [esp+16] call _Z1fv ; (1) mov eax, DWORD PTR [esp+24] mov DWORD PTR [esp], eax call _Z1fi ; (2) fld QWORD PTR [esp+16] fstp QWORD PTR [esp] call _Z1fd ; (3) lea eax, [esp+24] mov DWORD PTR [esp], eax call _Z1fPi ; (4) mov eax, DWORD PTR [esp+24] fld QWORD PTR [esp+16] fstp QWORD PTR [esp+4] mov DWORD PTR [esp], eax call _Z1fid ; (5) fstp st(0) mov eax, DWORD PTR [esp+24] fld QWORD PTR [esp+16] fstp QWORD PTR [esp+8] mov DWORD PTR [esp+4], eax lea eax, [esp+31] mov DWORD PTR [esp], eax call _ZN5Klasa1fEid ; (6) mov eax, DWORD PTR [esp+24] mov DWORD PTR [esp], eax call _ZN5Klasa1fEi ; (7) mov eax, 0 leave ret .LC0: .long 2576980378 .long 1072798105 |
Przykładowy plik C++
Odpowiadający mu plik asemblerowy (GAS)
Porównajmy dwa sposoby inicjowania struktury
typedef struct { int a; int b; char c; double d; double e; } Dane;
Dane init(){ Dane d; d.a = 1; d.b = 2; d.c = A; d.d = 1.244; d.e = 2.345; return d; } int main(){ Dane d = init(); return 0; } | void init(Dane * d){ d->a = 1; d->b = 2; d->c = 'A'; d->d = 1.244; d->e = 2.345; } int main(){ Dane d; init(&d); return 0; } |
Niebezpieczeństwo w pierwszym kodzie polega na tym, że pomimo iż do funkcji jest przekazywany adres struktury d,
to w funkcji zostanie utworzony lokalny obiekt i na końcu funkcji będzie potrzebne skopiowanie go.
W C++ nie mamy w funkcji init
jawnego dostępu do obiektu w którym zwracamy wartość.
Obecne kompilatory potrafią wygenerować wydajny kod nawet bez optymalizacji.
Pierwszy program (Pokaż/ukryj kod)
#include <stdio.h> typedef struct { int a; int b; char c; double d; double e; } Dane; Dane init(){ Dane d; d.a = 1; d.b = 2; d.c = 'A'; d.d = 1.244; d.e = 2.345; return d; } int main(){ Dane d = init(); return 0; }
.file "zwracanie_structury.cpp" .intel_syntax noprefix .text .globl _Z4initv .type _Z4initv, @function _Z4initv: .LFB0: .cfi_startproc push ebp .cfi_def_cfa_offset 8 mov ebp, esp .cfi_offset 5, -8 .cfi_def_cfa_register 5 sub esp, 32 mov eax, DWORD PTR [ebp+8] mov DWORD PTR [eax], 1 mov DWORD PTR [eax+4], 2 mov BYTE PTR [eax+8], 65 fld QWORD PTR .LC0 fstp QWORD PTR [eax+12] fld QWORD PTR .LC1 fstp QWORD PTR [eax+20] leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret 4 .cfi_endproc .LFE0: .size _Z4initv, .-_Z4initv .globl main .type main, @function main: .LFB1: .cfi_startproc push ebp .cfi_def_cfa_offset 8 mov ebp, esp .cfi_offset 5, -8 .cfi_def_cfa_register 5 sub esp, 36 lea eax, [ebp-28] mov DWORD PTR [esp], eax call _Z4initv sub esp, 4 mov eax, 0 leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc .LFE1: .size main, .-main .section .rodata .align 8 .LC0: .long 2336462209 .long 1072949100 .align 8 .LC1: .long 1546188227 .long 1073922703 .ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2" .section .note.GNU-stack,"",@progbits
Drugi program (Pokaż/ukryj kod)
#include <stdio.h> typedef struct { int a; int b; char c; double d; double e; } Dane; void init(Dane * d){ d->a = 1; d->b = 2; d->c = 'A'; d->d = 1.244; d->e = 2.345; } int main(){ Dane d; init(&d); return 0; }
.file "zwracanie_structury3.cpp" .intel_syntax noprefix .text .globl _Z4initP4Dane .type _Z4initP4Dane, @function _Z4initP4Dane: .LFB0: .cfi_startproc push ebp .cfi_def_cfa_offset 8 mov ebp, esp .cfi_offset 5, -8 .cfi_def_cfa_register 5 mov eax, DWORD PTR [ebp+8] mov DWORD PTR [eax], 1 mov eax, DWORD PTR [ebp+8] mov DWORD PTR [eax+4], 2 mov eax, DWORD PTR [ebp+8] mov BYTE PTR [eax+8], 65 mov eax, DWORD PTR [ebp+8] fld QWORD PTR .LC0 fstp QWORD PTR [eax+12] mov eax, DWORD PTR [ebp+8] fld QWORD PTR .LC1 fstp QWORD PTR [eax+20] pop ebp .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc .LFE0: .size _Z4initP4Dane, .-_Z4initP4Dane .globl main .type main, @function main: .LFB1: .cfi_startproc push ebp .cfi_def_cfa_offset 8 mov ebp, esp .cfi_offset 5, -8 .cfi_def_cfa_register 5 sub esp, 36 lea eax, [ebp-28] mov DWORD PTR [esp], eax call _Z4initP4Dane mov eax, 0 leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc .LFE1: .size main, .-main .section .rodata .align 8 .LC0: .long 2336462209 .long 1072949100 .align 8 .LC1: .long 1546188227 .long 1073922703 .ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2" .section .note.GNU-stack,"",@progbits
Przekazywanie argumentów przez referencje w C++ jest równoważne w assemblerze przekazywaniu wskaźnika zmiennej.
void fun(int * x) { *x = 1; } void fun(int & x) { x = 1; } int main() { int x = 0; fun(&x); fun(x); } | _Z3funPi: push ebp mov ebp, esp mov eax,[ebp+8] mov [eax], 1 pop ebp ret _Z3funRi: push ebp mov ebp, esp mov eax, [ebp+8] mov [eax], 1 pop ebp ret global main main: push ebp mov ebp, esp sub esp, 20 mov [ebp-4], 0 lea eax, [ebp-4] mov [esp], eax call _Z3funPi lea eax, [ebp-4] mov [esp], eax call _Z3funRi mov eax, 0 leave ret |
Referencje są tylko udogodnieniem w sposobie zapisu, zwłaszcza przy przeciążaniu operatorów np. a + b
zamiast &a + &b
.
Funkcje inline są z założenia krótkimi funkcjami, dla których w miejscu wywołania wkleja się ich ciało zamiast robić skok do podprogramu.
Funkcje te miały zastąpić makra preprocesora.
Funkcje inline poprzedzamy w C++ słowem kluczowym inline
. Jest to jednak tylko sugestia dla kompilatora.
Ciało takiej funkcji kompilator musi znać w momencie kompilacji dlatego najczęściej umieszcza się je w plikach nagłówkowych.
int kwadrat(int x){ return x*x; } inline int kwadrat_inline(int x){ return x*x; } int main(){ int x = 5, y ; y = kwadrat(x); y = kwadrat_inline(x2); } | _Z7kwadrati: push ebp mov ebp, esp mov eax, [ebp+8] imul eax, [ebp+8] pop ebp ret main: push ebp mov ebp, esp and esp, -16 sub esp, 32 mov [esp+28], 5 mov eax, [esp+28] push eax call _Z7kwadrati mov [esp+24], eax mov eax, [esp+28] imul eax, [esp+28] mov [esp+24], eax mov eax, 0 leave ret |
W praktyce kompilator GCC w tym przypadku nie inline'ował funkcji, a przy włączonej optymalizacji po prostu obliczał jej wartość
class A{ public: int x; int y; void f(){ x=1; } }; int main(){ A a a.y = 3; a.f(); } | main: push rbp ; ramka stosu mov rbp, rsp sub rsp, 16 mov DWORD [rbp-12], 3 ; a.y = 3 lea rax, [rbp-16] ; przesylamy adres a mov rdi, rax ; jako pierwszy parametr call _ZN1A1fEv ; metody f mov eax, 0 leave ret |
class A{ public: char ma; void f(){ cout << "\n f w A \n" ; } }; class B : public A { public: int mb; void f(){ cout << "\n f w B \n" ; } }; void check( A * p){ p->ma = 3; p->f(); } | _Z5checkP1A: push rbp mov rbp, rsp mov rax, rdi mov byte [rax], 3 call _ZN1A1fEv leave ret |
class A{ public: char ma; virtual void f(){ cout << "\n f w A \n" ; } }; class B : public A { public: int mb; void f(){ cout << "\n f w B \n" ; } }; void check( A * p){ p->ma = 3; p->f(); } | _Z5checkP1A: push rbp mov rbp, rsp sub rsp, 16 mov rax, rdi mov BYTE [rax+8], 3 mov rax, [rax] mov rdx, [rax] call rdx leave ret |
Jeżeli klasa bazowa i potomna nie mają funkcji wirtualnych to obiekty nie posiadają wskaźników do tablicy funkcji wirtualnych.
class A { public: int a; }; class B : public A { public: int b; }; | A --> +---------+ | a | +---------+ B --> +---------+ | a | +---------+ | b | +---------+ |
Jeżeli w klasie są obecne funkcje wirtualne to obiekt posiada wskaźnik do tablicy funkcji wirtualnych.
class A { public: int a; virtual void v(); }; class B : public A { public: int b; virtual void w(); void f(); };
+-----------------+ | 0 (top_offset) | B ------------------+ typeinfo >+--------+ | wsk do typeinfo |----> dla B | vtable |---> +-----------------+ +--------+ | * A::v() |----> A::v(){} | a | +-----------------+ +--------+ | * B::w() |----> B::w(){} | b | +-----------------+ +--------+
Jeżeli w klasie B nadpiszemy funkcję v to w vtable zmieni się wskaźnik.
class A { public: int a; virtual void v(); }; class B : public A { public: int b; void v(); virtual void w(); void f(); };
+-----------------+ | 0 (top_offset) | B ------------------+ typeinfo >+--------+ | wsk do typeinfo |----> dla B | vtable |---> +-----------------+ +--------+ | * A::v() |--+ A::v(){} | a | +-----------------+ +-> B::v(){} +--------+ | * B::w() |----> B::w(){} | b | +-----------------+ +--------+
class A { public: int a; virtual void v(); }; class B { public: int b; virtual void w(); }; class C : public A, public B { public: int c; };
+-----------------------+ | 0 (top_offset) | +-----------------------+ c --> +----------+ | ptr to typeinfo for C | | vtable |-------> +-----------------------+ +----------+ | A::v() | | a | +-----------------------+ +----------+ | -8 (top_offset) | | vtable |---+ +-----------------------+ +----------+ | | ptr to typeinfo for C | | b | +---> +-----------------------+ +----------+ | B::w() | | c | +-----------------------+ +----------+
class A { public: int a; virtual void v(); }; class B : public A { public: int b; virtual void w(); }; class C : public A { public: int c; virtual void x(); }; class D : public B, public C { public: int d; virtual void y(); };
+-----------------------+ | 0 (top_offset) | +-----------------------+ d --> +----------+ | ptr to typeinfo for D | | vtable |-------> +-----------------------+ +----------+ | A::v() | | a | +-----------------------+ +----------+ | B::w() | | b | +-----------------------+ +----------+ | D::y() | | vtable |---+ +-----------------------+ +----------+ | | -12 (top_offset) | | a | | +-----------------------+ +----------+ | | ptr to typeinfo for D | | c | +---> +-----------------------+ +----------+ | A::v() | | d | +-----------------------+ +----------+ | C::x() | +-----------------------+
class A { public: int a; virtual void v(); }; class B : public virtual A { public: int b; virtual void w(); }; class C : public virtual A { public: int c; virtual void x(); }; class D : public B, public C { public: int d; virtual void y(); };
+-----------------------+ | 20 (vbase_offset) | +-----------------------+ | 0 (top_offset) | +-----------------------+ | ptr to typeinfo for D | +----------> +-----------------------+ d --> +----------+ | | B::w() | | vtable |----+ +-----------------------+ +----------+ | D::y() | | b | +-----------------------+ +----------+ | 12 (vbase_offset) | | vtable |---------+ +-----------------------+ +----------+ | | -8 (top_offset) | | c | | +-----------------------+ +----------+ | | ptr to typeinfo for D | | d | +-----> +-----------------------+ +----------+ | C::x() | | vtable |----+ +-----------------------+ +----------+ | | 0 (vbase_offset) | | a | | +-----------------------+ +----------+ | | -20 (top_offset) | | +-----------------------+ | | ptr to typeinfo for D | +----------> +-----------------------+ | A::v() | +-----------------------+
Realizacja tablic funkcji wirtualnych zależy od tego jakie są klasy bazowe danej klasy. Jest to też często specyficzne dla danego kompilatora
Dobrą prezentację można znaleźć pod adresem
http://tinydrblog.appspot.com/?p=89001
Lokalna kopia strony
Itanium C++ ABI - można tam znaleźć wiele szczegółów implementacyjnych różnych konstrukcji C++.