1、虛函數簡介
虛函數的實現要求對象攜帶額外的信息,這些信息用於在運行時確定該對象應該調用哪一個虛函數。典型情況下,這一信息具有一種被稱為vptr(virtual table pointer,虛函數表指針)的指針的形式。vptr 指向一個被稱為 vtbl(virtual table,虛函數表)的函數指針數組,每一個包含虛函數的類都關聯到
vtbl。當一個對象調用了虛函數,實際的被調用函數通過下面的步驟確定:找到對象的 vptr 指向的 vtbl,然後在 vtbl 中尋找合適的函數指針。
虛擬函數的地址翻譯取決於對象的內存地址,而不取決於數據類型(編譯器對函數調用的合法性檢查取決於數據類型)。如果類定義了虛函數,該類及其派生類就要生成一張虛擬函數表,即vtable。而在類的對象地址空間中存儲一個該虛表的入口,占4個字節,這個入口地址是在構造對象時由編譯器寫入的。所以,由於對象的內存空間包含了虛表入口,編譯器能夠由這個入口找到恰當的虛函數,這個函數的地址不再由數據類型決定了。故對於一個父類的對象指針,調用虛擬函數,如果給他賦父類對象的指針,那麼他就調用父類中的函數,如果給他賦子類對象的指針,他就調用子類中的函數(取決於對象的內存地址)。
2、C++中含有虛函數的內存分布
涉及到虛函數的內存分布往往比較復雜,除了考慮其本身所帶來的額外的內存開銷,還要考慮繼承等所帶來的問題。針對這一方面,我們按照如下的步驟逐一解決。
1)、單個含有虛函數的類
2)、基類含有虛函數,使用普通繼承,派生類中不含虛函數
3)、基類含有虛函數,使用普通繼承,派生類中含有虛函數
4)、基類不含有虛函數,使用虛繼承,派生類中不含虛函數
5)、基類不含虛函數,使用虛繼承,派生類中含有虛函數
6)、基類含有虛函數,使用虛繼承,派生類中不含虛函數
7)、基類含有虛函數,使用虛繼承,派生類中含有虛函數
8)、基類含有虛函數,使用虛繼承,向下派生多次
9)、基類含有虛函數,多繼承
2.1 含有虛函數的單個類
#includetemplate class CPoint { public: CPoint() { _x = 0; _y = 0; _z = 0; } virtual void setX(T newX) { //std::cout << "CPoint setX" << std::endl; _x = newX; } virtual void setY(T newY) { _y = newY; } virtual void setZ(T newZ = 0) { _z = newZ; } virtual T getX() const { return _x; } virtual T getY() const { return _y; } virtual T getZ() const { return _z; } protected: T _x; T _y; T _z; };
void main() { CPointm_Point; std::cout <<"CPoint:"<< sizeof(m_Point) << std::endl; std::cin.get(); }
上述的代碼輸出為32,一方面和內存布局有關,另一方面還和內存對齊有關。類模板實例化為double,構建一個對象,對象中有三個數據成員,每個數據成員占8字節。
m_Point對象的內存布局如上圖所示,可以看到m_Point內部除了三個成員變量之外,還有一個_vfptr,_vfptr是一個虛函數表的指針,保存的是虛函數表的地址。m_Point內部一共有5個虛函數,所以對應的虛函數表中便有5個與虛函數對應得地址。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHA+08nT2tDpuq/K/bHt1rjV69W8vt00uPbX1r3ao6yyosfStKbT2sDgtcTE2rTmtdjWt8bwyry0pqOsy/nS1NX7uPbA4NK7ubLVvL7dMzK49tfWvdqhozwvcD4KPHA+PGJyPgo8L3A+CjxwPjIuMrv5wOC6rNPQ0Om6r8r9o6zKudPDxtXNqLzMs9CjrMXJyfrA4NbQsru6rNDpuq/K/TwvcD4KPHA+0N64xMnPw+a1xLT6wuujrLXDtb3I58/CtcTE2sjdPC9wPgo8cD48cHJlIGNsYXNzPQ=="brush:java;">#include
void main()
{
CPoint
整個類的大小為32字節,我們看一下內存分布就明白了
可以看到m_Point2D的內存布局和m_Point的內存布局很類似。一個虛函數表指針,然後三個成員變量。虛函數表中的內容和m_Point中的一摸一樣。這是因為CPoint2D 是從CPoint繼承過來的。
2.3基類含有虛函數,使用普通繼承,派生類中含有虛函數
繼續修改上面的代碼,得到如下的內容
#includetemplate class CPoint { public: CPoint() { _x = 0; _y = 0; _z = 0; } virtual void setX(T newX) { //std::cout << "CPoint setX" << std::endl; _x = newX; } virtual void setY(T newY) { _y = newY; } virtual void setZ(T newZ = 0) { _z = newZ; } virtual T getX() const { return _x; } virtual T getY() const { return _y; } virtual T getZ() const { return _z; } protected: T _x; T _y; T _z; }; template class CPoint2D : public CPoint { public: CPoint2D() { _x = 0; _y = 0; _z = 0; } CPoint2D(T x, T y, T z = 0) { _x = x; _y = y; _z = z; } CPoint2D(const CPoint2D &point2D) { _x = point2D.getX(); _y = point2D.getY(); _z = point2D.getZ(); } const CPoint2D& operator = (const CPoint2D& point2D) { if (this == &point2D) return *this; _x = point2D.getX(); _y = point2D.getY(); _z = point2D.getZ(); } void operator +(const CPoint2D& point2D) { _x += point2D.getX(); _y += point2D.getY(); _z += point2D.getZ(); } void operator -(const CPoint2D &point2D) { _x -= point2D.getX(); _y -= point2D.getY(); _z -= point2D.getZ(); } virtual T getZ() const { std::cout << "CPoint2D:"< ::getZ()) << std::endl; return 0; } virtual void setZ(T newZ = 0) { //std::cout << "CPoint2D:" << sizeof(CPoint2D::setZ()) << std::endl; _z = 0; } };
void main() { CPoint上面的代碼輸出內容如下所示:m_Point; CPoint2D m_Point2D(0.0,0.0); std::cout <<"CPoint:"<< sizeof(m_Point) << std::endl; std::cout <<"CPoint2D:"<< sizeof(m_Point2D)<< std::endl; std::cout <<"CPoint2D::getZ:"<< sizeof(&CPoint2D ::getZ) << std::endl; std::cin.get(); }
輸出的內容和之前派生類中沒有虛函數的一樣,但是內存布局發生了變化。變化體現在_vfptr中,_vfptr中有4個地址是和CPoint中的一樣,2個不一樣,這是因為在CPoint2D中,重寫了CPoint中的兩個虛函數,從而派生類中的虛函數覆蓋了父類中的虛函數。這地方的重寫不僅僅是函數名相同,還要保證函數的參數類型,參數個數,函數的返回形式也和基類中的一致。
從上面的例子中我們可以得出以下的結論:
1)、類中一旦出現虛函數,編譯器便會給其分配一個虛函數表,虛函數表指針的大小和編譯器有關。
2)、派生類中如果對父類的虛函數進行了重寫,那麼派生類中的虛函數會覆蓋父類的虛函數,體現在上圖的虛函數表中的地址發生了變化。
3)、虛函數表指針總是處於類的地址的開始處,所以在計算類的大小時要注意這一點。
2.4基類不含有虛函數,使用虛繼承,派生類中不含虛函數
這一次使用前一章節的代碼,對前一章節的代碼進行修改,得到如下的內容
#includeusing namespace std; class CBase { //public public: CBase() { } }; class CBaseClass { //private members private: int nCount; //public members public: //private member funcs private: CBaseClass(const CBaseClass &base) { } CBaseClass &operator = (const CBaseClass& base) { return *this; } //public members public: CBaseClass(int count = 0) { nCount = count; } ~CBaseClass() { } }; class CBaseClassNew { //private members private: int nCount; //public members public: int nNewCount; //private member funcs private: CBaseClassNew(const CBaseClassNew &base) { } CBaseClassNew &operator = (const CBaseClassNew& base) { return *this; } //public members public: CBaseClassNew(int count = 0) { nCount = count; } ~CBaseClassNew() { } }; class CDerivedClass : virtual public CBaseClass { //private members: private: int nDeriveCount; //public members public: int nCurrentNum; //private member funcs private: CDerivedClass(const CDerivedClass& derived) { } CDerivedClass & operator = (const CDerivedClass &derived) { return *this; } //public member funcs public: CDerivedClass(int nDerived = 0) { nDeriveCount = nDerived; nCurrentNum = 0; } };
void main() { CBase base; cout << "base Size:" << sizeof(base) << endl; CBaseClass baseClass(10); cout << "baseClass Size:" << sizeof(baseClass) << endl; CDerivedClass derivedClass(12); cout << "derivedClass Size:" << sizeof(derivedClass) << endl; cin.get(); }
CBase 中只有一個構造函數,所以占一個字節
CBaseClass中有一個成員變量,為int型,所以占4個字節
CDerivedClass中自身的2個成員變量和基類中的1個成員變量均是int型,一共12個字節。CDerivedClass使用的是虛繼承,這導致在派生類中會產生一個指針指向基類,所以派生類的大小為14字節。
其內存分布如下圖所示:
因為篇幅太長,剩下的內容後面再說了。