1. 程式人生 > >你所不知道的C++ 之 C++虛類模型

你所不知道的C++ 之 C++虛類模型

我們知道,一個C++類如果帶有virtual關鍵字的函式,那麼,它就是一個虛類。虛類都有虛擬函式表。這個虛擬函式表真的存在嗎?能摸得著、看得見嗎?

的確是可以的。下面我們就展示一下如何看到C++類的虛擬函式表。請注意,我使用的系統是Ubutu 10.04, g++ 4.6.3。

下面我們先定義一個簡單的純虛類和它的實現類:

class BaseClassNoDeconstructor {
public:
    virtual void func() = 0;
};


class VirtualClassNoDeconstructor : public BaseClassNoDeconstructor{
public:
    virtual void func() {
        cout << "VirtualClassNoDeconstructor func\n";
    }
};

然後,針對類VirtualClassNoDeconstructor,我們定義兩個等價的struct:
struct VirtualClassNoDeconstructorMember;
struct VirtualClassNoDeconstructorVTable {
    void (*func)(VirtualClassNoDeconstructorMember*);
};
struct VirtualClassNoDeconstructorMember {
    VirtualClassNoDeconstructorVTable * vtable;
};
然後,我們用VirtualClassNoDeconstructorMember來呼叫它:
void dofuncNoDeconstructor(BaseClassNoDeconstructor* bcd) {
    cout <<"======= call by class no deconstructor by vtable: ===\n";
    VirtualClassNoDeconstructorMember * pvcd = (VirtualClassNoDeconstructorMember*)bcd;
    pvcd->vtable->func(pvcd);

    cout <<"======== end =======\n";
}
想看下輸出結果嗎?
======= call by class no deconstructor by vtable: ===
VirtualClassNoDeconstructor func
======== end =======

我想不用多說什麼了。

那麼,對於一個有虛析構造的類,情況又是什麼呢?首先,還是先看簡單的類:

class BaseClass {
public:
    virtual ~BaseClass() { }
    virtual void func() = 0;
};

class VirtualClass : public BaseClass {
public:
    virtual ~VirtualClass(){
        cout << "Virtual Class destory"<<endl;
    }
    virtual void func() { 
        cout << "Virtual Class : v="<<value<<endl;
    }
    VirtualClass(int v) : value(v) { };
private:
    int value;
};
這兩個類有虛的析構造,對應的等價struct則需要定義為:
struct VirtualClassVTable {
    void (*dector)(VirtualClassMember* self);
    void (*delete_obj)(VirtualClassMember* self);
    void (*func)(VirtualClassMember* self);
};

struct VirtualClassMember {
    VirtualClassVTable* vtable;
    int value;
};
看,多了兩個函式:dector和delete_obj。先彆著急,先看看測試程式碼和輸出結果
void dofunc(BaseClass* bc) {
    cout <<"====== call class by vtable: ===\n";
    VirtualClassMember *pvcm = (VirtualClassMember*)bc;
    cout <<"from member value="<< pvcm->value<<endl;
    pvcm->vtable->func(pvcm);
    pvcm->vtable->dector(pvcm);
    printf("VTable %p: func=%p, ~=%p\n",pvcm->vtable, pvcm->vtable->func, pvcm->vtable->dector);

    printf("pvcm->vtable->delete_obj=%p\n", pvcm->vtable->delete_obj);
    printf("pvcm->vtable->dector=%p\n", pvcm->vtable->dector);
    printf("pvcm->vtable->func=%p\n", pvcm->vtable->func);
    cout <<"======== end ======\n";
}

輸出結果呢,則是
====== call class by vtable: ===
from member value=1234
Virtual Class : v=1234
Virtual Class destory
VTable 0x401150: func=0x400970, ~=0x400ce2
pvcm->vtable->delete_obj=0x400d14
pvcm->vtable->dector=0x400ce2
pvcm->vtable->func=0x400970
======== end ======

按照通常的做法,增加了一個析構造,應該增加一個函式才對,而實際上卻增加了兩個。其中一個就是真正的構造,而另外一個,是伴隨他產生的一個刪除函式:delete_obj。

這個函式是由編譯器自動產生的。那麼,他的作用是什麼?為了搞清楚這個問題,我用objdump反編譯程式碼,從其中摘抄了下面一段:

0000000000400d14 <_ZN9BaseClassD0Ev>:
  400d14:	55                   	push   %rbp
  400d15:	48 89 e5             	mov    %rsp,%rbp
  400d18:	48 83 ec 10          	sub    $0x10,%rsp
  400d1c:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
  400d20:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  400d24:	48 89 c7             	mov    %rax,%rdi
  400d27:	e8 b6 ff ff ff       	callq  400ce2 <_ZN9BaseClassD1Ev>
  400d2c:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  400d30:	48 89 c7             	mov    %rax,%rdi
  400d33:	e8 d8 fb ff ff       	callq  400910 <_ZdlPv@plt>
  400d38:	c9                   	leaveq 
  400d39:	c3                   	retq  
這一段彙編程式碼的地址正是delete_obj函式指向的地址400d14。請看400d27位置的callq指令,呼叫的地址是400ce2。這個地址是dector的地址,即析構造的地址。再請看400d33的callq指令。_ZdlPv@plt表示什麼意思呢?@plt表示這是一個引入了其他動態庫的函式。_ZdlPv用c++filt轉換後,就是名字"operator delete(void*)"。

在這裡,也提醒下各位,當使用虛類的時候,一定要帶上虛的析建構函式。否則在刪除一個虛類物件時,很可能會發生記憶體的洩漏!