虛函數原理及應用

包含虛函數的類中隱含一個指針,叫做vptr(virtual table pointer,虛函數表指針)。 vptr 指向一個vtbl(virtual table,虛函數表)函數指針數組,該數組記錄實際調用的函數地址。對於多繼承的時候,情況稍微複雜一些。如果多個父類都包含虛函數,則類中vptr指針也會有多個。當子類對象實例賦值給父類時會對父類中的vptr進行初始化,使其指向含有實際訪問的vtbl函數指針數組。

#include <iostream>

using namespace std;

class empty
{
};

class base
{
public:
    virtual void f(){cout<<"base, virtual function f()"<<endl;}
};

class derived: public base
{
public:
    virtual void f1(){cout<<"derived, virtual function f1()"<<endl;}
};

int main(void)
{
    base b;
    base *pb;
    
    derived *pd;

    void (* pf)(void);

    pd = new derived();

    cout<<"size of empty class: "<<sizeof(empty)<<endl;
    cout<<"size of base class: "<<sizeof(base)<<endl;
    cout<<"size of derived class: "<<sizeof(derived)<<endl;

    cout<<"base vptr address: "<<(int *)(&b)<<endl;
    cout<<"function address in base: "<<(int *)*(int *)(&b)<<endl;

    pf = (void (*)()) * ((int *)*(int *)(&b));
    (*pf)();
    pf();

    return 0;
}


yongmi@yongmi-hn:~/c$ g++ virtual_class.cpp 
yongmi@yongmi-hn:~/c$ ./a.out 
size of empty class: 1
size of base class: 4
size of derived class: 4
base vptr address: 0xbfc30f50
function address in base: 0x8048c18
base, virtual function f()
base, virtual function f()


空類的大小爲1(每個類實例在內存中都有一個獨一無二的地址,爲了達到這個目的,編譯器往往會給一個空類隱含的加一個字節,這樣空類在實例化後在內存得到了獨一無二的地址),包含虛函數的類大小爲4(隱含一個vptr指針)。

關係如下所示:

vptr---->base:f

(int *)(&b)是vptr的地址,存放的內容爲*(int *)(&b),即vtbl數組地址。
(int *)*(int *)(&b),將vptr地址中的內容轉換爲int型指針,即vtbl數組中保存的值轉換爲int型指針。
(void (*)()) * ((int *)*(int *)(&b)),vtbl數組中保存的指針值轉換爲函數指針。


上面這個實例中可以看出,通過獲取vptr可以通過函數指針來訪問類中的成員函數(即使該成員函數爲私有也能訪問),這一點有違類的訪問控制權限。


C++父類的析構函數應該使用虛函數,否則可能會造成內存泄漏。看下面這個例子:

#include <iostream>

using namespace std;

class base
{
public:
    ~base ()
    {
        cout << "base, ~base()" << endl;
    }
};

class derived:public base
{
private:
    char *name;

public:
    derived ()
    {
        name = new char[10];
    } 

    virtual ~derived ()
    {
        delete []name;
        cout << "derived, ~derived()" << endl;
    }
};

int main (void)
{
    derived d;
    
    base *pb = new derived ();

    delete pb;
}

 
yongmi@yongmi-hn:~/c$ g++ virtual_constructor.cpp
yongmi@yongmi-hn:~/c$ ./a.out 
base, ~base()
derived, ~derived()
base, ~base()

運行結果顯示,類derived的析構函數只調用了一次,造成子類中申請的內存泄漏。將程序修改之後如下:

#include <iostream>

using namespace std;

class base
{
public:
    virtual ~base ()
    {
        cout << "base, ~base()" << endl;
    }
};

class derived:public base
{
private:
    char *name;

public:
    derived ()
    {
        name = new char[10];
    } 

	virtual ~derived ()
    {
        delete []name;
        cout << "derived, ~derived()" << endl;
    }
};

int main (void)
{
    derived d;
    
    base *pb = new derived ();

    delete pb;
}

 
yongmi@yongmi-hn:~/c$ g++ virtual_constructor.cpp
yongmi@yongmi-hn:~/c$ ./a.out 
derived, ~derived()
base, ~base()
derived, ~derived()
base, ~base()

此時程序調用了類derived兩次析構函數,沒有造成內存泄漏。


原因是因爲將子類對象賦給父類時,如果子類對象動態申請了內存空間,而父類析構函數不是虛函數時,子類的析構函數不能調用,所以無法釋放內存空間。




參考資料:

C++ 虛函數表解析

C++中虛函數工作原理和()繼承類的內存佔用大小計算

C++虛函數的原理

C++中基類的析構函數爲什麼要用virtual虛析構函數



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