C++幕後故事(四)--虛函數的故事

提出問題:

我們討論前提都是在windows 10 vs2013 debug模式下

1.虛函數指針和虛表在哪裏?
2.我們如何手動調用虛函數?
3.爲什麼只有在子類以父類的引用或者指針的形式才能出現多態?
4.虛函數的調用爲什麼效率相比普通的成員函數較低?又具體低了多少?
5.爲什麼構造函數和析構函數儘量不要調用虛函數?
6.純虛函數到底是什麼?爲什麼禁止我調用?有什麼辦法可以繞編譯器?

7.看碼說話

class MemsetObj {
public:
    MemsetObj()
{
    memset(this, 0, sizeof(MemsetObj));
        cout << "memsetobj ctor" << endl;
    }
    void test() { cout << "memset obj test" << endl; }
    virtual void virtual_func() {  cout << "virtual test" << endl; }
};
     // 1.
    MemsetObj obj;
    obj.virtual_func();
     // 2.
    MemsetObj *pobj = new MemsetObj();
    pobj->virtual_func();
// 這兩種方式調用有問題?什麼問題?

0.基礎知識說明

1.VS 調試

1.打開內存窗口

  1. 先下斷點
  2. 在VS中啓動程序
  3. VS就會你下的斷點處停下
  4. 在菜單欄->調試->窗口->內存,就會發現4個內存選項隨便選擇一個就可以

2.打開反彙編窗口

  1. 同樣在調試的狀態下,鼠標右鍵菜單->轉到反彙編點

2.彙編指令

這裏我解釋下常用的和常見的一些指令,更多的需要大家自己課後學習。

  1. 彙編中有些通用的寄存器分別爲eax,ebx,ecx,edx,esi,edi,esp,ebp,es,ds,ss,sp等等,類似高級語言中的變量,但是這些變量的數量和名稱都是固定的。
  2. mov ax,bx ; 將bx的內容移動ax中
  3. lea ax,[obj] ; load effective address 將obj對象的地址移動ax中
  4. pop,push 就是棧中的壓棧和出棧的操作,函數中的參數就是這麼傳遞的。
  5. 在彙編的角度看C++的成員函數調用的方式叫做__thiscall,就是C++中的this指針,是通過ecx指針傳遞的,所以會經常看到lea ecx,[base] call FunctionName,套路都是固定的。

1.虛函數的含義

只有用virtual聲明類的成員函數,稱之爲虛函數。

2.虛函數的作用

就是一句話:實現多態的基石
實現多態的三大步:

  1. 存在繼承關係
  2. 重寫父類的virtual function
  3. 子類以父類的指針或者是引用的身份出現

3.虛函數的實現原理

相信很多人都能說出來其中實現關鍵原理,就是兩點:虛函數表指針(vptr),虛函數表(vftable)

3.1 虛函數表指針

1. 什麼是虛函數表指針,他在哪裏,有什麼用?

我們把對象從首地址開始的4個字節,這個位置我們稱之爲虛函數表指針(vptr),它裏面包含一個地址指向的就是虛函數表(vftable)的地址。

3.2虛函數表

1. 什麼是虛函數表,他又在哪裏,有什麼用?

虛函數表說白了就是裏面是一組地址的數組(就是函數指針數組),他所在的位置就是虛函數表指針裏面所存儲的地址,它裏面所包含的地址就是我們重寫了父類的虛函數的地址(沒有重寫父類的虛函數那麼默認的就是父類的函數地址)。

3.3 探索虛表位置

1. 手動探索虛表位置

class Base 
{
public:
    int m_a = 0;
    virtual void f() { cout << "Base::f()" << endl; }
    virtual void g() { cout << "Base::g()" << endl; }
virtual void h() { cout << "Base::h()" << endl; }
    // virtual ~Base() { cout << "~Base" << endl; }
 };
