准備工作
1、VS2012使用命令行選項查看對象的內存布局
微軟的Visual Studio提供給用戶顯示C++對象在內存中的布局的選項:/d1reportSingleClassLayout。使用方法很簡單,直接在[項目P]選項下找到“visual屬性”后點擊即可。切換到cpp文件所在目錄下輸入如下的命令即可
c1 [filename].cpp /d1reportSingleClassLayout[className]
其中[filename].cpp就是我們想要查看的class所在的cpp文件,[className]指我們想要查看的class的類名。(下面舉例說明...)
虛繼承和虛函數是完全無相關的兩個概念。
虛繼承是解決C++多重繼承問題的一種手段,從不同途徑繼承來的同一基類,會在子類中存在多份拷貝。這將存在兩個問題:
其一,浪費存儲空間;
第二,存在二義性問題,通常可以將派生類對象的地址賦值給基類對象,實現的具體方式是,將基類指針指向繼承類(繼承類有基類的拷貝)中的基類對象的地址,但是多重繼承可能存在一個基類的多份拷貝,這就出現了二義性。
虛繼承可以解決多種繼承前面提到的兩個問題:
虛繼承底層實現原理與編譯器相關,一般通過虛基類指針和虛基類表實現,每個虛繼承的子類都有一個虛基類指針(占用一個指針的存儲空間,4字節)和虛基類表(不占用類對象的存儲空間)(需要強調的是,虛基類依舊會在子類里面存在拷貝,只是僅僅最多存在一份而已,並不是不在子類里面了);當虛繼承的子類被當做父類繼承時,虛基類指針也會被繼承。
實際上,vbptr指的是虛基類表指針(virtual base table pointer),該指針指向了一個虛基類表(virtual table),虛表中記錄了虛基類與本類的偏移地址;通過偏移地址,這樣就找到了虛基類成員,而虛繼承也不用像普通多繼承那樣維持着公共基類(虛基類)的兩份同樣的拷貝,節省了存儲空間。
在這里我們可以對比虛函數的實現原理:他們有相似之處,都利用了虛指針(均占用類的存儲空間)和虛表(均不占用類的存儲空間)。
虛基類依舊存在繼承類中,只占用存儲空間;虛函數不占用存儲空間。
虛基類表存儲的是虛基類相對直接繼承類的偏移;而虛函數表存儲的是虛函數地址。
補充:
1、D繼承了B,C也就繼承了兩個虛基類指針
2、虛基類表存儲的是,虛基類相對直接繼承類的偏移(D並非是虛基類的直接繼承類,B,C才是)
#if 0 //測試虛表的存在 #include <iostream> using namespace std; class A { int i = 10; int ia = 100; void func() {} virtual void run() { cout << "A::run()" << endl; } virtual void run1() { cout << "A::run1()" << endl; } virtual void run2() { cout << "A::run2()" << endl; } }; class B : public A { virtual void run() { cout << "B::run()" << endl; } virtual void run1() { cout << "B::run1()" << endl; } }; class C :public A { virtual void run() { cout << "C::run()" << endl; } virtual void run1() { cout << "C::run1()" << endl; } virtual void run3() { cout << "C::run3()" << endl; } }; class D :/*virtual*/ public A { virtual void run() { cout << "D::run()" << endl; } virtual void run1() { cout << "D::run1()" << endl; } virtual void run2() { cout << "D::run2()" << endl; } virtual void run3() { cout << "D::run3()" << endl; } }; int test() { cout << sizeof(A) << endl << sizeof(B) << endl << sizeof(C) << endl << sizeof(D) << endl; cout << sizeof(long long) << endl; //A * pA = new D; D d; //d.run(); typedef void(*Function)(void); int ** pVtable = (int **)&d; #if 0 int * pVtable = (int*)&d; int vtaleAdress = *pVtable; int * ppVtable = (int*)vtaleAdress; int func1 = *ppVtable; Function f1 = (Function)func1; f1() #endif //pVtable[0][0] for (int idx = 0; pVtable[0][idx] != NULL; ++idx) { Function f = (Function)pVtable[0][idx]; f(); } //cout << (int)pVtable[1] << endl; //cout << (int)pVtable[2] << endl; getchar(); return 0; } int main(void) { test(); return 0; } #endif
測試一、二:單個繼承的不同情況
#if 0 // 測試一:單個虛繼承,不帶虛函數 // 虛繼承與繼承的區別 // 1. 多了一個虛基指針 // 2. 虛基類位於派生類存儲空間的最末尾 // 測試二:單個虛繼承,帶虛函數 // 1.如果派生類沒有自己的虛函數,此時派生類對象不會產生 // 虛函數指針 // 2.如果派生類擁有自己的虛函數,此時派生類對象就會產生自己本身的虛函數指針, // 並且該虛函數指針位於派生類對象存儲空間的開始位置 // #pragma vtordisp(off) #include <iostream> using std::cout; using std::endl; class A { public: A() : _ia(10) {} //virtual void f() { cout << "A::f()" << endl; } private: int _ia; }; class B : virtual public A { public: B() : _ib(20) {} void fb() { cout << "A::fb()" << endl; } virtual void f() { cout << "B::f()" << endl; } #if 1 virtual void fb2() { cout << "B::fb2()" << endl; } #endif private: int _ib; }; int main(void) { cout << sizeof(A) << endl; cout << sizeof(B) << endl; B b; getchar(); return 0; } #endif
測試三:多重繼承
// 測試三:多重繼承(帶虛函數) // 1. 每個基類都有自己的虛函數表 // 2. 派生類如果有自己的虛函數,會被加入到第一個虛函數表之中 // 3. 內存布局中, 其基類的布局按照基類被聲明時的順序進行排列 // 4. 派生類會覆蓋基類的虛函數,只有第一個虛函數表中存放的是 // 真實的被覆蓋的函數的地址;其它的虛函數表中存放的並不是真實的 // 對應的虛函數的地址,而只是一條跳轉指令 #if 1 #pragma vtordisp(off) #include <iostream> using std::cout; using std::endl; class Base1 { public: Base1() : _iBase1(10) {} /*virtual*/ void f() { cout << "Base1::f()" << endl; } /*virtual*/ void g() { cout << "Base1::g()" << endl; } /*virtual*/ void h() { cout << "Base1::h()" << endl; } private: int _iBase1; }; class Base2 { public: Base2() : _iBase2(100) {} virtual void f() { cout << "Base2::f()" << endl; } /*virtual*/ void g() { cout << "Base2::g()" << endl; } /*virtual*/ void h() { cout << "Base2::h()" << endl; } private: int _iBase2; }; class Base3 { public: Base3() : _iBase3(1000) {} virtual void f() { cout << "Base3::f()" << endl; } /*virtual*/ void g() { cout << "Base3::g()" << endl; } /*virtual*/ void h() { cout << "Base3::h()" << endl; } private: int _iBase3; }; class Derived : virtual public Base1 //, virtual public Base2 //, public Base3 { public: Derived() : _iDerived(10000) {} void f() { cout << "Derived::f()" << endl; } /*virtual*/ void g1() { cout << "Derived::g1()" << endl; } private: int _iDerived; }; int main(void) { Derived d; Base1 b1; //Base1 *pBase1 = &b1; //Base2 * pBase2 = &d; //Base3 * pBase3 = &d; Derived * pDerived = &d; //pBase2->f(); cout << "sizeof(d) = " << sizeof(d) << endl; cout << "&Derived = " << &d << endl; // 這三個地址值是不一樣的 //cout << "pBase1 = " << pBase1 << endl; //cout << "pBase2 = " << pBase2 << endl; // //cout << "pBase3 = " << pBase3 << endl; // getchar(); return 0; } #endif
測試四:鑽石型繼承
// 測試四:鑽石型虛繼承(菱形繼承) //虛基指針所指向的虛基表的內容: // 1. 虛基指針的第一條內容表示的是該虛基指針距離所在的子對象的首地址的偏移 // 2. 虛基指針的第二條內容表示的是該虛基指針距離虛基類子對象的首地址的偏移 #if 0 #pragma vtordisp(off) #include <iostream> using std::cout; using std::endl; class B { public: B() : _ib(10), _cb('B') {} virtual void f() { cout << "B::f()" << endl; } virtual void Bf() { cout << "B::Bf()" << endl; } private: int _ib; char _cb; }; class B1 : virtual public B { public: B1() : _ib1(100), _cb1('1') {} virtual void f() { cout << "B1::f()" << endl; } #if 1 virtual void f1() { cout << "B1::f1()" << endl; } virtual void Bf1() { cout << "B1::Bf1()" << endl; } #endif private: int _ib1; char _cb1; }; class B2 : virtual public B { public: B2() : _ib2(1000), _cb2('2') {} virtual void f() { cout << "B2::f()" << endl; } #if 1 virtual void f2() { cout << "B2::f2()" << endl; } virtual void Bf2() { cout << "B2::Bf2()" << endl; } #endif private: int _ib2; char _cb2; }; class D : public B1, public B2 { public: D() : _id(10000), _cd('3') {} virtual void f() { cout << "D::f()" << endl; } #if 1 virtual void f1() { cout << "D::f1()" << endl; } virtual void f2() { cout << "D::f2()" << endl; } virtual void Df() { cout << "D::Df()" << endl; } #endif private: int _id; char _cd; }; int main(void) { D d; cout << sizeof(d) << endl; getchar(); return 0; } #endif
道友可以自己將嘗試每種情況下程序內存分布的情況,以便更清晰的認識,虛函數與虛繼承。