程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++代碼分析

C++代碼分析

編輯:C++入門知識

C++虛函數是構成多態的一部分,多態指的是運行期決定調用哪個函數,下面是個純虛函數例子:
#include "stdafx.h"
class Test{
public:
  Test(){
    printf("Test::Test\n");
  }
  virtual ~Test(){
    printf("Virtual ~Test()\n");
  }
  virtual void prointer()=0;
  virtual void pointf()=0;
};
class TestA:public Test{
public:
  TestA(){
    printf("TestA::TestA\n");
  }
  virtual ~TestA(){
    printf("TestA::TestA\n");
  }
  virtual void prointer(){
    printf("Derive Class TestA::Pointer\n");
  }
  virtual void pointf(){
    printf("Derive Class TestA::Pointf\n");
  }
};
int _tmain(int argc, _TCHAR* argv[]){
  TestA *pTest=new TestA;
  pTest->pointf();
  pTest->prointer();
  delete pTest;
  return 0;
}
這段代碼定義了一個抽象類,和一個派生類,抽象類不能創建自己的對象,但是可以間接的從派生類創建自己的對象,構成純虛函數的條件:
1.  一個類中必須要有一個虛函數
2.  在虛函數後面添加一個=0就是一個純虛函數了
抽象基類的所有純虛函數必須被派生類定義的虛函數覆蓋,否者派生類也是一個抽象基類,不能創建自己的對象;先看下Test類,由於Test類不能創建自己的對象,所以我根據TestA類來解析調用過程。Test類我們可以把它看做一個地址,這個地址裡面有些指針,只想函數的地址,假如Test類的地址是0x401000,那麼在這個地址裡面的第一個就是虛折構函數,方便釋放類的對象的時候調用,第二個沒有了,因為我們只在Test類中定義一個析構函數,和一個構造函數,構造函數在編譯的時候就被編譯器從類的裡面給趴到Main來了,看下反匯編代碼:
00401091  |.  6A 04         PUSH 4
00401093  |.  E8 68000000   CALL <JMP.&MSVCR90.operator new>
00401098  |.  8BF0          MOV ESI,EAX
0040109A  |.  83C4 04       ADD ESP,4
0040109D  |.  85F6          TEST ESI,ESI
0040109F  |.  74 27         JE SHORT 004010C8
這裡就是TestA *pTest=new TestA這裡了,從這段代碼我們可以看出,new是無論何如都會調用成功的,因為CALL <JMP.&MSVCR90.operator new>後的返回值,被比較是否等於0了,雖然這個比較不是我們的代碼,但是編譯器就已經夠定了new無論如何都會調用成功,如果CALL <JMP.&MSVCR90.operator new>的返回值是0,那麼構造函數都會被跳過,而構造函數是會被程序調用的,如果不調用的話,這樣就和C++構造函數的說法相反了,所以new 操作符分配的內存一定會成功的,我們在接著看下下面這段代碼:
004010A1  |.  57            PUSH EDI
004010A2  |.  8B3D B0204000 MOV EDI,DWORD PTR DS:[<&MSVCR90.printf>]      ;  msvcr90.printf
004010A8  |.  68 0C214000   PUSH 0040210C                          ; /format = "Test::Test"
004010AD  |.  C706 7C214000 MOV DWORD PTR DS:[ESI],0040217C         ; |
004010B3  |.  FFD7          CALL EDI                                      ; \printf
004010B5  |.  68 2C214000   PUSH 0040212C                          ;  ASCII "TestA::TestA"
004010BA  |.  C706 8C214000 MOV DWORD PTR DS:[ESI],0040218C
004010C0  |.  FFD7          CALL EDI
這段代碼顯然是兩個類的構造函數被調用了,那麼其中傳遞了兩個地址給ESI,我們看下這個地址是什麼類容,我們跟隨到數據窗口看一下,顯示格式選擇為地址格式
0040217C  00401000  這就是這個地址的內容,一個代碼地址
C++構造.00401000其中第一個地址指向如下地址,跟隨一下
00401000   .  56            PUSH ESI
00401001   .  8BF1          MOV ESI,ECX
00401003   .  68 18214000   PUSH 00402118                      ; /format = "Virtual ~Test()"
00401008   .  C706 7C214000 MOV DWORD PTR DS:[ESI],0040217C               ; |
0040100E   .  FF15 B0204000 CALL DWORD PTR DS:[<&MSVCR90.printf>]         ; \printf
這裡顯然就是折夠函數了,所以當一個類中有虛析構函數的時候,這個虛析構函數的地址會被放在類指針的最前面,這裡把Test的地址的指針放入ESI裡面,然後根據ESP+8來判斷是否調用delete操作符,這些都是編譯器自動添加的,這是編譯器的事,我還沒那技術去研究
00401014   .  83C4 04       ADD ESP,4
00401017   .  F64424 08 01  TEST BYTE PTR SS:[ESP+8],1
0040101C   .  74 09         JE SHORT 00401027
0040101E   .  56            PUSH ESI
0040101F   .  E8 D6000000   CALL <JMP.&MSVCR90.operator delete>
00401024   .  83C4 04       ADD ESP,4
00401027   >  8BC6          MOV EAX,ESI
00401029   .  5E            POP ESI
0040102A   .  C2 0400       RETN 4
繼續我們上面的構造函數,類的構造函數被一次從上至下的調用之後,傳遞了Test和TestA的地址到ESI裡面,我們聲明的是TestA的對象,所以最後一個地址就是TestA了,看下反匯編代碼的調用過程
004010C2  |.  83C4 08       ADD ESP,8
004010C5  |.  5F            POP EDI
004010C6  |.  EB 02         JMP SHORT 004010CA
004010C8  |>  33F6          XOR ESI,ESI
004010CA  |>  8B06          MOV EAX,DWORD PTR DS:[ESI]                    ; 
004010CC  |.  8B50 08       MOV EDX,DWORD PTR DS:[EAX+8]
004010CF  |.  8BCE          MOV ECX,ESI
004010D1  |.  FFD2          CALL EDX
這裡ESI指向TestA類的起始地址,把這個起始地址傳到EAX裡面之後,就把這個類裡面的一個函數地址放到EDX裡面,TestA類本身一共有4個函數,剛才構造函數被外部也就是Main調用了,那麼裡面只剩下3個地址了,我們知道一個類如果有虛析構函數,第一個地址就指向虛析構函數的地址,EAX+8就是調用了
pTest->pointf();至於為什麼,自己想一下,MOV ECX,ESI通過ECX來保證堆棧的平衡
004010D3  |.  8B06          MOV EAX,DWORD PTR DS:[ESI]                    ;  C++構造.0040218C
004010D5  |.  8B50 04       MOV EDX,DWORD PTR DS:[EAX+4]
004010D8  |.  8BCE          MOV ECX,ESI
004010DA  |.  FFD2          CALL EDX
這裡就調用了pTest->prointer();因為我們是根據類的地址來決定調用哪個函數的
004010DC  |.  8B06          MOV EAX,DWORD PTR DS:[ESI]
004010DE  |.  8B10          MOV EDX,DWORD PTR DS:[EAX]                    ;  C++構造.00401050
004010E0  |.  6A 01         PUSH 1
004010E2  |.  8BCE          MOV ECX,ESI
004010E4  |.  FFD2          CALL EDX
這裡就是調用TestA類的虛折構函數,也就是當前類的地址的第一個指針,我們跟蹤進去看一下,下面是反匯編代碼:
00401050   .  56            PUSH ESI
00401051   .  57            PUSH EDI
00401052   .  8B3D B0204000 MOV EDI,DWORD PTR DS:[<&MSVCR90.printf>]      ;  msvcr90.printf
00401058   .  8BF1          MOV ESI,ECX
0040105A   .  68 2C214000   PUSH 0040212C                     ; /format = "TestA::TestA"
0040105F   .  C706 8C214000 MOV DWORD PTR DS:[ESI],0040218C               ; |
00401065   .  FFD7          CALL EDI                                      ; \printf
00401067   .  68 18214000   PUSH 00402118                        ;  ASCII "Virtual ~Test()"
0040106C   .  C706 7C214000 MOV DWORD PTR DS:[ESI],0040217C
00401072   .  FFD7          CALL EDI
00401074   .  83C4 08       ADD ESP,8
00401077   .  F64424 0C 01  TEST BYTE PTR SS:[ESP+C],1
0040107C   .  74 09         JE SHORT 00401087
0040107E   .  56            PUSH ESI
0040107F   .  E8 76000000   CALL <JMP.&MSVCR90.operator delete>
00401084   .  83C4 04       ADD ESP,4
00401087   >  5F            POP EDI
00401088   .  8BC6          MOV EAX,ESI
0040108A   .  5E            POP ESI
0040108B   .  C2 0400       RETN 4
這裡調用了兩個虛析構函數的地方,為什麼是先調用~TestA,而不是先調用~Test呢,因為我們把這兩個析構函數定義為了虛函數,虛函數是在運行期決定調用誰的,當我們把TestA的成員函數調用完畢之後,析構函數會自動調用,因此,TestA完了之後就調用自己的析構函數,釋放最新分配的內存,所以,先調用TestA的折構函數,再調用Test的折構函數,這也是為什麼把析構函數聲明為虛函數的原因,
這裡調用了兩個虛析構函數之後,就是用delete指針刪除了由new分配的地址,分析完畢。
總結一下:
當我們定義了一個帶有虛函數的類的時候,這個類的虛函數就會被放在一個地址表裡面,這個地址表被放在類的入口裡面,當我們調用哪個類的時候,就使用哪個類的入口來調用裡面的虛函數,這就證名明了C++中得函數同名的虛函數的多態機制。
講得不好,如果有錯誤的地方請各位指出來,謝謝!


作者“科技改變世界”

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved