深入理解虛函數


一.  什么為虛函數

簡而言之,在一個類中,前面帶有virtual聲明的成員函數就叫做虛函數,例如

class Base{
public:
    int x;
    int y;
public:
    Base(){
            x=1;
            y=2;
}
    virtual void Print(){         //該函數即為虛函數
            printf("%d %d\n",x,y);
}
};

二.虛函數的間接調用

class Base            
{            
public:            
    void Function_1()            
    {            
        printf("Function_1...\n");            
    }            
    virtual void Function_2()        //虛函數    
    {            
        printf("Function_2...\n");            
    }            
};            

我們生成一個Base實例,通過對象訪問函數,查看反匯編

Base base;            
            
base.Function_1();            
00401090 8D 4D FC             lea         ecx,[ebp-4]            
00401093 E8 9F FF FF FF       call        @ILT+50(Base::Function_1) (00401037)            
            
base.Function_2();            
00401098 8D 4D FC             lea         ecx,[ebp-4]            
0040109B E8 65 FF FF FF       call        @ILT+0(Base::Function_2) (00401005)            
            

我們可以觀察到,Fn1與Fn2都是通過Call指令進行訪問的,即代表着在編譯期間,編譯器就已經給這兩個函數確定的地址,在CPU內留下硬地址,我們利用Call指令就可訪問並執行函數

接着,我們用一個Base類型的指針來訪問函數,查看反匯編得

Base* pb = &base;            
            
pb->Function_1();            
004010A6 8B 4D F8             mov         ecx,dword ptr [ebp-8]            
004010A9 E8 89 FF FF FF       call        @ILT+50(Base::Function_1) (00401037)            
            
pb->Function_2();            
004010AE 8B 4D F8             mov         ecx,dword ptr [ebp-8]     //將this指針放入ecx中,即結構體的首地址       
004010B1 8B 11                mov         edx,dword ptr [ecx]       //將結構體首地址的前四個字節取出放入到edx中     
004010B3 8B F4                mov         esi,esp                 
004010B5 8B 4D F8             mov         ecx,dword ptr [ebp-8]            
004010B8 FF 12                call        dword ptr [edx]           //將那四個字節代表的地址中的前四個字節拿出,當做地址進行call 

經過分析匯編代碼可以得到,用指針訪問虛函數時,函數的地址並不確定,而是通過各種轉換而得到真正的函數地址並執行

總結:

通過對象調用時,virtual函數和普通函數都是E8 Call,即直接Call

通過指針調用時,普通函數為直接Call,virtual函數為FF Call,即間接Call

 

三.深入虛函數調用

class Base            
{            
public:            
    int x;            
    int y;            
    virtual void Function_1()            
    {            
        printf("Function_1...\n");            
    }            
    virtual void Function_2()            
    {            
        printf("Function_2...\n");            
    }            
};  
int main(){
  printf("%d",sizeof(Base));
}

當類中含有虛函數時,我們觀察類的大小

去除兩個局部變量的大小,我們發現虛函數的大小為4,而當我們去掉一個虛函數再測試大小時發現,虛函數大小仍為4.

於是去認真研究反匯編代碼得

 

pb->Function_1();                            
0040D9E3 8B 4D F0             mov         ecx,dword ptr [ebp-10h]       //將結構體首地址放到ecx中                    
0040D9E6 8B 11                mov         edx,dword ptr [ecx]           //將首地址的前四個字節拿出放到edx中            
0040D9E8 8B F4                mov         esi,esp                            
0040D9EA 8B 4D F0             mov         ecx,dword ptr [ebp-10h]       //將結構體首地址放到ecx中                     
0040D9ED FF 12 call dword ptr [edx]                            
                            
                            
pb->Function_2();                            
0040D9F6 8B 45 F0             mov         eax,dword ptr [ebp-10h]                            
0040D9F9 8B 10                mov         edx,dword ptr [eax]                            
0040D9FB 8B F4                mov         esi,esp                            
0040D9FD 8B 4D F0             mov         ecx,dword ptr [ebp-10h]                            
0040DA00 FF 52 04             call        dword ptr [edx+4]             //首地址前四個字節所代表的的地址里面存的值偏移+4               

 

發現帶有虛函數的類中,首地址的前4個字節代表一個新的屬性,這個地址指向一張表,即虛函數表,里面存儲所有虛函數的地址