class BaseOne
{
public:
    int m_b = 0;
    virtual void i() { cout << "BaseOne::i()" << endl; }
    virtual void j() { cout << "BaseOne::j()" << endl; }
virtual void k() { cout << "BaseOne::k()" << endl; }
// virtual ~ BaseOne () { cout << "~ BaseOne " << endl; }
};
class Derive : public Base
{
public:
    int m_d = 0;
virtual void g() { cout << "Derive::g()" << endl; }
// virtual ~ Derive () { cout << "~ Derive " << endl; }
};
class MultiDerive: public BaseOne, public Base
{
public:
    int m_e = 0;
    virtual void h() { cout << "MultiDerive::h()" << endl; }
    virtual void k() { cout << "MultiDerive::k()" << endl; }
virtual void m() { cout << "MultiDerive::m()" << endl; }
// virtual ~ MultiDerive () { cout << "~ MultiDerive " << endl; }
};

如果代碼看的累,我們直接看圖(這裏我沒有用UML圖表示):

既然我們知道vptr的位置,我們開始嘗試手動調用虛函數

typedef void (*Fun)();
void test_multi_inhert_virtual_fun()
{
    MultiDerive multiderive;
    // 從對象的首地址4個字節,獲取vptr的地址,x86平臺下指針都是4個字節
    long *pbaseone_tablepoint      = (long *)(&multiderive);
    // 對vptr指針解引用操作獲取vftable的地址
    long *baseone_table_address    = (long *)*(pbaseone_tablepoint);
    for(int i = 0; i < 4; ++i) {
        cout << std::hex << "0x" << baseone_table_address[i] << endl;
    }
    ((Fun)(baseone_table_address[0]))();
    ((Fun)(baseone_table_address[1]))();
    ((Fun)(baseone_table_address[2]))();
    /* 打印的結果
    BaseOne::i()
    BaseOne::j()
    MultiDerive::k()
    */
    // 獲取第二個虛函數表指針的位置,從基址開始的地址+第一個繼承對象的大小
    long *pbase_tablepoint = 
        (long *)(reinterpret_cast<char *>(pbaseone_tablepoint)+sizeof(BaseOne));
    long *base_table_address = (long *)(*pbase_tablepoint);
    for(int i = 0; i < 4; ++i) {
        cout << std::hex << "0x" << base_table_address[i] << endl;
    }
    ((Fun)(base_table_address[0]))();
    ((Fun)(base_table_address[1]))();
    ((Fun)(base_table_address[2]))();
    /*打印的結果
    Base::f()
    Base::g()
    MultiDerive::h()
    */
}

對於不想看代碼的同學,我畫了一張圖

對於這張圖裏面的一些強轉我做些解釋:

  1. x86平臺下,指針類型都是4個字節,所用你可以把long *替換爲int *沒有問題,只要這個指針的步長是4字節的都可以。
  2. 獲取第二個vftable時,強轉爲char*類型,因爲指針類型做+/-運算有個步長概念,有的稱之爲比例因子。
函數 打印結果
((Fun)(baseone_table_address[0]))(); BaseOne::i()
((Fun)(baseone_table_address[1]))(); BaseOne::j()
((Fun)(baseone_table_address[2]))(); MultiDerive::k()
((Fun)(base_table_address[0]))(); Base::f()
((Fun)(base_table_address[1]))(); Base::g()
((Fun)(base_table_address[2]))(); MultiDerive::h()

從表中我們可以看出來,其實這個vptr和vftable也沒啥神祕的地方,就是一個指針指向裝有函數指針的數組。數組裏面的內容如果子類沒有override,那麼默認值就是父類的虛函數地址,否則就是子類自己的函數(表中紅色部分)

2.cl.exe驗證虛表的位置

我們對類的聲明代碼稍作修改,做成獨立的cpp文件

