1.無虛函數的單繼承
class A
{
public:
A():_ai(1){}
void Afun();
static void static_fun();
static int _val;
private:
int _ai;
};
class B:public A
{
public:
B():_bi(2){}
void Bfun();
private:
int _bi;
};
int main()
{
A a;
B b;
cout<<sizeof(A)<<endl;//4
cout<<sizeof(B)<<endl;//8
return 0;
}
上述代碼內存分佈:
我們可以調試在內存中看一下:(VS2008調試,本文後續都在此環境)
2.有虛函數的單繼承
虛函數是C++多態實現的一種形式,那麼,在虛函數中,編譯器是怎麼知道什麼時候調用合適的函數呢?
編譯器爲每個包含虛函數的類創建一個表(虛函數表/VTABLE)。虛表中存放着類裏面虛函數的地址。在每個虛函數的類中,有一個指針指向虛表,稱爲vpointer(VPTR),這個指針指向虛表。當進行多態調用時,編譯器通過VPTR在虛表中通過函數地址來查找函數代碼,這樣就能正確的調用函數實行動態編譯。看下面代碼:
class A
{
public:
A():_a(1){}
virtual void fun1(){};
virtual void fun2(){};
private:
int _a;
};
int main()
{
A a;
cout<<sizeof(A)<<endl;
return 0;
}
類A的大小爲8說明了A裏面還有別的東西,在此假設爲VPTR。調試上述代碼 ,查看對象a的內存:
可以看到 ,對象a的地址開始4個字節處存放的是VPRT。
把VPRT的那串地址拿出來在內存中look一下,VS2008爲小端存儲
那我們不妨通過虛表裏面的函數地址調用一下虛函數:
typedef void (* pfun)();
void PrintVirtual(pfun *_pfun)
{
while(*_pfun)
{
(*_pfun)();
++_pfun;
}
}
void test()
{
A a;
pfun* p = (pfun*)*((int*)(&a));//拿出對象a前4個字節,把它強轉爲函數指針
PrintVirtual(p);
}
成功的通過虛表調用了對象a裏面的虛函數:
清楚了以上的虛函數表,下面研究有虛函數的單繼承,看代碼:
class Base
{
public:
Base():_b(1){}
virtual void fun1()
{
cout<<"Base::fun1()"<<endl;
}
virtual void fun2()
{
cout<<"Base::fun2()"<<endl;
}
private:
int _b;
};
class Derive:public Base
{
public:
Derive():_d(2){}
virtual void fun1()
{
cout<<"Derive::fun1()"<<endl;
}
virtual void fun3()
{
cout<<"Derive::fun3()"<<endl;
}
virtual void fun4()
{
cout<<"Derive::fun4()"<<endl;
}
private:
int _d;
};
int main()
{
Base b;
Derive d;
cout<<sizeof(Base)<<endl;//8
cout<<sizeof(Derive)<<endl;//12
return 0;
}
按照前面虛表的知識,上面代碼中的理論上派生類Derive的大小是基類大小+派生類成員大小+派生類VPRT = 8+4+4 = 16
,但是上面的結果卻是12,這是因爲派生類並沒有生成自己的虛表。派生類真實的內存佈局爲:
調試程序,對Derive類對象d的內存佈局進行分析:
用上面的打印函數打印驗證:
這裏,編譯器是怎麼做的呢?
先將基類虛表裏的內容拷貝一份,如果派生類對基類中的虛函數進行重寫,使用派生類的虛函數替換相同偏移位置的基類虛函數,如果派生類中增加自己的虛函數,按照其在派生類的聲明次序,放在上述虛函數之後。