虛函數表

存儲虛函數地址的表就叫做虛函數表

 

打印虛函數表

class Base                    
{                    
public:                    
    int x;                
    int y;                
    virtual void Function_1()                    
    {                    
        printf("Function_1...\n");                    
    }                    
    virtual void Function_2()                    
    {                    
        printf("Function_2...\n");                    
    }                    
    virtual void Function_3()                    
    {                    
        printf("Function_3...\n");                    
    }                    
};                    
                    
                    
                    
void TestMethod()                    
{                    
    //查看 Sub 的虛函數表                
    Base base;                    
                    
    //對象的前四個字節就是虛函數表                
    printf("base 的虛函數表地址為:%x\n",*(int*)&base);                
                    
    //通過函數指針調用函數,驗證正確性                
    typedef void(*pFunction)(void);                    
                    
    pFunction pFn;                
                    
    for(int i=0;i<3;i++)                
    {                
        int temp = *((int*)(*(int*)&base)+i);            
        pFn = (pFunction)temp;            
        pFn();            
    }                
                    
                    
}                    

 

總結:

當類中有虛函數時,會多出一個屬性,這個屬性占4個字節

多出的一個屬性是虛函數表的地址

 

 

四.深入研究虛函數表

struct Base                                
{                                
public:                                
    virtual void Function_1()                                
    {                                
        printf("Function_1...\n");                                
    }                                
    virtual void Function_2()                                
    {                                
        printf("Function_2...\n");                                
    }                                
    virtual void Function_3()                                
    {                                
        printf("Function_3...\n");                                
    }                                
};                                
                                
                                
                                
int main(int argc, char* argv[])                                
{                                
    //查看 Base 的虛函數表                            
    Base base;                                
                                
    //對象的前四個字節就是虛函數表                            
    printf("Base 的虛函數表地址為:%x\n",*(int*)&base);                            
                                
    //虛函數表中第1個函數地址                            
    printf("虛函數表中第1個函數地址:%x\n",*((int*)(*(int*)&base)+0));                            
    //虛函數表中第2個函數地址                            
    printf("虛函數表中第2個函數地址:%x\n",*((int*)(*(int*)&base)+1));                            
    //虛函數表中第3個函數地址                            
    printf("虛函數表中第3個函數地址:%x\n",*((int*)(*(int*)&base)+2));                            
                                
    //通過函數指針調用函數,驗證正確性                            
    typedef void(*pFunction)(void);                            
                                
    pFunction pFn;                            
    pFn = (pFunction)*((int*)(*(int*)&base)+0);                            
    pFn();                            
    pFn = (pFunction)*((int*)(*(int*)&base)+1);                            
    pFn();                            
    pFn = (pFunction)*((int*)(*(int*)&base)+2);                            
    pFn();                            
                                
    return 0;                            
}                                

下面來研究幾個類型來加強對虛函數的理解

 

單繼承無函數覆蓋

struct Base                        
{                        
public:                        
    virtual void Function_1()                        
    {                        
        printf("Base:Function_1...\n");                        
    }                        
    virtual void Function_2()                        
    {                        
        printf("Base:Function_2...\n");                        
    }                        
    virtual void Function_3()                        
    {                        
        printf("Base:Function_3...\n");                        
    }                        
};                        
struct Sub:Base                        
{                        
public:                        
    virtual void Function_4()                        
    {                        
        printf("Sub:Function_4...\n");                        
    }                        
    virtual void Function_5()                        
    {                        
        printf("Sub:Function_5...\n");                        
    }                        
    virtual void Function_6()                        
    {                        
        printf("Sub:Function_6...\n");                        
    }                        
};                        
int main(int argc, char* argv[])                        
{                        
    //查看 Sub 的虛函數表                    
    Sub sub;                        
                        
    //對象的前四個字節就是虛函數表                    
    printf("Sub 的虛函數表地址為:%x\n",*(int*)&sub);                    
                        
    //通過函數指針調用函數,驗證正確性                    
    typedef void(*pFunction)(void);                        
                        
    pFunction pFn;                    
                        
    for(int i=0;i<6;i++)                    
    {                    
        pFn = (pFunction)*((int*)(*(int*)&sub)+i);                
        pFn();                
    }                    
                        
                        
    return 0;                    
}                        
                        

