1. 程式人生 > >C++基礎 類函式

C++基礎 類函式

C++類函式

第1章 引言

類函式的介紹基於《C++之路基礎函式》,對C++的函式再進一步延伸。這裡一個函式有一個歸屬的問題(靜態函式除外)。
此文將介紹類函式最主要的兩點:普通函式與虛擬函式、C++多型實現的基礎(虛擬函式表),所以我們這裡不考慮C++的多繼承和虛繼承。

第2章 函式呼叫

之前提到的類函式區別於普通函式就是:類函式有一個歸屬,C++是通過this指標來實現的。
C++將this指標通過函式的引數傳入類函式中。
為了介紹方便,我們這裡使用兩個簡單的類CA和CB。CB是CA的一個子類。

class CA 
{ 
public: 
int fun
(int x,int y){return 1;} virtual int fun_v1(int x,int y){return x+y;} virtual int fun_v2(int x,int y){return x+y;} }; class CB : public CA { virtual int fun_v1(int x,int y){return 2;} virtual int fun_v2(int x,int y){return 3;} };
//使用程式碼
CA * p = new CB(); 
p->fun(1,2); 
p->fun_v1(1,2);

2.1. 類物件建立

對於上述的程式碼和類,我們首先來分析第一句,類物件的建立。這裡用到了操作符operator new和類的建構函式。雖然是簡簡單單的一句程式碼,其實是C++中較為複雜的部分。
按照以往的方式,我們按照彙編語句展開,用註釋方式來解釋:

CA * p = new CB(); 
003B172E  push 4 //這裡push 4,其實這個4是一個引數,需要傳入operator new(size_t size)
                              //CB的大小就是4,其實就是sizeof(CB)
003B1730  call operator new (3
B11D6h) //呼叫operator new(size_t size);分配4位元組的記憶體 003B1735 add esp,4 //平衡當前函式的棧指標esp; // 這裡的4和push 4意義不同,這裡的4表示是要在棧上分配4個位元組來存放new出來的記憶體地 也即是指標 003B1738 mov dword ptr [ebp-0D4h],eax //將分配的記憶體地址存放在棧上分配的4位元組中 003B173E cmp dword ptr [ebp-0D4h],0 //比較指標p是否為0 003B1745 je wmain+4Ah (3B175Ah) //如果指標p為0就跳轉,不執行建構函式 003B1747 mov ecx,dword ptr [ebp-0D4h] //將指標p賦給ecx,對於每一個類的非靜態函式都會傳入this指標,就是這麼傳入的 003B174D call CB::CB (3B1195h) //呼叫CB的建構函式,會重新分配適合 003B1752 mov dword ptr [ebp-0DCh],eax //將返回結果存放在 ebp-0DCh位置 003B1758 jmp wmain+54h (3B1764h) //跳轉到 0B51764h處程式碼 003B175A mov dword ptr [ebp-0DCh],0 //將 ebp-0DCh位置處賦0 這個語句不會執行到 003B1764 mov eax,dword ptr [ebp-0DCh] //將新的記憶體地址分配給eax 003B176A mov dword ptr [p],eax //也即是將建構函式返回的地址重新賦給指標p,這一步操作的詳細,將在之後的建構函式中詳細展開

2.2. 類普通函式呼叫

類物件構建完成之後,開始呼叫類普通的成員函式:

p->fun(1,2); 
003B176D  push 2 //壓入引數
003B176F  push 1 //壓入引數
003B1771  mov ecx,dword ptr [p]//傳入this指標p   
003B1774  call CA::fun (3B11B8h) //呼叫fun函式

2.3. 虛擬函式呼叫

虛擬函式的呼叫:

