# 第31章 類
## 31.1 **簡單的例子**
在程序內部,C++類的表示基本和結構體一樣。 讓我們試試這個有2個變量,2個構造函數和1個方法的類。
```
#include <stdio.h>
class c
{
private:
int v1;
int v2;
public:
c() // default ctor
{
v1=667;
v2=999;
};
c(int a, int b) // ctor
{
v1=a;
v2=b;
};
void dump()
{
printf ("%d; %d
", v1, v2);
};
};
int main()
{
class c c1;
class c c2(5,6);
c1.dump();
c2.dump();
return 0;
};
```
**31.1.1 MSVC-X86**
這里可以看到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個字節。這正好是2個變量存儲所需的大小。 對c1來說一個默認的無參數構造函數??0c@@QAE@XZ會被調用。對c2來說另一個??0c@@QAE@HH@Z會被調用,有兩個數字會被作為參數傳遞。 指向對象的指針(c++術語的“this”)會被通過ECX寄存器傳遞。這被叫做thiscall(31.1.1)--這是一個指向對象的指針傳遞方式。 MSVC使用ECX來傳遞它。無需說明的是,它并不是一個標準化的方法,其他編譯器可能用其他方法,例如通過第一個函數參數,比如GCC就是這么做的。 為什么函數的名字這么奇怪?這是因為名字打碎方式的緣故。 C++類可能有多個同名的重載函數,因此,不同的類也可能有相同的函數名。 名字打碎可以把類的類名+函數名+參數類型編碼到一個字符串里面,然后它就會被用作內部名稱。這完全是因為編譯器和DLL OS加載器都 不知道C++或者面向對象的緣故。 Dump()函數在之后被調用了2次。 讓我們看看構造函數的代碼。
```
_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中存儲的指向結構體的指針,然后把指針指向自己的本地變量,但是,這個操作并不是必須的。 對C++標準來說我們知道構造函數不應該返回任何值。事實上,構造函數會返回指向新創建對象的指針,比如“this”。 現在看看dump()函數:
```
_this$ = -4 ; size = 4
?dump@c@@QAEXXZ PROC ; c::dump, 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 [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
mov esp, ebp
pop ebp
ret 0
?dump@c@@QAEXXZ ENDP ; c::dump
```
簡單的可以:dump()會把帶有2個int的結構體傳給ecx,然后從他里面取出2個值,然后傳給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
```
還要說的就是棧指針在調用add esp ,x之后并不正確。所以構造函數還需要ret 8來返回,而不是ret。 這是因為這兒調用方式是thiscall(31.1.1),這個方法會使用棧來傳遞參數,和stdcall對比(47.2)來看,他將為被調用者維護正確的棧,而不是調用者。Ret x指令會額外的給esp加上x,然后會把控制流交還給調用者函數。 調用轉換見47章。 還有需要注意的是,編譯器會決定什么時候調用構造函數什么時候調用析構函數,但是我們從c++語言基礎里面已經知道調用時機了。
**31.1.2 MSVC-x86-64**
像我們已經知道的那樣,x86-64中前4個函數參數是通過RCX/RDX/R8/R9寄存器傳遞的,剩余的通過棧傳遞。但是this是用RCX傳遞的 ,而第一個函數參數是從RDX開始傳遞的。我們可以通過c(int a, int b)這個函數看出來。
```
; void dump()
?dump@c@@QEAAXXZ PROC ; c::dump
mov r8d, DWORD PTR [rcx+4]
mov edx, DWORD PTR [rcx]
lea rcx, OFFSET FLAT:??_C@_07NJBDCIEC@?$CFd?$DL?5?$CFd?6?$AA@ ; ’%d; %d’
jmp printf
?dump@c@@QEAAXXZ ENDP ; c::dump
; c(int a, int b)
??0c@@QEAA@HH@Z PROC ; c::c
mov DWORD PTR [rcx], edx ; 1st argument: a
mov DWORD PTR [rcx+4], r8d ; 2nd argument: b
mov rax, rcx
ret 0
??0c@@QEAA@HH@Z ENDP ; c::c
; default ctor
??0c@@QEAA@XZ PROC ; c::c
mov DWORD PTR [rcx], 667 ; 0000029bH
mov DWORD PTR [rcx+4], 999 ; 000003e7H
mov rax, rcx
ret 0
??0c@@QEAA@XZ ENDP ; c::c
```
X64中,Int數據類型依然是32位的。所以這里也使用了32位寄存器部分。 我們還可以看到dump()里的JMP printf,而不是RET,這個技巧我們已經在11.1.1里面見過了。
**31.1.3 GCC-x86**
幾乎和GCC4.4.1一樣的結果,除了幾個例外。
```
public main
main proc near ; DATA XREF: _start+17
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
```
我們可以看到另一個命名破碎模式,這個GNU特殊的模式可以看到指向對象的this時針其實是作為函數的第一個參數被傳入的,當然,這個對程序員來說是透明的。 第一個構造函數:
```
public _ZN1cC1Ev ; weak
_ZN1cC1Ev proc near ; CODE XREF: main+10
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
"
call _printf
leave
retn
_ZN1c4dumpEv endp
```
在這個函數的內部表達中有一個單獨的參數,被用作指向當前對象,也即this。 因此,如果從這些簡單的例子來看,MSVC和GCC的區別也就只有函數名編碼的區別和傳入this指針的區別(ECX寄存器或通過第一個參數)。
**31.1.14 GCC-X86-64**
前6個參數,會通過RDI/RSI/RDX/RCX/R8/R9[21章]的順序傳遞,this指針會通過第一個RDI來傳遞,我們可以接著看到。 Int數據類型也是一個32位的數據,JMP替換RET的技巧這里也用到了。
```
; default ctor
_ZN1cC2Ev:
mov DWORD PTR [rdi], 667
mov DWORD PTR [rdi+4], 999
ret
; c(int a, int b)
_ZN1cC2Eii:
mov DWORD PTR [rdi], esi
mov DWORD PTR [rdi+4], edx
ret
; dump()
_ZN1c4dumpEv:
mov edx, DWORD PTR [rdi+4]
mov esi, DWORD PTR [rdi]
xor eax, eax
mov edi, OFFSET FLAT:.LC0 ; "%d; %d
"
jmp printf
```
## 31.2 類繼承
可以說關于類繼承就是我們已經研究了的這個結構體,但是它現在擴展成類了。 讓我們看個簡單的例子:
```
#include <stdio.h>
class object
{
public:
int color;
object() { };
object (int color) { this->color=color; };
void print_color() { printf ("color=%d
", color); };
};
class box : public object
{
private:
int width, height, depth;
public:
box(int color, int width, int height, int depth)
{
this->color=color;
this->width=width;
this->height=height;
this->depth=depth;
};
void dump()
{
printf ("this is box. color=%d, width=%d, height=%d, depth=%d
", color, width,
height, depth);
};
};
class sphere : public object
{
private:
int radius;
public:
sphere(int color, int radius)
{
this->color=color;
this->radius=radius;
};
void dump()
{
printf ("this is sphere. color=%d, radius=%d
", color, radius);
};
};
int main()
{
box b(1, 10, 20, 30);
sphere s(2, 40);
b.print_color();
s.print_color();
b.dump();
s.dump();
return 0;
};
```
讓我們觀察一下生成的dump()的代碼和object::print_color(),讓我們看看結構體對象的內存輸出(作為32位代碼) 所以,dump()方法其實是對應了好幾個類,下面代碼由MSVC 2008生成(/Ox+/Ob0) 優化的MSVC 2008 /Ob0
```
??_C@_09GCEDOLPA@color?$DN?$CFd?6?$AA@ DB ’color=%d’, 0aH, 00H ; ‘string’
?print_color@object@@QAEXXZ PROC ; object::print_color, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx]
push eax
; ’color=%d’, 0aH, 00H
push OFFSET ??_C@_09GCEDOLPA@color?$DN?$CFd?6?$AA@
call _printf
add esp, 8
ret 0
?print_color@object@@QAEXXZ ENDP ; object::print_color
優化的MSVC2008 /Ob0
?dump@box@@QAEXXZ PROC ; box::dump, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx+12]
mov edx, DWORD PTR [ecx+8]
push eax
mov eax, DWORD PTR [ecx+4]
mov ecx, DWORD PTR [ecx]
push edx
push eax
push ecx
; ’this is box. color=%d, width=%d, height=%d, depth=%d’, 0aH, 00H ; ‘string’
push OFFSET ??_C@_0DG@NCNGAADL@this?5is?5box?4?5color?$DN?$CFd?0?5width?$DN?$CFd?0@
call _printf
add esp, 20 ; 00000014H
ret 0
?dump@box@@QAEXXZ ENDP ; box::dump
?dump@sphere@@QAEXXZ PROC ; sphere::dump, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx+4]
mov ecx, DWORD PTR [ecx]
push eax
push ecx
; ’this is sphere. color=%d, radius=%d’, 0aH, 00H
push OFFSET ??_C@_0CF@EFEDJLDC@this?5is?5sphere?4?5color?$DN?$CFd?0?5radius@
call _printf
add esp, 12 ; 0000000cH
ret 0
?dump@sphere@@QAEXXZ ENDP ; sphere::dump
```
所以,這就是他的內存暑促后: (基類對象)

繼承的對象 Box:

Sphere:

讓我們看看main()函數體:
```
PUBLIC _main
_TEXT SEGMENT
_s$ = -24 ; size = 8
_b$ = -16 ; size = 16
_main PROC
sub esp, 24 ; 00000018H
push 30 ; 0000001eH
push 20 ; 00000014H
push 10 ; 0000000aH
push 1
lea ecx, DWORD PTR _b$[esp+40]
call ??0box@@QAE@HHHH@Z ; box::box
push 40 ; 00000028H
push 2
lea ecx, DWORD PTR _s$[esp+32]
call ??0sphere@@QAE@HH@Z ; sphere::sphere
lea ecx, DWORD PTR _b$[esp+24]
call ?print_color@object@@QAEXXZ ; object::print_color
lea ecx, DWORD PTR _s$[esp+24]
call ?print_color@object@@QAEXXZ ; object::print_color
lea ecx, DWORD PTR _b$[esp+24]
call ?dump@box@@QAEXXZ ; box::dump
lea ecx, DWORD PTR _s$[esp+24]
call ?dump@sphere@@QAEXXZ ; sphere::dump
xor eax, eax
add esp, 24 ; 00000018H
ret 0
_main ENDP
```
繼承的類必須永遠將它們的范圍添加到基類的范圍中,所以這樣可以讓基類的方法對其范圍生效。 當object::print_color()方法被調用時,會有一個指針指向box對象和sphere對象會被傳遞進去,它就是“this”。它可以和這些對象簡單的互動,因為color域指向的永遠是固定的地址(+0x00偏移)。 可以說,object::print_color()方法對于輸入對象類型來說是不可知的,如果你創建一個繼承類,例如繼承了box類編譯器會自動在depth域之后加上新域,而把box的類域固定在一個固定的位置。 因此,box::dump()方法會在訪問color/width/height/depths的時候順利工作,因為地址的固定,它會很容易的知道偏移。 GCC生成的代碼基本一樣,只有一個不一樣的就是this的傳遞,就像之前說的一樣,它是作為第一個參數傳遞的,而不是通過ECX傳遞的。
## 31.3 封裝
封裝是一個把數據裝在類的private域里面的動作,這樣會讓它們只能從類的內部被訪問到,而從外面訪問不到。 但是,生成的代碼里面是否有什么東西指示一個變量是private呢? 沒有,讓我們看看簡單的例子:
```
#include <stdio.h>
class box
{
private:
int color, width, height, depth;
public:
box(int color, int width, int height, int depth)
{
this->color=color;
this->width=width;
this->height=height;
this->depth=depth;
};
void dump()
{
printf ("this is box. color=%d, width=%d, height=%d, depth=%d
", color, width,
height, depth);
};
};
```
在MSVC 2008+/Ox和/Ob0選項,然后看看box::dump()代碼:
```
?dump@box@@QAEXXZ PROC ; box::dump, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx+12]
mov edx, DWORD PTR [ecx+8]
push eax
mov eax, DWORD PTR [ecx+4]
mov ecx, DWORD PTR [ecx]
push edx
push eax
push ecx
; ’this is box. color=%d, width=%d, height=%d, depth=%d’, 0aH, 00H
push OFFSET ??_C@_0DG@NCNGAADL@this?5is?5box?4?5color?$DN?$CFd?0?5width?$DN?$CFd?0@
call _printf
add esp, 20 ; 00000014H
ret 0
?dump@box@@QAEXXZ ENDP ; box::dump
```
這就是類的內存分布:

所有域都不允許其他類的訪問,但是,我們知道這個存放方式之后是否可以修改這些域? 所以我加了hack_oop_encapsulation()函數,假設他有這個代碼,當然我們沒有編譯:
```
void hack_oop_encapsulation(class box * o)
{
o->width=1; // that code can’t be compiled: "error C2248: ’box::width’ : cannot access
private member declared in class ’box’"
};
```
還有,如果要轉換box的類型,把它從指針轉為int數組,然后如果我們能修改這些數字,那么我們就成功了。
```
void hack_oop_encapsulation(class box * o)
{
unsigned int *ptr_to_object=reinterpret_cast<unsigned int*>(o);
ptr_to_object[1]=123;
};
```
這個函數的代碼非常簡單,剋說函數指示把指針指向這些int,然后把123寫入第二個int:
```
?hack_oop_encapsulation@@YAXPAVbox@@@Z PROC ; hack_oop_encapsulation
mov eax, DWORD PTR _o$[esp-4]
mov DWORD PTR [eax+4], 123 ; 0000007bH
ret 0
?hack_oop_encapsulation@@YAXPAVbox@@@Z ENDP ; hack_oop_encapsulation
```
看看它是怎么工作的:
```
int main()
{
box b(1, 10, 20, 30);
b.dump();
hack_oop_encapsulation(&b);
b.dump();
return 0;
};
```
運行后:
```
this is box. color=1, width=10, height=20, depth=30
this is box. color=1, width=123, height=20, depth=30
```
可以看到,private只是在編譯階段被保護了,c++編譯器不會允許其他代碼修改private域下的內容,但是如果用一些技巧,就可以修改private的值。
## 31.4 多重繼承
多重繼承是一個類的創建,這個類會從2個或多個類里面繼承函數和成員。 看一個簡單的例子:
```
#include <stdio.h>
class box
{
public:
int width, height, depth;
box() { };
box(int width, int height, int depth)
{
this->width=width;
this->height=height;
this->depth=depth;
};
void dump()
{
printf ("this is box. width=%d, height=%d, depth=%d
", width, height, depth);
};
int get_volume()
{
return width * height * depth;
};
};
class solid_object
{
public:
int density;
solid_object() { };
solid_object(int density)
{
this->density=density;
};
int get_density()
{
return density;
};
void dump()
{
printf ("this is solid_object. density=%d
", density);
};
};
class solid_box: box, solid_object
{
public:
solid_box (int width, int height, int depth, int density)
{
this->width=width;
this->height=height;
this->depth=depth;
this->density=density;
};
void dump()
{
printf ("this is solid_box. width=%d, height=%d, depth=%d, density=%d
", width,
height, depth, density);
};
int get_weight() { return get_volume() * get_density(); };
};
int main()
{
box b(10, 20, 30);
solid_object so(100);
solid_box sb(10, 20, 30, 3);
b.dump();
so.dump();
sb.dump();
printf ("%d
", sb.get_weight());
return 0;
};
```
讓我們在MSVC 2008中用/Ox和/Ob0選項來編譯,然后看看box::dump()、solid_object::dump()和solid_box::dump()的函數代碼:
```
?dump@box@@QAEXXZ PROC ; box::dump, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx+8]
mov edx, DWORD PTR [ecx+4]
push eax
mov eax, DWORD PTR [ecx]
push edx
push eax
; ’this is box. width=%d, height=%d, depth=%d’, 0aH, 00H
push OFFSET ??_C@_0CM@DIKPHDFI@this?5is?5box?4?5width?$DN?$CFd?0?5height?$DN?$CFd@
call _printf
add esp, 16 ; 00000010H
ret 0
?dump@box@@QAEXXZ ENDP ; box::dump
?dump@solid_object@@QAEXXZ PROC ; solid_object::dump, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx]
push eax
; ’this is solid_object. density=%d’, 0aH
push OFFSET ??_C@_0CC@KICFJINL@this?5is?5solid_object?4?5density?$DN?$CFd@
call _printf
add esp, 8
ret 0
?dump@solid_object@@QAEXXZ ENDP ; solid_object::dump
?dump@solid_box@@QAEXXZ PROC ; solid_box::dump, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx+12]
mov edx, DWORD PTR [ecx+8]
push eax
mov eax, DWORD PTR [ecx+4]
mov ecx, DWORD PTR [ecx]
push edx
push eax
push ecx
; ’this is solid_box. width=%d, height=%d, depth=%d, density=%d’, 0aH
push OFFSET ??_C@_0DO@HNCNIHNN@this?5is?5solid_box?4?5width?$DN?$CFd?0?5hei@
call _printf
add esp, 20 ; 00000014H
ret 0
?dump@solid_box@@QAEXXZ ENDP ; solid_box::dump
```
所以,這三個類的內存分布是:
Box:

Solid_object:

可以說,solid_box的類內存空間就是它們的組合:

Box::get_volume()和solid_object::get_density()函數的代碼如下:
```
?get_volume@box@@QAEHXZ PROC ; box::get_volume, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx+8]
imul eax, DWORD PTR [ecx+4]
imul eax, DWORD PTR [ecx]
ret 0
?get_volume@box@@QAEHXZ ENDP ; box::get_volume
?get_density@solid_object@@QAEHXZ PROC ; solid_object::get_density, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx]
ret 0
?get_density@solid_object@@QAEHXZ ENDP ; solid_object::get_density
```
但是solid_box::get_weight()的代碼更有趣:
```
?get_weight@solid_box@@QAEHXZ PROC ; solid_box::get_weight, COMDAT
; _this$ = ecx
push esi
mov esi, ecx
push edi
lea ecx, DWORD PTR [esi+12]
call ?get_density@solid_object@@QAEHXZ ; solid_object::get_density
mov ecx, esi
mov edi, eax
call ?get_volume@box@@QAEHXZ ; box::get_volume
imul eax, edi
pop edi
pop esi
ret 0
?get_weight@solid_box@@QAEHXZ ENDP ; solid_box::get_weight
```
Get_weight()函數只會調用2個函數,但是對于get_volume()來說,他只是傳遞指針給this,對get_density()來說,他指示傳遞指針給this,同時移位12(0xC)字節,然后在solid_box類的內存空間理,solid_object類開始了。 因此,solid_object::get_density()方法相信它正在處理普通的solid_object類,而且box::get_volume類將對它的3個域生效,而且相信這是普通的box類對象。 因此,我們可以說,類的一個對象,是從多個其他類繼承阿日來,在內存中代表著組合起來的類,因為它有所有繼承來的域。每個繼承的方法都會又一個指向對應結構部分的指針來處理。
## 31.5 虛函數
還有一個簡單的例子:
```
#include <stdio.h>
class object
{
public:
int color;
object() { };
object (int color) { this->color=color; };
virtual void dump()
{
printf ("color=%d
", color);
};
};
class box : public object
{
private:
int width, height, depth;
public:
box(int color, int width, int height, int depth)
{
this->color=color;
this->width=width;
this->height=height;
this->depth=depth;
};
void dump()
{
printf ("this is box. color=%d, width=%d, height=%d, depth=%d
", color, width,
height, depth);
};
};
class sphere : public object
{
private:
int radius;
public:
sphere(int color, int radius)
{
this->color=color;
this->radius=radius;
};
void dump()
{
printf ("this is sphere. color=%d, radius=%d
", color, radius);
};
};
int main()
{
box b(1, 10, 20, 30);
sphere s(2, 40);
object *o1=&b;
object *o2=&s;
o1->dump();
o2->dump();
return 0;
};
```
類object有一個虛函數dump(),被box和sphere類繼承者替換。 如果在一個并不知道什么類型是什么對象的環境下,就像在main()這個函數里面一樣,當一個虛函數dump()被調用的時候,我們還是需要知道它的返回類型的。 讓我們在MSVC2008用/Ox 、 /Ob0編譯看看main()的函數代碼:
```
_s$ = -32 ; size = 12
_b$ = -20 ; size = 20
_main PROC
sub esp, 32 ; 00000020H
push 30 ; 0000001eH
push 20 ; 00000014H
push 10 ; 0000000aH
push 1
lea ecx, DWORD PTR _b$[esp+48]
call ??0box@@QAE@HHHH@Z ; box::box
push 40 ; 00000028H
push 2
lea ecx, DWORD PTR _s$[esp+40]
call ??0sphere@@QAE@HH@Z ; sphere::sphere
mov eax, DWORD PTR _b$[esp+32]
mov edx, DWORD PTR [eax]
lea ecx, DWORD PTR _b$[esp+32]
call edx
mov eax, DWORD PTR _s$[esp+32]
mov edx, DWORD PTR [eax]
lea ecx, DWORD PTR _s$[esp+32]
call edx
xor eax, eax
add esp, 32 ; 00000020H
ret 0
_main ENDP
```
指向dump()函數的指針在這個對象的某處被使用了,那么新函數的地址寫到了哪里呢?只有在構造函數中有可能:其他地方都不會被main()調用。 看看類構造函數的代碼:
```
??_R0?AVbox@@@8 DD FLAT:??_7type_info@@6B@ ; box ‘RTTI Type Descriptor’
DD 00H
DB ’.?AVbox@@’, 00H
??_R1A@?0A@EA@box@@8 DD FLAT:??_R0?AVbox@@@8 ; box::‘RTTI Base Class Descriptor at
(0,-1,0,64)’
DD 01H
DD 00H
DD 0ffffffffH
DD 00H
DD 040H
DD FLAT:??_R3box@@8
??_R2box@@8 DD FLAT:??_R1A@?0A@EA@box@@8 ; box::‘RTTI Base Class Array’
DD FLAT:??_R1A@?0A@EA@object@@8
??_R3box@@8 DD 00H ; box::‘RTTI Class Hierarchy Descriptor’
DD 00H
DD 02H
DD FLAT:??_R2box@@8
??_R4box@@6B@ DD 00H ; box::‘RTTI Complete Object Locator’
DD 00H
DD 00H
DD FLAT:??_R0?AVbox@@@8
DD FLAT:??_R3box@@8
??_7box@@6B@ DD FLAT:??_R4box@@6B@ ; box::‘vftable’
DD FLAT:?dump@box@@UAEXXZ
_color$ = 8 ; size = 4
_width$ = 12 ; size = 4
_height$ = 16 ; size = 4
_depth$ = 20 ; size = 4
??0box@@QAE@HHHH@Z PROC ; box::box, COMDAT
; _this$ = ecx
push esi
mov esi, ecx
call ??0object@@QAE@XZ ; object::object
mov eax, DWORD PTR _color$[esp]
mov ecx, DWORD PTR _width$[esp]
mov edx, DWORD PTR _height$[esp]
mov DWORD PTR [esi+4], eax
mov eax, DWORD PTR _depth$[esp]
mov DWORD PTR [esi+16], eax
mov DWORD PTR [esi], OFFSET ??_7box@@6B@
mov DWORD PTR [esi+8], ecx
mov DWORD PTR [esi+12], edx
mov eax, esi
pop esi
ret 16 ; 00000010H
??0box@@QAE@HHHH@Z ENDP ; box::box
```
我們可以看到一些輕微的內存布局的變化:第一個域是一個指向box::`vftable`(這個名字由MSVC編譯器生成)的指針。 在這個函數表里我們看到了一個指向box::`RTTI Complete Object Locator`的連接,而且還有一個指向box::dump()函數的。所以這就是被命名的虛函數表和RTTI。虛函數表可以包含所有虛函數體的地址,RTTI表包含類型的信息。另外一提,RTTI表是c++調用dynamic_cast和typeid的結果的枚舉表。你可以看到這里函數名是用明文表記的。因此,一個基對象可以調用虛函數object::dump(),然后,會從這個對象的結構里調用這個繼承類的函數。 枚舉這些函數表需要消耗額外的CPU時間,所以可以認為虛函數比普通調用要慢一些。 在GCC生成的代碼里,RTTI表的構造有些輕微的不同。
- 第一章 CPU簡介
- 第二章 Hello,world!
- 第三章? 函數開始和結束
- 第四章 棧
- Chapter 5 printf() 與參數處理
- Chapter 6 scanf()
- CHAPER7 訪問傳遞參數
- Chapter 8 一個或者多個字的返回值
- Chapter 9 指針
- Chapter 10 條件跳轉
- 第11章 選擇結構switch()/case/default
- 第12章 循環結構
- 第13章 strlen()
- Chapter 14 Division by 9
- chapter 15 用FPU工作
- Chapter 16 數組
- Chapter 17 位域
- 第18章 結構體
- 19章 聯合體
- 第二十章 函數指針
- 第21章 在32位環境中的64位值
- 第二十二章 SIMD
- 23章 64位化
- 24章 使用x64下的SIMD來處理浮點數
- 25章 溫度轉換
- 26章 C99的限制
- 27章 內聯函數
- 第28章 得到不正確反匯編結果
- 第29章 花指令
- 第30章 16位Windows
- 第31章 類
- 三十二 ostream