得到有一張虛函數表,且每個虛函數得到執行

 

單繼承有函數覆蓋

struct Base                    
{                    
public:                    
    virtual void Function_1()                    
    {                    
        printf("Base:Function_1...\n");                    
    }                    
    virtual void Function_2()                    
    {                    
        printf("Base:Function_2...\n");                    
    }                    
    virtual void Function_3()                    
    {                    
        printf("Base:Function_3...\n");                    
    }                    
};                    
struct Sub:Base                    
{                    
public:                    
    virtual void Function_1()                    
    {                    
        printf("Sub:Function_1...\n");                    
    }                    
    virtual void Function_2()                    
    {                    
        printf("Sub:Function_2...\n");                    
    }                    
    virtual void Function_6()                    
    {                    
        printf("Sub:Function_6...\n");                    
    }                    
};                    
int main(int argc, char* argv[])                    
{                    
    //查看 Sub 的虛函數表                
    Sub sub;                    
                    
    //對象的前四個字節就是虛函數表                
    printf("Sub 的虛函數表地址為:%x\n",*(int*)&sub);                
                    
    //通過函數指針調用函數,驗證正確性                
    typedef void(*pFunction)(void);                    
                    
    pFunction pFn;                
                    
    for(int i=0;i<6;i++)                
    {                
        int temp = *((int*)(*(int*)&sub)+i);            
        if(temp == 0)            
        {            
            break;        
        }            
        pFn = (pFunction)temp;            
        pFn();            
    }                
                    
    printf("%x\n",*((int*)(*(int*)&sub)+6));                
                    
    return 0;                
}                    

 

 

 通過該輸出我們可以得出一個結論,當有函數重載時,子類虛函數會覆蓋基類虛函數,僅執行子類虛函數

 

多重繼承有函數覆蓋

struct Base1                            
{                            
public:                            
    virtual void Fn_1()                            
    {                            
        printf("Base1:Fn_1...\n");                            
    }                            
    virtual void Fn_2()                            
    {                            
        printf("Base1:Fn_2...\n");                            
    }                            
};                            
struct Base2                            
{                            
public:                            
    virtual void Fn_3()                            
    {                            
        printf("Base2:Fn_3...\n");                            
    }                            
    virtual void Fn_4()                            
    {                            
        printf("Base2:Fn_4...\n");                            
    }                            
};                            
struct Sub:Base1,Base2                            
{                            
public:                            
    virtual void Fn_1()                            
    {                            
        printf("Sub:Fn_1...\n");                            
    }                            
    virtual void Fn_3()                            
    {                            
        printf("Sub:Fn_3...\n");                            
    }                            
    virtual void Fn_5()                        
    {                            
        printf("Sub:Fn_5...\n");                            
    }                            
};                            
int main(int argc, char* argv[])                            
{                            
    //查看 Sub 的虛函數表                        
    Sub sub;                            
                            
    //通過函數指針調用函數,驗證正確性                        
    typedef void(*pFunction)(void);                            
                            
                            
    //對象的前四個字節是第一個Base1的虛表                        
    printf("Sub 的虛函數表地址為:%x\n",*(int*)&sub);                        
                            
    pFunction pFn;                        
                            
    for(int i=0;i<6;i++)                        
    {                        
        int temp = *((int*)(*(int*)&sub)+i);                    
        if(temp == 0)                    
        {                    
            break;                
        }                    
        pFn = (pFunction)temp;                    
        pFn();                    
    }                        
                            
    //對象的第二個四字節是Base2的虛表                        
    printf("Sub 的虛函數表地址為:%x\n",*(int*)((int)&sub+4));                        
                            
    pFunction pFn1;                        
                            
    for(int k=0;k<2;k++)                        
    {                        
        int temp = *((int*)(*(int*)((int)&sub+4))+k);                    
        pFn1 = (pFunction)temp;                    
        pFn1();                    
    }                        
                            
    return 0;                        
}                            

 

得出結論,當一個子類同時繼承兩個父類時,會產生兩張虛函數表,且也是子虛函數會覆蓋基類虛函數。

 

通過虛函數的使用,我們可以引出一句話:虛函數的本質就是動態綁定,即當函數被真正調用時,才能知道函數的地址。

總結:

只有virtual函數是動態綁定的

動態綁定還有一個名字:多態(多態的基礎是虛函數)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM