C++ 中的虛函數

什么是虛函數

在我看來,虛函數是在基類中聲明并由派生類重新定義的成員函數(如果不是純虛函數也可以不覆蓋)。當用基類的指針或者引用訪問虛函數時會發生「動態綁定」找到真正的函數地址。

一個基本的例子就是這樣:


#include <iostream>
using namespace std;
 
class base {
public:
    virtual void print()
    {
        cout << "print base class" << endl;
    }
 
    void show()
    {
        cout << "show base class" << endl;
    }
};
 
class derived : public base {
public:
    void print()
    {
        cout << "print derived class" << endl;
    }
 
    void show()
    {
        cout << "show derived class" << endl;
    }
};
 
int main()
{
    base* bptr;
    derived d;
    bptr = &d;
 
    // virtual function, binded at runtime
    bptr->print();
 
    // Non-virtual function, binded at compile time
    bptr->show();
}

注意:如果我們在基類中定義了虛函數,函數在派生類中自動被視為虛函數。比如我們看這個代碼:

#include <iostream>

using namespace std;

class base {
private:
public:
    virtual void print() const {
        cout << "base" << '\n';
    }
};

class A : public base {
public:
    void print() const {
        cout << "A" << '\n';
    }
};

class C : public A {
public:
    void print() const {
        cout << "C" << '\n';
    }
};

int main() {
    base *b;
    b = new A();
    b->print();

    b = new C();
    b->print();

    return 0;
}

結果:

A
C

A 中沒定義 print 是虛函數,但是還是調用到了 C 的 print。證明 C 這個類中有一個「虛函數表」,也就證明了 print 還是一個虛函數。

虛函數的實現

之前提到了虛函數表,什么是虛函數表(VTABLE)?以及虛函數怎么實現的?

如果一個類包含一個虛函數,那么編譯器本身會做兩件事:

  • 如果創建了該類的對象,則將虛擬指針(VPTR)作為該類的數據成員指向該類的 VTABLE。對于創建的每一個新對象都會插入一個新的虛擬指針作為該類的數據成員。
  • 無論對象是否創建,一個類的 VTABLE 都會存在,本質上就是一個靜態的函數指針數組,數組的每個單元包含該類中的每個虛函數的地址。

什么時候編譯器會選擇使用虛表

我覺得就兩個條件:

  • 該類的某一個父類的指針或者引用
  • 調用虛函數

我理解的虛函數為什么會讓代碼變慢的原因

以一個經典的 ARM 五級流水線為例子,一條指令的執行有五個過程:1. Fetch 2. Decode 3. Excute 4. Memory 5. Write

在編譯器確定函數的情況下,在執行跳轉的時候,函數的指令就已經可以放在流水線里面了。但是虛函數必須得等到從虛表中拿到函數地址才能繼續執行,不能提前運行函數的指令。所以就需要空出幾個 CPU 周期,所以會變慢。

但是真正的瓶頸還是 IO 這一點消耗應該也不算什么。

虛析構函數有什么作用

我們來看這個例子:

#include<iostream>

using namespace std;

class base {
public:
    base(){
        cout<<"Constructing base \n";
    }
    ~base() {
        cout<<"Destructing base \n";
    }
};

class derived: public base {
public:
    derived() {
        cout<<"Constructing derived \n";
    }
    ~derived() {
        cout<<"Destructing derived \n";
    }
};

int main() {
    derived *d = new derived();
    base *b = d;
    delete b;
    return 0;
}

結果是:

Constructing base 
Constructing derived 
Destructing base

沒有正確釋放派生類的對象。

如果給基類的西溝函數加上 virtual 就會正確釋放 派生類的對象。所以為了避免內存泄漏,任何時候在類中有虛函數時,都應該立即添加虛析構函數(即使它什么都不做)。

#include<iostream>

using namespace std;

class base {
public:
    base(){
        cout<<"Constructing base \n";
    }
    virtual ~base() {
        cout<<"Destructing base \n";
    }
};

class derived: public base {
public:
    derived() {
        cout<<"Constructing derived \n";
    }
    ~derived() {
        cout<<"Destructing derived \n";
    }
};

int main() {
    derived *d = new derived();
    base *b = d;
    delete b;
    return 0;
}

參考

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 類模板 函數模板 成員模板(member template) 成員模板其實就是一個類里面使用了一個模板函數。使用模...
    madao756閱讀 261評論 0 1
  • 我們知道,在同一類中是不能定義兩個名字相同、參數個數和類型都相同的函數的,否則就是“重復定義”。但是在類的繼承層次...
    踩在浪花上00閱讀 456評論 0 1
  • 多態性 多態性指相同對象收到不同消息或不同對象收到相同消息時產生不同的實現動作。C++支持兩種多態性:編譯時多態性...
    陳星空閱讀 5,008評論 0 2
  • 1. 析構函數和虛析構函數 如果基類的析構函數是虛的,那么它的派生類的析構函數都是虛的 這將導致:當派生類析構的時...
    杰倫哎呦哎呦閱讀 2,502評論 0 2
  • C++虛函數 C++虛函數是多態性實現的重要方式,當某個虛函數通過指針或者引用調用時,編譯器產生的代碼直到運行時才...
    小白將閱讀 1,755評論 4 19