首先我們要知道函數是有地址的,虛函數也不例外。虛函數的地址存放在虛函數表中,運行期
多態就是通過虛函數和虛函數表來實現的。類的對象內部會有指向類內部虛表的指針,通過這個
指針調用虛函數,虛函數的調用會被編譯器轉換為對虛表的訪問。虛表指針名字也會被編譯器修改,所以在多繼承下,類的內部可能存在多個虛表指針。
==========華麗的分割線==============
虛函數提供了動態多態,但它也是可以被壓制,下面來看下例子。
class A
{
public:
virtual void f(){ std::cout<
};
classB : public A
{
public:
virtual void f(){std::cout<
};
int main()
{
B b ;
b.f(); //因為B重載了f的實現 所以會調用B的f
//那麼怎麼才能調用到基類的f呢 我們可以通過域操作符來進行
b.A::f();
}
==========華麗的分割線==============
單繼承的話派生類中僅有一個虛函數表。這個虛函數表和基類的虛函數表不是一個表。下面來看例子。
class A1
{
public:
A1(int _a1 = 1):a1(_a1){}
virtual void f() {std::cout<
virtual void g() {std::cout<
private:
int a1;
};
class B1:public A1
{
public:
B1(int _a1 = 1, int _b1 = 4):A1(_a1),b(_b1){}
private:
int b;
};
int main()
{
B1 b1;
b1.g();
return 0;
}
如果派生類沒有重寫基類的虛函數那麼派生類中虛函數表的虛函數地址和基類是一樣的。
==========華麗的分割線==============
多繼承情況下,派生類中有多個虛函數表,虛函數表的排列方式和繼承的順序一致,派生類重寫函數將會
覆蓋所有虛函數表的同名內容,派生類自定義的新虛函數將會在第一個類的虛函數表的後面擴充
下面看例子:
class A1
{
public:
A1(int _a1 = 1) : a1(_a1) { }
virtual void f() { cout << "A1::f" << endl; }
virtual void g() { cout << "A1::g" << endl; }
virtual void h() { cout << "A1::h" << endl; }
~A1() {}
private:
int a1;
};
class A2
{
public:
A2(int _a2 = 2) : a2(_a2) { }
virtual void f() { cout << "A2::f" << endl; }
virtual void g() { cout << "A2::g" << endl; }
virtual void h() { cout << "A2::h" << endl; }
~A2() {}
private:
int a2;
};
class A3
{
public:
A3(int _a3 = 3) : a3(_a3) { }
virtual void f() { cout << "A3::f" << endl; }
virtual void g() { cout << "A3::g" << endl; }
virtual void h() { cout << "A3::h" << endl; }
~A3() {}
private:
int a3;
};
class B : public A1, public A2, public A3
{
public:
B(int _a1 = 1, int _a2 = 2, int _a3 = 3, int _b = 4) :A1(_a1), A2(_a2), A3(_a3), b(_b) { }
virtual void f1() { cout << "B::f" << endl; }
virtual void g1() { cout << "B::g" << endl; }
virtual void h1() { cout << "B::h" << endl; }
private:
int b;
};
==========華麗的分割線==============
如果多重繼承的情況下,派生類重載了基類的虛函數,虛表是什麼樣子的呢?
下面我們上代碼看看
class A1
{
public:
A1(int _a1 = 1) : a1(_a1) { }
virtual void f() { cout << "A1::f" << endl; }
virtual void g() { cout << "A1::g" << endl; }
virtual void h() { cout << "A1::h" << endl; }
~A1() {}
private:
int a1;
};
class A2
{
public:
A2(int _a2 = 2) : a2(_a2) { }
virtual void f() { cout << "A2::f" << endl; }
virtual void g() { cout << "A2::g" << endl; }
virtual void h() { cout << "A2::h" << endl; }
~A2() {}
private:
int a2;
};
class A3
{
public:
A3(int _a3 = 3) : a3(_a3) { }
virtual void f() { cout << "A3::f" << endl; }
virtual void g() { cout << "A3::g" << endl; }
virtual void h() { cout << "A3::h" << endl; }
~A3() {}
private:
int a3;
};
class B : public A1, public A2, public A3
{
public:
B(int _a1 = 1, int _a2 = 2, int _a3 = 3, int _b = 4) :A1(_a1), A2(_a2), A3(_a3), b(_b) { }
virtual void f() { cout << "B::f" << endl; }
virtual void g() { cout << "B::g" << endl; }
virtual void h() { cout << "B::h" << endl; }
private:
int b;
};
從編譯器給出的信息我們可以看到在第二個虛函數表中有adjustor{8}的字樣,這就是A類的大小,也就是
說這就是告訴編譯器需要進行8字字節的偏移。當B類用不同的基類指針指向的時候,運行的是不同的基類中的
虛函數,例如A2類指向B的時候虛函數指針是自動跳到B類中的A2類所在的地方的。經過這樣的調整
A1 A2 A3都會指向正確的類的位置。當B類重寫了函數之後 A2 A3的虛函數表所指向的已經不再是簡單的函數指針了
而是一個trunk對象,這就是c++的trunk技術。所謂的trunk就是一段彙編代碼 這段彙編代碼可以適當的偏移來
調整this指針來跳到對應的虛函數中去 並調用這個函數,也就是當使用A1的指針指向B的對象的時候 不需要發生偏移
而使用A2指向B時則需要進行sizeof(A1)字節的偏移,並跳轉到A2中的函數來執行,這就是通過trunk的jump
指令跳轉到這個函數的。
閱讀更多 coder人生 的文章