/****************************************************************************
**
** Copyright (C) 2019 [email protected]
** All rights reserved.
**
****************************************************************************/
#include <iostream>
using std::cout;
using std::endl;
class BaseOne
{
public:
    int m_b = 0;
    virtual void i() { cout << "BaseOne::i()" << endl; }
    virtual void j() { cout << "BaseOne::j()" << endl; }
    virtual void k() { cout << "BaseOne::k()" << endl; } 
};
class Base
{
public:
    int m_a = 0;
    virtual void f() { cout << "Base::f()" << endl; }
    virtual void g() { cout << "Base::g()" << endl; }
    virtual void h() { cout << "Base::h()" << endl; }
};
class MultiDerive :public BaseOne, public Base
{
public:
    int m_e = 0;
    virtual void h() { cout << "MultiDerive::h()" << endl; }
    virtual void k() { cout << "MultiDerive::k()" << endl; }
    virtual void m() { cout << "MultiDerive::m()" << endl; }
};

int main()
{   
    return 1;
}

在windows菜單中找到vs2013命令行工具

cd 到你所在cpp文件的目錄中比如我:cd /d j:\code\polymorphism_virtual\source
執行命令 cl /d1 reportSingleClassLayoutMultiDerive analysis_virtual_by_tools.cpp
注意MultiDerive表示你想導出來的類佈局

從導出來的數據中可以看出:

1.MultiDerive大小、內存佈局、以及成員變量的偏移、vftable的尋址

2.大家可能看到紅色標註的-8有點奇怪,這個就是計算第二個vptr的偏移地址,計算方式是首地址-第二個vptr的所在的地址=-8,他們之間的距離就是一個BaseOne的大小。

3.VS IDE查看虛表位置

這種方式比較簡答,就是啓動VS,下個斷點,將鼠標移動到MultiDerive實例上,可以看到詳細的信息。

從這裏你可能發現一個問題,子類自己的虛函數這裏沒有體現出來,所以建議cl.exe工具驗證虛表。

4.內存佈局圖

到這裏我們可以畫圖上述代碼中類的內存佈局圖:

從Derive和MultiDerive內存圖我們發現一些規律:

1.繼承的體系越複雜,子類的體積越大。

2.子類中普通成員順序按照繼承的先後順序來的。

3.多重繼承,子類中含有多個vptr,分別指向不同的vftable。

4. vftable中的虛函數地址和在類中聲明的順序一致。

5.如果子類override父類中虛函數,那麼子類vftable中就會替換原來父類的虛函數。

6.如果子類自己含有額外的虛函數,則會附加到第一個vftable中。

7. vftable中的最後一個值可能爲0x0,有時候並不是爲0,上圖中紅色字部分。

8.子類有vftable,同時父類也有一份vftable,兩個vftable沒有關聯。每個實例化子類都共享一個vftable,同樣父類所有實例化對象也共享一份vftable,非常類似類的靜態變量

3.4虛析構函數用函數指針間接調用會崩潰

眼尖的同學可能注意到探索虛表位置的代碼,我註釋掉了父類中所有的虛析構函數。我本想拿到虛表地址,就拿到了虛析構函數的地址,我用函數指針強制轉換就可以調用,但是我一調用就崩潰。好,廢話不說,反彙編走起來。

1.手動調用析構函數的反彙編代碼

