在博客多態&虛函數中主要對多態的一些基本概念和虛函數做了介紹,下面,我們來探究一下【虛表】。
含有虛函數的類
- 先來看看含有虛函數的類的大小吧!
class B
{
public:
virtual void Show()
{
cout << _b << endl;
}
public:
int _b;
};
一眼看過去,這個類中只有一個int類型的變量_b,那麼他的大小是不是隻有4個字節呢?我運行了之後發現並不是,它的大小是8個字節。運行結果我這就不看了,下面我們用內存和監視來分析一下對象b的對象模型。
可以看到,當執行完
b._b = 1;
時,對象b的成員_b與對象b的地址偏移了4個字節。而這4個字節裏存放的是一個地址,我們把這個地址叫做虛表指針,它指向一個存放虛函數地址的內存塊。這個內存塊就是虛表。由此,我們可得到b的對象模型
有虛函數的類相比普通類多了4個字節,用來存放虛表指針。在對象模型中,類中的成員變量存放在虛表指針之後。
當類中有多個對象時,這些對象共用同一虛表。(可同時創建兩個對象,在內存裏查看其虛表指針是否相同來驗證)
當類中有多個成員變量時,對象模型中各變量的存放順序按其在類中的聲明順序存放。
當類中有多個虛函數時,虛表中各虛函數存放順序按照其在類中的聲明順序存放。
直接來看例子吧:
class B
{
public:
virtual void Fun3()
{}
virtual void Fun1()
{}
virtual void Fun2()
{}
public:
int _b3;
int _b1;
int _b2;
};
下圖是我根據監視內存畫出來的b的存儲結構
再來看看它的對象模型是怎樣的?
- 需要注意一點
- 類的構造函數在這裏起填充虛表指針的作用
下面我們來看一下繼承(參見博客【繼承】)體系中虛函數的結構如何?
單繼承中的虛函數
- 虛函數無覆蓋
class B
{
public:
B()
:_b(1)
{}
virtual void Fun1()
{
cout << "B::Fun1()";
}
virtual void Fun2()
{
cout << "B::Fun2()";
}
virtual void Fun3()
{
cout << "B::Fun3()";
}
public:
int _b;
};
class D :public B
{
public:
D()
:_d(2)
{}
virtual void Fun4()
{
cout << "D::Fun4()";
}
virtual void Fun5()
{
cout << "D::Fun5()";
}
virtual void Fun6()
{
cout << "D::Fun6()";
}
public:
int _d;
};
typedef void(*FUN_TEST)();
void FunTest()
{
B b;
cout<<sizeof(b)<<endl;
cout << "B vfptr:" << endl;
for (int iIdx = 0; iIdx < 3; ++iIdx)
{
FUN_TEST funTest = (FUN_TEST)(*((int*)*(int *)&b + iIdx)); funTest();
cout << ": " << (int *)funTest << endl;
}
cout << endl;
D d;
cout << sizeof(d) << endl;
cout << "D vfptr:" << endl;
for (int iIdx = 0; iIdx < 6; ++iIdx)
{
FUN_TEST funTest = (FUN_TEST)(*((int*)*(int *)&d + iIdx));
funTest();
cout << ": " << (int *)funTest << endl;
}
}
其中FunTest()函數用於打印各對象中的函數。
先來看看結果吧
d中有兩個成員變量以及一個虛表指針,所以大小爲12.
可以發現,在派生類中,也完全繼承了基類的虛函數,且遵循繼承的存儲結構。
派生類中繼承的基類的虛函數地址與基類中的相同。
- 虛函數有覆蓋
同樣拿上面例子來說,我把派生類中的函數名Fun5改爲Fun3,把Fun6改爲Fun2,並且派生類中的循環次數改爲4,其餘保持不變。再次運行代碼得到如下結果:
基類無變化,但是在派生類中,只打印了派生類的函數Fun2和Fun3.但是我的定義順序明明是Fun3在Fun2前面的呀。怎麼打印結果卻是反的?這是怎麼一回事呢?
當對象d被創建的時候,其虛表指針已經形成,但此時,虛表中存放的是從基類繼承來的虛函數,當系統檢測到派生類中已經對基類的虛函數進行重寫的函數時,就拿該函數去替換虛表中基類對應的虛函數。所以雖然,Fun3定義在Fun2之前,但是替換時,系統從基類的Fun1開始,依次向下檢測,當檢測到Fun2被重寫時,直接拿派生類中的Fun2去替換當前位置上的Fun2。如下圖:
由此,可得出單繼承的對象模型:
虛表的形成:
菱形繼承
前面在繼承中,爲了解決二義性問題,我們引入了虛擬繼承。在虛擬繼承中,派生類中前4字節是偏移量的地址。但引入虛函數之後,我們看到虛表指針也存放於派生類的前4字節。那麼,我們來看看,在菱形繼承中,派生類的對象模型如何?
- 菱形繼承
class B
{
public:
virtual void FunTest1()
{
cout << "B::FunTest1()" << endl;
}
int _b;
};
class C1 :public B
{
public:
void FunTest1()
{
cout << "C1::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "C1::FunTest2()" << endl;
}
int _c1;
};
class C2 :public B
{
public:
virtual void FunTest1()
{
cout << "C2::FunTest1()" << endl;
}
virtual void FunTest3()
{
cout << "C2::FunTest3()" << endl;
}
int _c2;
};
class D :public C1, public C2
{
public:
virtual void FunTest1()
{
cout << "D::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "D::FunTest2()" << endl;
}
virtual void FunTest3()
{
cout << "D::FunTest3()" << endl;
}
virtual void FunTest4()
{
cout << "D::FunTest4()" << endl;
}
int _d;
};
typedef void(*Fun)();
void Printvpf()
{
D d;
cout << sizeof(d) << endl;
d.C1::_b = 1;
d.C2::_b = 2;
d._c1 = 3;
d._c2 = 4;
d._d = 5;
C1& c1 = d;
int* vpfAddr = (int*)*(int*)&c1;
Fun* pfun = (Fun*)vpfAddr;
while (*pfun)
{
(*pfun)();
pfun = (Fun*)++vpfAddr;
}
cout << endl;
C2& c2 = d;
vpfAddr = (int*)*(int*)&c2;
pfun = (Fun*)vpfAddr;
while (*pfun)
{
(*pfun)();
pfun = (Fun*)++vpfAddr;
}
}
來看看結果吧!
結果可見,系統將派生類自己的虛函數放在了其第一個基類C1後面,並且派生類中重寫的虛函數覆蓋了基類的虛函數。
其對象模型如下:
- 菱形虛擬繼承
class B
{
public:
virtual void FunTest1()
{
cout << "B::FunTest1()" << endl;
}
int _b;
};
class C1 :virtual public B
{
public:
void FunTest1()
{
cout << "C1::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "C1::FunTest2()" << endl;
}
int _c1;
};
class C2 :virtual public B
{
public:
virtual void FunTest1()
{
cout << "C2::FunTest1()" << endl;
}
virtual void FunTest3()
{
cout << "C2::FunTest3()" << endl;
}
int _c2;
};
class D :public C1, public C2
{
public:
virtual void FunTest1()
{
cout << "D::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "D::FunTest2()" << endl;
}
virtual void FunTest3()
{
cout << "D::FunTest3()" << endl;
}
virtual void FunTest4()
{
cout << "D::FunTest4()" << endl;
}
int _d;
};
typedef void(*Fun)();
void Printvpf()
{
D d;
cout << sizeof(d) << endl;
d._b = 1;
d._c1 = 2;
d._c2 = 3;
d._d = 4;
C1& c1 = d;
int* vpfAddr = (int*)*(int*)&c1;
Fun* pfun = (Fun*)vpfAddr;
while (*pfun)
{
(*pfun)();
pfun = (Fun*)++vpfAddr;
}
cout << endl;
C2& c2 = d;
vpfAddr = (int*)*(int*)&c2;
pfun = (Fun*)vpfAddr;
while (*pfun)
{
(*pfun)();
pfun = (Fun*)++vpfAddr;
}
B& b = d;
vpfAddr = (int*)*(int*)&b;
pfun = (Fun*)vpfAddr;
while (*pfun)
{
(*pfun)();
pfun = (Fun*)++vpfAddr;
}
}
int main()
{
Printvpf();
return 0;
}
結果如圖:
表示偏移量的地址緊隨虛表指針之後,其次纔是成員變量。各類成員存放遵循繼承規則。
上圖中表示偏移量的第一個數可能有人無法理解爲什麼幾乎是一串f,其實它是負數在內存中的存儲形式。
由此可得對象模型爲: