程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 記錄幾個C++多繼承中,this指針與多虛表間編譯與處理的疑問,看編譯器的行為。

記錄幾個C++多繼承中,this指針與多虛表間編譯與處理的疑問,看編譯器的行為。

編輯:C++入門知識

記錄幾個C++多繼承中,this指針與多虛表間編譯與處理的疑問,看編譯器的行為。


簡單無理的的測試代碼:

 

#include 
#include 

using namespace std;

class A
{
public:
     int x;
     int y;
public:
        A()
{
          cout<<構造函數A傳入的this指針得值是:<x = 1;
          this->y = 2;
          cout<z = 10;
		cout<<構造函數C傳入的this指針得值是:<z = 1;//分析繼承的函數關系,確定他的位置
       a = (int)this;
	}
    void F4()
	{
		this->z = 1;
       a = (int)this;
	}
};

void diaplay(A *a)
{
	a->F1();
}

typedef void(C::*pFun)(void); 

int main(int argc, char** argv)
{
     //A* pc = new C();
	// A* pd = new D();
	A* pc = new C();
	 C* pc1 = new C();

	A* pd = new D();
	 D* pd1 = new D();

	// diaplay(pc);
//	 diaplay(pd);
//	 pc->F1();
//	 pd->F1();
	 pd1->F3();//先切換為pd1中基類B的位置
	 pd1->F4();

     cin>>argc;
     return 0;
}
簡單無理的測試code:

 

類A具有一個F1與F3虛函數,其中F3為純虛函數

類B具有一個F2虛函數

其中類C的繼承順序為A、B,而類D的繼承順序為B、A。

則類C與D的對象在內存中的虛表指針存儲位置也是前後相反的,如下圖。

/

 

疑問與總結:

 

1. 如何在基類中調用派生類的成員函數

可以通過虛函數的方法來實現,純虛函數最好。在基類的成員函數中調用派生類,進而讓派生類的實現函數去執行。

本質上需要說明的是:這裡是虛表指針來做調度,當前this只能訪問到基類自己的成員變量、函數以及一張虛表,虛表中的函數通過基類中聲明的位置就可以被編譯器定位到。故無論虛函數位置在虛表哪裡,這個是可以在編譯階段確定的,為靜態編譯。對於具體的虛表,那就是隨著this的建立而動態運行的,即C++多態的本質,延遲執行。本質上還是靜態編譯,只是通過this來訪問虛表,在虛表中偏移位置後確定函數入口。

這裡要說明的是虛表中call時存儲的不一定是函數執行的直接入口,而是一些列函數導出符號表的入口地址,內部通過jump到實際的函數執行入口,這其中都是由編譯器來決定的,個人猜測是通過符號表可以更快速的鏈接到外部模塊的函數,先通過符號表名來綁定函數入口後,後續再添加可執行文件中的函數入口地址到符號表中完成初始化。

 

2. 基類如果存在基類虛表,則所在的this值應當就是虛表所在的位置,即每個基類對象的0地址位置。

 

3.C++下多繼承下的內存布局

每一個基類都需要占據一個內存區域,對含有虛函數的基類,對象內存的堆中需要含有一個基類虛表指針,其中虛表中的函數根據類繼承的覆蓋關系會被派生類覆蓋。

 

4. this指針在多繼承中的靈活使用,基類與派生類函數調用時實際的this指針是不一樣的

首先編譯器在生成對象時,依次調用基類與派生類的構造函數,如上述C類與D類中,在基類A與B對象的構建過程中傳入的this指針值和派生類的對象是不一致的,這裡面編譯器做了處理。對於C類來說,如果其this是0xxxx00,則A類構造函數傳入的也是0xxxx00,因為兩者在內存分布上空間位置是一樣的。反之對D類來說,如果其this是0xxxx00,傳入到A類構造函數的this值則是0xxxx08,偏移的量剛好是B類所占的虛表指針與成員變量的大小。

所以每次在編譯基類函數時,編譯器都是將基類內存對象所在的地址壓入到棧幀中,作為當前函數中的this值,其根本目的是當前基類中this值一旦確定後,編譯器就可以根據虛表位置調用虛表函數(無論是否被派生類覆蓋)以及所以的成員變量。

 