p->fun_v1(1,2); 
003B1779  mov esi,esp //
003B177B  push 2 //壓入引數 
003B177D  push 1 //壓入引數 
003B177F  mov eax,dword ptr [p] //將p所指向的值付給eax
003B1782  mov edx,dword ptr [eax] //將地址等於eax處的值付給edx,也就是p指向記憶體地址存放的虛擬函式表的指標
003B1784  mov ecx,dword ptr [p]   //傳入this指標p   
003B1787  mov eax,dword ptr [edx]   // 其實就是將虛擬函式表中第一個函式fun_v1的地址
                                                            //如果呼叫的是p->fun_v2(1,2),那麼這裡應該是 mov eax,dword ptr [edx+4],移動到第二個函式槽
003B1789  call eax //呼叫fun_v1   
003B178B  cmp esi,esp //校驗esp

這裡就是簡單的類物件的生成,以及函式的呼叫和虛擬函式的呼叫

2.4. 記憶體分析

函式的呼叫的展開介紹完了,我們看一下的在這個過程的記憶體:
首先類物件指標p所指向的記憶體地址是0x00976478
0x00976478 40 57 3b 00 cd cd cd cd fd fd fd fd ab ab ab ab ab ab ab ab ee fe ee fe 00 00 00 00 00 00 00 00 f0 e5 d9
看一下前四個位元組 ,其實這個就是一個地址,就是虛擬函式表的地址。
我們看一下虛擬函式表中存了什麼,這裡的第一個四個位元組 ,也是就是一個地址,即第一個虛擬函式的地址
0x003B5740 0a 10 3b 00 35 12 3b 00 90 65 3b 00 18 11 3b 00 30 12 3b 00 a8 65
這裡的前四個位元組0a 10 3b 00轉換成地址就是0x003B100A。我們看一下“2.3 虛擬函式呼叫”中0x003B1789處的彙編語句(003B1789 call eax),再往裡面跳轉便是程式碼段(003B100A jmp CB::fun_v1 (3B1870h)),此程式碼段的地址與虛擬函式表中存放的第一個虛擬函式地址吻合。

第3章 函式詳解

C++中的建構函式,可以說是C++類中最為複雜的一部分。接下來,將詳細介紹建構函式。我們對第二章中的兩個函式修改。
我們把建構函式與虛擬函式放在一起解釋,是因為有關於虛擬函式表。

class CA 
{ 
protected: 
int m_x0; 

public: 
CA(){m_x0 = 0;} 
virtual ~CA(){} 
int fun(int x,int y){return 1;} 
virtual int fun_v1(int x,int y){return x+y;} 
virtual int fun_v2(int x,int y){return 1;} 
}; 

class CB : public CA 
{ 
public: 
CB(){m_x = 1;} 
~CB(){} 
int m_x; 
virtual int fun_v1(int x,int y) 
{ 
int z = m_x0 + x; 
z += m_x; 
return z; 
} 
virtual int fun_v2(int x,int y){return 3;} 
};

對兩個類新增成員變數,並加上建構函式和解構函式。

3.1. 建構函式

我們首先來看一下建構函式,建構函式應該是類中最複雜的函數了,他做了當前類以及父類的初始化工作;
我們來看一下它的彙編程式碼(去除頭和尾部)
CB(){m_x = 1;}

013618EF pop ecx //獲取this指標,這裡有些不符,如果是按照之前呼叫之前,this指標存放在了ecx,這裡應該是可以直接用的