;  base.~Base();
; 傳遞參數0
002D99EF  push        0  
; 傳遞this到ecx
002D99F1  lea         ecx,[base]  
; 調用一個類似析構函數的函數
002D99F4  call        virtual_fun_table::Base::`scalar deleting destructor' (02D16C2h) 

2.動手驗證猜想

看了反彙編代碼,會驚訝的發現,居然push 0了,說明編譯器幫我們傳遞一個參數,那我們就自己手動也傳遞一個,你同時發現了這裏並不是直接調用Base的析構函數(代理析構函數)。

typedef void (*DctorFun)(int);
Base base;
base.~Base();
long *vptr      = (long *)(&base);
long *vftable   = (long *)(*vptr);
((Fun)(vftable[0]))();
((Fun)(vftable[1]))();
((Fun)(vftable[2]))();
((DctorFun)(vftable[3]))(0); ; 調用Base的虛析構函數

恩,應該沒有問題了,開心的跑起來,VS F5跑起來。但還是拋出異常了…
想不出來啥原因,還是看反彙編吧,不斷的跟啊跟啊,發現崩潰在~Base函數裏面。

01276EFF  pop          ecx  
01276F00  mov         dword ptr [this],ecx  
01276F03  mov         eax,dword ptr [this]  
01276F06  mov         dword ptr [eax],1282BD8h

這裏拿到ecx,然後又一次賦值虛表地址1282BD8h。但是我們是直接調用虛函數,沒有push ecx,所以這裏拿到的ecx值是個不確定的值就相當於一個野指針問題。
我們知道了爲什麼崩潰的原因,但是仔細想想這裏爲什麼還需要再一次重新賦值虛表地址,在構造函數的時候不是已經初始化好虛表的地址了?
試想下這樣的情形:Derive繼承Base,在調用Derive析構函數時,先是調用Derive的析構函數,再Base調用的析構函數。如果在調用Base的析構函數時不重置虛表的話,那麼Base中可能出現間接的調用Derive虛函數,而此時Derive的已經執行過析構函數,裏面的數據已經是不可靠的,存在安全隱患。
同時得出結論析構函數和構造函數中調用虛函數並沒有多態的特性。
關於間接的調用虛函數說明:
我們知道,在構造函數中直接調用虛函數的話,是沒有多態的特性,但是如果我們寫一個函數,這個函數再去調用虛函數,這個時候生成的彙編代碼,會根據虛表找函數地址,就有了多態的特性。有興趣的同學可以自己反彙編驗證下。

4.vptr和vftable的初始化問題

4.1 vptr什麼時候初始化?

1.對象初始化時

2.拷貝構造調用

我們從彙編的角度看,在對象初始化時如何設置vftable的地址。

在vs2013 在調試狀態下,右鍵轉到反彙編就可以看到彙編代碼

// c++代碼
MultiDerive multiderive;

這下面三個都是彙編代碼

; MultiDerive multiderive;
; multiderive的地址放到ecx寄存器中
00FC9CD8  lea           ecx,[multiderive]
; 調用MultiDerive構造函數
00FC9CDB  call          virtual_fun_table::MultiDerive::MultiDerive (0FC12BCh) 
; 跳轉到 virtual_fun_table::MultiDerive::MultiDerive:
00FC12BC  jmp         virtual_fun_table::MultiDerive::MultiDerive (0FC4180h) 
00FC4180  push        ebp  
00FC4181  mov        ebp,esp  
00FC4183  sub         esp,0CCh  
00FC4189  push        ebx  
00FC418A  push        esi  
00FC418B  push        edi  
00FC418C  push        ecx  
00FC418D  lea          edi,[ebp-0CCh]  
00FC4193  mov         ecx,33h  
00FC4198  mov         eax,0CCCCCCCCh  
00FC419D  rep stos    dword ptr es:[edi]  ; 從00FC4180~00FC419D都是做函數調用過程初始化準備
00FC419F  pop         ecx                       ; 拿到this指針
00FC41A0  mov        dword ptr [this],ecx  
00FC41A3  mov        ecx,dword ptr [this]  
00FC41A6  call         virtual_fun_table::BaseOne::BaseOne (0FC1235h)  ; 調用BaseOne的構造函數
00FC41AB  mov        ecx,dword ptr [this]  
00FC41AE  add         ecx,8                 ; 調整this指針偏移,並將this指針傳遞給調用Base構造函數
00FC41B1  call          virtual_fun_table::Base::Base (0FC15D2h)  
00FC41B6  mov         eax,dword ptr [this]  
00FC41B9  mov         dword ptr [eax],0FD1B54h  ; 將vftable的地址設置到第一個vptr中
00FC41BF  mov         eax,dword ptr [this]              
00FC41C2  mov         dword ptr [eax+8],0FD1B6Ch ;跳過8字節,設置第二個虛表地址放到第二個vptr中
00FC41C9  mov         eax,dword ptr [this]  
00FC41CC  mov         dword ptr [eax+10h],0  
00FC41D3  mov         eax,dword ptr [this]  
00FC41D6  pop         edi  
00FC41D7  pop         esi  
00FC41D8  pop         ebx  
00FC41D9  add         esp,0CCh  
00FC41DF  cmp         ebp,esp  
00FC41E1  call          __RTC_CheckEsp (0FC1488h)  
00FC41E6  mov         esp,ebp  
00FC41E8  pop          ebp  
00FC41E9  ret  

關於這裏的彙編代碼做下說明:

  1. c++中類成員的函數調用叫做thiscall方式,就是this指針通過ecx寄存器來傳遞的。
  2. 注意看下旁邊的註釋行文字
  3. 代碼中紅色加粗的兩個地址0FD1B54h0FD1B6Ch就是關鍵的vftable地址
  4. 這裏我只帶領大家看了構造函數的,那麼拷貝構造原理是一樣的大家可以自己嘗試。
    如果大家的彙編看不懂,就看看下面的這張調用圖便於大家的理解:

4.2 vftable裏面的內容什麼時候初始化?

這個編譯器在編譯期間就已經初始化好了,爲每個類確定好了虛表裏面對應的內容。

5.到底是怎麼實現多態特性

前面,我們詳細介紹了虛函數的實現細節,但是子類爲什麼以父類引用或指針的身份出現就會有多態的特性,總感覺還有一層窗戶紙沒有捅破。
上代碼,我們看下面的這段代碼

;  MultiDerive multiderive;  c++代碼
01289FC8  lea         ecx,[multiderive]  
01289FCB  call        virtual_fun_table::MultiDerive::MultiDerive (012812BCh)  
 ;   multiderive.m(); c++代碼
 ; 注意看這裏以普通的身份調用虛函數,就兩行彙編代碼,直接調用傳遞this指針
01289FD0  lea         ecx,[multiderive]  
01289FD3  call        virtual_fun_table::MultiDerive::m (012812F8h)  

; MultiDerive *multiderive2 = new MultiDerive(); c++代碼
; 這裏從0x01289FD8~到0x0128A014主要是進行了申請堆內存
; 申請成功了調用構造函數,申請失敗跳過構造函數
01289FD8  push        14h  
01289FDA  call         operator new (01281587h)  
01289FDF  add         esp,4  
01289FE2  mov         dword ptr [ebp-13Ch],eax  
01289FE8  cmp         dword ptr [ebp-13Ch],0  
01289FEF  je            virtual_fun_table::test_multi_inhert_virtual_fun+64h (0128A004h)  
01289FF1  mov         ecx,dword ptr [ebp-13Ch]  
01289FF7  call          virtual_fun_table::MultiDerive::MultiDerive (012812BCh)  
01289FFC  mov         dword ptr [ebp-144h],eax  
0128A002  jmp         virtual_fun_table::test_multi_inhert_virtual_fun+6Eh (0128A00Eh)  
0128A004  mov         dword ptr [ebp-144h],0  
0128A00E  mov         eax,dword ptr [ebp-144h]  
0128A014  mov         dword ptr [multiderive2],eax  
; multiderive2->m(); c++代碼
; 這裏是我們需要看的重點內容
; 將multiderive2的首地址,放到eax寄存器
0128A017  mov         eax,dword ptr [multiderive2]  
; eax就是我們之前說的vptr,對vptr解引用獲取到vftable地址放到edx
0128A01A  mov         edx,dword ptr [eax]  
0128A01C  mov         esi,esp  
; thiscall方式調用,通過ecx傳遞this指針地址
0128A01E  mov          ecx,dword ptr [multiderive2]  
; edx裏面存放了vftable的地址+偏移地址
; 還原成高級語言就是數組取值 eax = vftable[0CH],每個函數指針是4個字節,4*3=12byte
; eax中就是存放了虛函數m的地址
0128A021  mov         eax,dword ptr [edx+0Ch]  
0128A024  call          eax  
0128A026  cmp         esi,esp  
0128A028  call          __RTC_CheckEsp (01281488h)  

我們簡化下來對比下代碼

multiderive.m(); 01289FD0 lea ecx,[multiderive]
01289FD3 call virtual_fun_table::MultiDerive::m (012812F8h)
multiderive2->m(); 0128A017 mov eax,dword ptr [multiderive2]
0128A01A mov edx,dword ptr [eax]
0128A01C mov esi,esp
0128A01E mov ecx,dword ptr [multiderive2]
0128A021 mov eax,dword ptr [edx+0Ch]
0128A024 call eax

如果不是引用或者指針,虛函數的調用則是直接尋址,2行彙編代碼搞定。

如果是指針或者是引用,看出虛函數的尋址並不是那麼簡單的。先是找到vptr再找到vftable在加上偏移地址(偏移量是在編譯器就已經確定的)最後纔是真正的函數調用地址,用了6行代碼。

現在你能理解爲什麼虛函數的調用效率比較慢了吧。

6.純虛函數理解

6.1 含義:

如果我們在虛函數原型的後面加上=0(virtual void func()= 0),同時這個函數是沒有實現的。

6.2作用

有純虛函數的類表示這是一個抽象類,既然是抽象的,那麼肯定就是不能實例化。關於抽象類不能實例化可以從邏輯上理解也是合理的,比如說:動物,老虎,獅子,人都是動物。但是你說動物沒人能理解你說的動物到底指的是什麼東西。

由純虛函數的引出了抽象類,抽象類的出現是爲了解決什麼問題?

抽象類就是爲了被繼承的,它爲子類實例化提供藍圖。在相關的組織繼承層次中,它來提供一個公共的根。其他相關子類都是這裏衍生出來。

它與接口的區別是什麼?

接口是對動作的抽象,抽象類是對根源的抽象。比如說人,有五官,有其他屬性。但是喫這個動作應該定義爲接口更合適。因爲其他動物也有喫的動作。

6.3 從彙編角度看純虛函數特別之處

1.間接的查看AbstractBase虛表內容

// #define test_call_abstract_virtual_fun 1
class AbstractBase
{
public:
#ifdef test_call_abstract_virtual_fun
AbstractBase()      { CallAbsFunc(); }
void CallAbsFunc()  { AbsFunc(); }
#else
    AbstractBase()      { }
#endif // test_call_abstract_virtual_fun
    virtual void AbsFunc()  = 0;
    virtual void AbsFunc2() = 0;
};
class Child : public AbstractBase
{
public:
    Child()         { AbsFunc(); }
    void AbsFunc()  { cout << "" << endl; }
    void AbsFunc2() { cout << "" << endl; }
};
void test_abstract_virtual_fun()
{
    // 因爲抽象類不能直接實例化,通過子類實例化,反彙編找到AbstractBase找到構造函數
    AbstractBase *child = new Child();
    child->AbsFunc();
}

在24行代碼處下斷點->啓動vs->右鍵菜單->反彙編->快捷鍵F11單步調試。
反彙編的代碼比較多,我就挑出重點的代碼畫圖解釋下:

在反彙編的時候,我們拿到了虛表的地址0x0F82D98,我們把這個地址放到vs2013中的內存窗口中。根據我們前面的學到的知識,就知道AbstractBase應該有兩個虛函數,那麼表中應該有兩個函數指針,如下圖所示。但是你會驚訝的發現表中的兩個函數指針都是0x00f714ab

2.純虛類虛表中不僅有內容還是一樣的?

現在我們很好奇,爲什麼虛表中的內容的是一樣的。還有純虛類的虛函數都沒有任何的實現的,爲什麼虛表中還有內容。還有這個地址到底是個什麼?幹啥的?

那我就在想,如果我可以拿到這個地址直接轉成函數指針,通過函數指針調用就可以了。

  1. 方案一:在純虛父類構造函數中,直接調用純虛函數,編譯失敗error LNK2019: 無法解析的外部符號(因爲純虛函數沒有實現,直接調用沒有任何意義)。方案否決。

  2. 方案二:嘗試拿到純虛父類的vftable地址,但是發現純虛父類不能實例化。方案否決。

  3. 方案三:嘗試在子類構造函數,拿到this指針的,在根據this指針拿到虛表地址。反彙編的代碼看編譯器的代碼先於我的代碼執行,就是說等到了執行我的代碼時候,這個this指針已經不是純虛父類的vftable,而是子類的vftable了。
    貌似進入死衚衕,沒有方案了。

問題回到剛開始時候,我現在是怎麼拿到純虛父類的vftable。我是實例化了子類,然後反彙編F11一步步跟過去的。就是說我是間接的通過子類去獲取純虛父類的vftable,等等,是不是有思路了。

  1. 方案四:在純虛父類中寫一個普通的函數,在構造函數->調用普通的函數->調用純虛函數

在上面的代碼將宏放開#define test_call_abstract_virtual_fun 1,編譯通過ok,接下來可以愉快的玩耍了。
代碼整理好,反彙編走起來。我把重要的調用過程畫圖表示出來,便於理解。

從純虛函數的調用過程來看,調用純虛函數->__purecall->call 0FE51470(19h) ->拋出異常

現在我們大概的猜一猜上面提出的疑問了:

1.純虛函數的確是沒有實現的,而虛表的內容時編譯器塞進去的。

2.純虛函數本來就是不能讓我們調用的,我們現在通過某種手段繞過編譯器了。如果我們直接調用純虛函數,編譯器能夠檢查出來,會報錯error LNK2019: 無法解析的外部符號。而如果我們間接的調用了純虛函數,編譯器也無能爲力,但是編譯器還是道高一尺,它知道自己可能在編譯期間解析不出來,所以編譯器就在虛表中插入__purecall函數,你有幾個虛函數我就插入幾個__purecall函數,當你在運行時調用,我就讓你調用__purecall拋出異常。讓你不能調用你就是不能調用,強行調用我就給你shutdown。現在能夠解釋爲什麼虛表的內容是一樣的。

7.虛函數的缺點

天使與魔鬼是並存的,虛函數在帶來超強的多態特性,但是不可避免的帶來了其他缺點。

1.間接尋址造成的效率慢,在怎麼實現多態特性上彙編角度可以看出來,引入時間複雜度。

2.繼承關係帶來的強耦合關係,父類動子類可能地動山搖,對象關係複雜度上升。

3.體積的增加,尤其是多繼承時體現的更明顯,引入額外的空間複雜。

但是在軟件開發的角度看大大降低軟件的開發週期和維護成本,總的來說瑕不掩瑜。與帶來的多態特性相比,我覺得還是值得。

8.虛函數的外延探索

  1. 在多態中,我們通過對象->虛表指針->虛表->虛函數,最終找到我們想要的函數。簡化的看對象->(中間操作)->虛函數,對象經過中間的一系列操作得到虛函數。這種思路稱爲間接思維,當我們想要某種東西的時候,可能沒法直接獲取或者是直接獲取的成本太高,但是通過間接輕鬆的獲取。這種思路隨處可見,比如:我要喫飯要通過錢去等價交換,我去公司上班通過地鐵過去,計算機中的緩存作用。

  2. 關於實現多態的特性,網上還有利用模板的編譯期多態的特性,有興趣同學可以搜一下。

  3. 設計模式的裏面的套路,就是基於的虛函數多態的特性。

9.擴展資料

《C++深入理解對象模型》
《C++反彙編與逆向分析》

10.總結

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章