5. 編譯器如何處理派生類中無繼承的虛函數以及繼承的虛函數或純虛函數

類似上述D類中的F3與F4,通過匯編代碼可以知道,在F3中的this還是是0xxxx08,而F4中確是0xxxx00.本質目的是因為編譯器知道了F3是繼承了基類A的,而基類A的虛表位置是在0xxxx08的,故傳入的this指針需要調整派生類對象的this值。

而對於F4而言,實際傳入的this值就是0xxxx00.

對於上述兩個一樣的代碼邏輯,編譯器在處理是確是不一樣的:

 

 

   void F4()
	{
00CE20F0  push        ebp  
00CE20F1  mov         ebp,esp  
00CE20F3  sub         esp,0CCh  
00CE20F9  push        ebx  
00CE20FA  push        esi  
00CE20FB  push        edi  
00CE20FC  push        ecx  
00CE20FD  lea         edi,[ebp-0CCh]  
00CE2103  mov         ecx,33h  
00CE2108  mov         eax,0CCCCCCCCh  
00CE210D  rep stos    dword ptr es:[edi]  
00CE210F  pop         ecx  
00CE2110  mov         dword ptr [ebp-8],ecx  
		this->z = 1;
00CE2113  mov         eax,dword ptr [this]  
00CE2116  mov         dword ptr [eax+4],1  
       a = (int)this;
00CE211D  mov         eax,dword ptr [this]  
00CE2120  mov         ecx,dword ptr [this]  
00CE2123  mov         dword ptr [eax+14h],ecx  
	}

 

 

在給基類B的成員變量賦值是,F4采用的是取當前派生類的this值到eax, eax+4為z所在的位置,即傳入的是派生類對象的this值。

 

   virtual void F3()
	{
00CE20A0  push        ebp  
00CE20A1  mov         ebp,esp  
00CE20A3  sub         esp,0CCh  
00CE20A9  push        ebx  
00CE20AA  push        esi  
00CE20AB  push        edi  
00CE20AC  push        ecx  
00CE20AD  lea         edi,[ebp-0CCh]  
00CE20B3  mov         ecx,33h  
00CE20B8  mov         eax,0CCCCCCCCh  
00CE20BD  rep stos    dword ptr es:[edi]  
00CE20BF  pop         ecx  
00CE20C0  mov         dword ptr [ebp-8],ecx  
		this->z = 1;//分析繼承的函數關系,確定他的位置
00CE20C3  mov         eax,dword ptr [this]  
00CE20C6  mov         dword ptr [eax-4],1  
       a = (int)this;
00CE20CD  mov         eax,dword ptr [this]  
00CE20D0  sub         eax,8  
00CE20D3  mov         ecx,dword ptr [this]  
00CE20D6  mov         dword ptr [ecx+0Ch],eax  
	}
在給基類B的成員變量賦值是,F3采用的是取當前基類所在的this值到eax, eax-4為z所在的位置,即很明確this值應當是基類A虛表所在的位置。

兩者的區別就在於前者F4是無繼承形的虛函數,後者是繼承形的虛函數,編譯器處理的方式是不一樣的,繼承性的處理需要以基類為服從方式,因為基類是派生的基礎。

 

6 編譯自主識別繼承關系,如F3在基類中的調用。

傳入到F3中的this指針值還是,當需要操作讀寫this時,編譯器會自動加入代碼修改this指針為派生類自己的地址。所以無論是基類指針還是派生類指針如果指向一個派生類對象,則其壓入到棧幀中的當前函數this值理論都是指向基類的,這是編譯器的行為,函數內部的處理是一致對基類指針而言的。故對於一個派生類指針,在執行F3時,還是要經歷虛表的調用,先將this值轉為基類所在的地址,作為參數傳入到棧幀之中。

 

7 不同根據類型的繼承與派生的關系,this指針在傳遞過程中,本質是一個動態變化的過程,但要明確的是每個對象就一個this指針,在函數間傳遞的是this以一個輸入參數的形式在棧幀中傳入。

 

 

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