013618F0  mov dword ptr [ebp-8],ecx //分配棧控制元件存放this指標
013618F3  mov ecx,dword ptr [this]  //分獲取this指向的內容
013618F6  call CA::CA (28100Ah)
CA::CA()//CA的建構函式也在這裡展開了
{
0136195F   pop ecx   //獲取this指標, 
01361960  mov dword ptr [ebp-8],ecx   //分配棧控制元件存放this指標 
01361963  mov eax,dword ptr [this]   //分獲取this指向的內容 
01361966 mov dword ptr [eax],offset CA::`vftable' (1366754h)     //將CA的虛擬函式表的地址存放在地址為eax的地方,也就是this指向的地方
0136196C  mov eax,dword ptr [this] //將this指向的值付給eax,也就是將虛擬函式表的地址存放在eax
0136196F  mov dword ptr [eax+4],0 //將虛擬函式表地址的下四個位元組賦0,也是 m_x0 = 0; 
01361976  mov eax,dword ptr [this] //將this指標指向的地址賦給eax,作為返回值
  }

013618FB  mov eax,dword ptr [this]  //將this指向的值付給eax,也就是將虛擬函式表的地址存放在eax 
013618FE mov dword ptr [eax],offset CB::`vftable' (1366740h)   //將CB的虛擬函式表的地址存放在地址為eax的地方,也就是this指向的地方
                                                     //其實這裡將虛擬函式表的地址覆蓋了,從用CA的虛擬函式表改為用CB的虛擬函式表
01361904  mov eax,dword ptr [this]  //將this指向的值付給eax,也就是將虛擬函式表的地址存放在eax 
01361907  mov dword ptr [eax+8],1  //將虛擬函式表地址的第二個四個位元組賦1,也就是 m_x = 1; 
0136190E  mov eax,dword ptr [this]  //將this指標指向的地址賦給eax,作為返回值 
...  

3.2. 記憶體分析

我們再從記憶體角度看一下這個過程,從ebp-8為 0x0036F94C,ebp-8就是存放了this指標
0x0036F94C a0 66 22 00 cc cc cc cc 34 fa 36 00 fb 18 36 01 34 fb 36 00 00 00
從上述記憶體的前四個位元組,獲取this指標0x002266a0,這個地址所在的記憶體中存放的內容:
0x002266A0 cd cd cd cd cd cd cd cd cd cd cd cd fd fd fd fd ab ab
程式碼段(01361966 mov dword ptr [eax],offset CA::`vftable’ (1366754h))執行之後,地址0x002266A0處的記憶體放生了變化:
0x002266A0 54 67 36 01 cd cd cd cd cd cd cd cd fd fd fd fd ab ab
再來看一下這段記憶體前四個位元組(54 67 36 01)轉換成地址就是0x0x01366754,這個地址就是CA虛擬函式表的地址。

程式碼段(013618FE mov dword ptr [eax],offset CB::`vftable’ (1366740h))這執行之後的變化,再觀察0x002266A0處的記憶體:
0x002266A0 40 67 36 01 00 00 00 00 cd cd cd cd fd fd fd fd ab ab
這裡虛擬函式表的地址變成了0x01366740,這個地址就是CB的虛擬函式表,this指向的地址變成了這樣:
0x002266A0 40 67 36 01 00 00 00 00 01 00 00 00 fd fd fd fd ab ab

對於虛擬函式表指向的地址的記憶體,這裡就不展開了,裡面存放的就是類每一個虛擬函式的地址。還有0x002266A0 40 67 36 01 00 00 00 00 01 00 00 00 fd fd fd fd ab ab,高亮部分等到多繼承時候再詳細解釋。

3.3. 虛擬函式

//虛擬函式
virtual int fun_v1(int x,int y) 
{ 
...   
003B1ADF pop ecx    //獲取this指標, 
003B1AE0 mov dword ptr [ebp-8],ecx //存放this指標在棧上
int z = m_x0 + x; 
003B1AE3 mov eax,dword ptr [this] //存放this指向的地址存放在eax
003B1AE6 mov ecx,dword ptr [eax+4]  //取this指向地址的後四個位元組,其實就是去m_x0的值
003B1AE9 add ecx,dword ptr [x]  //和傳入的x相加
003B1AEC mov dword ptr [z],ecx //將m_x0 + x儲存在z中,這裡z是在函式的棧空間,這裡就不展開了
z += m_x; 
003B1AEF mov eax,dword ptr [this]  // 存放this指向的地址存放在eax 
003B1AF2 mov ecx,dword ptr [z]   //將z的值存放在ecx中
003B1AF5 add ecx,dword ptr [eax+8] // 取this指向地址的後第二個四位元組,其實就是取m_x,然後和ecx中的值相加
003B1AF8 mov dword ptr [z],ecx //將值存放在z中
return z; 
003B1AFB mov eax,dword ptr [z] //將z中的值放入eax中,作為返回值
}

3.4. 解構函式

//delete p
delete p; 
009C180C mov eax,dword ptr [ebp-14h] //獲取p的值
009C180F mov dword ptr [ebp-0E0h],eax //分配臨時棧控制元件存放this
009C1815 mov ecx,dword ptr [ebp-0E0h] //又將棧空間的this存放在ecx中
009C181B mov dword ptr [ebp-0ECh],ecx //有一次分配臨時棧空間存放ecx中的值
009C1821 cmp dword ptr [ebp-0ECh],0 //判斷this指向的地方是否為0
009C1828 je wmain+0FFh (9C184Fh) //如果為0就直接跳過下面程式碼,不執行函式
009C182A mov esi,esp //儲存esp
009C182C push 1 //將1壓入,delete 壓入1 ;delete[] 壓入3?
                          //這個引數是什麼意義,是一個flag
                            // vector destructor iterator還是不同的destructor

009C182E mov edx,dword ptr [ebp-0ECh] //將存放在棧空間的this值賦值給edx
009C1834 mov eax,dword ptr [edx]  //將地址為edx存放值相同的所存放的值賦給eax,就是將虛擬函式表賦給eax
009C1836 mov ecx,dword ptr [ebp-0ECh] 又將this指標放入ecx中
009C183C mov edx,dword ptr [eax] //將eax中的值賦給edx,就是虛擬函式表的地址
009C183E call edx //呼叫edx
//其實就是 CB::`scalar deleting destructor'
{
...
009C1B6F pop ecx 
009C1B70 mov dword ptr [ebp-8],ecx //1放入ebp-8的棧空間中
009C1B73 mov ecx,dword ptr [this] //傳入this
009C1B76 call CB::~CB (9C115Eh) //呼叫 CB::~CB ()
//CB::~CB()
{
...
009C1BDF pop ecx 
009C1BE0 mov dword ptr [ebp-8],ecx 
009C1BE3 mov eax,dword ptr [this] 
009C1BE6 mov dword ptr [eax],offset CB::`vftable' (9C6740h) //將CB的需函式表地址賦給this指向的地方
                           //和建構函式相反
009C1BEC mov ecx,dword ptr [this] //傳入this
009C1BEF call CA::~CA (9C1163h)  //呼叫 CA::~CA()
//CA::~CA()
{
...
009C1A9F pop ecx 
009C1AA0 mov dword ptr [ebp-8],ecx 
009C1AA3 mov eax,dword ptr [this] 
009C1AA6 mov dword ptr [eax],offset CA::`vftable' (9C6754h)  //將CA的需函式表地址賦給this指向的地方
             //和建構函式相反 
...  
}

}
009C1B7B mov eax,dword ptr [ebp+8] //獲取
009C1B7E and eax,1  //將eax中的值與1 與操作 ,就是在 009C182C push 1 ,傳入的flag
                                   //這裡與下一句配合,為了讓標誌位ZF(Zero Flag) = 1,這裡如果eax和1與操作後為0就讓下一句跳轉
009C1B81 je CB::`scalar deleting destructor'+3Fh (9C1B8Fh) //如果eax與1為0  就跳過下面的操作,不執行delete操作
009C1B83 mov eax,dword ptr [this] //傳入this指標
009C1B86 push eax //壓入this指標
009C1B87 call operator delete (9C10AAh) //呼叫operator delete
009C1B8C add esp,4 //平衡棧
009C1B8F mov eax,dword ptr [this] //返回值
...  
}
009C1840 cmp esi,esp 
009C1842 call @ILT+415(__RTC_CheckEsp) (9C11A4h) 
009C1847 mov dword ptr [ebp-10Ch],eax 
009C184D jmp wmain+109h (9C1859h) 
009C184F mov dword ptr [ebp-10Ch],0

第4章 小結

這裡介紹類函式,其實是以分析為主來介紹類函式,包括類的虛擬函式表以及this指標。
這裡可以配置著《深入C++物件模型》一起學習。