目錄
一,多態的原理
1,虛函數表與虛函數表指針
2,原理調用
3,動態綁定與靜態綁定
二,抽象類
三,單繼承和多繼承關系的虛函數表
1,單繼承中的虛函數表
2,多繼承中的虛函數表
3,菱形繼承、菱形虛擬繼承
四,繼承和多態常見的經典題型
1,概念邏輯考察
2,問答題
一,多態的原理
1,虛函數表與虛函數表指針
? ? ? ? 虛函數表:存放虛函數指針的表,簡稱虛表。? ? ? ? ? ? 虛函數表指針:指向虛函數表的指針。
????????一個含有虛函數的類中,至少都有一個虛函數表指針__vfptr,因為虛函數的地址要被放到虛函數表中,而虛函數表本質是一個存虛函數指針的指針數組,一般情況這個數組最后面放了一個nullptr。注意:類似于友元函數、靜態函數不能為虛函數,因為它們都不屬于類,即都不是類的成員函數,無法放到虛函數表中。
? ? ? ? 派生類的虛表生成一共有以下步驟:a.先將基類中的虛表內容拷貝一份到派生類虛表中 b.如果派生類重寫了基類中某個虛函數,用派生類自己的虛函數覆蓋虛表中基類的虛函數 c.派生類自己新增加的虛函數按其在派生類中的聲明次序增加到派生類虛表的最后(注意:在vs監視窗口下可能在虛表中看不到新增的虛函數,但是通過內存窗口下可以觀察到)。
#include <iostream>
using namespace std;
class Base
{
public:
?? ?virtual void Fun1() {? ?//虛函數,地址存放到虛表中
?? ??? ?cout << "Base::Fun1()" << endl;
?? ?}
?? ?virtual void Fun2() {? //虛函數,地址存放到虛表中
?? ??? ?cout << "Base::Fun2()" << endl;
?? ?}
?? ?void Fun3() {? //不是虛函數,地址沒有存放到虛表中
?? ??? ?cout << "Base::Fun3()" << endl;
?? ?}
private:
?? ?int _a = 1;
?? ?char _ch = 'a';
};
class Derive : public Base
{
public:
?? ?virtual void Func1() {? //虛函數,與子類后成重寫,將子類的Func1覆蓋
?? ??? ?cout << "Derive::Func1()" << endl;
?? ?}
private:
?? ?int _d = 2;
};
int main()
{
?? ?Base bb;
?? ?Derive dd;
?? ?return 0;
}
原理圖如下:
? ? ? ? 我們通過以上知識來計算下基類與派生類的存儲大小。觀察以下代碼:
#include <iostream>
using namespace std;
class Base
{
public:
?? ?virtual void Fun1() {
?? ??? ?cout << "Base::Fun1()" << endl;
?? ?}
?? ?virtual void Fun2() {
?? ??? ?cout << "Base::Fun2()" << endl;
?? ?}
?? ?void Fun3() {
?? ??? ?cout << "Base::Fun3()" << endl;
?? ?}
private:
?? ?int _a = 1;
?? ?char _ch = 'a';
};
class Derive : public Base
{
public:
?? ?virtual void Func1()
?? ?{
?? ??? ?cout << "Derive::Func1()" << endl;
?? ?}
private:
?? ?int _d = 2;
};
int main()
{
?? ?//因為有了虛函數,所以這里多了虛函數表指針。虛表指針也存儲在類中
?? ?cout << sizeof(Base) << endl; //輸出12?? ?cout << sizeof(Derive) << endl; ?//輸出16
?? ?return 0;
}
? ? ? ? 這里說明一下,虛表指針存放的位置與平臺有關,有些平臺可能會放到對象的最后面,有些平臺可能會放到對象的最前面。這里的測試是放在對象的前面且是32位機器,也就是說虛表指針占用4字節空間,然后這里再根據空間對齊規則,計算出總空間大小。
? ? ? ? 下面,我們研究下虛表存放的區域(注意:這里研究的不是虛表指針存放的區域,即研究虛表指針指向的地址,不是虛表指針本身的地址,這里不要搞錯)。這里可以使用對比法來觀察。具體做法是先輸出各大區域的代表地址,然后輸出虛表地址(即虛表指針),通過對比觀察與哪個區域代表地址相差最小的就是存儲在哪塊區域,因為各大區域的地址相差非常大。我們先創造以下類
class Base
{
public:
?? ?virtual void Fun1() {
?? ??? ?cout << "Base::Fun1()" << endl;
?? ?}
?? ?virtual void Fun2() {
?? ??? ?cout << "Base::Fun2()" << endl;
?? ?}
?? ?void Fun3() {
?? ??? ?cout << "Base::Fun3()" << endl;
?? ?}
private:
?? ?int _a = 1;
?? ?char _ch = 'a';
};
內部結構圖如下:?
?#include <iostream>
using namespace std;
class Base
{
public:
?? ?virtual void Fun1() {
?? ??? ?cout << "Base::Fun1()" << endl;
?? ?}
?? ?virtual void Fun2() {
?? ??? ?cout << "Base::Fun2()" << endl;
?? ?}
?? ?void Fun3() {
?? ??? ?cout << "Base::Fun3()" << endl;
?? ?}
private:
?? ?int _a = 1;
?? ?char _ch = 'a';
};
int main()
{
?? ?Base bb;
?? ?//創造熟為認知的四大區域的代表
?? ?int a = 0;
?? ?int* b = new int;
?? ?static int c = 1;
?? ?const char* d = "a";
?? ?fprintf(stdout, "棧區: %p\n", &a);
?? ?fprintf(stdout, "堆區: %p\n", b);
?? ?fprintf(stdout, "靜態區(數據段): %p\n", &c);
?? ?fprintf(stdout, "常量區(代碼段): %p\n", d);
?? ?//因為在32位下,內存中一個地址占用四個字節,所以這里解引用需解出bb對象開頭的前四字節內容。這里可轉換成int*,一次性可解引用出4個字節
?? ?Base* pb = &bb;
?? ?fprintf(stdout, "虛表存放區域: %p\n", *(int*)pb); ?//地址與常量區代表地址最相近,即虛表存放在常量區中
?? ?return 0;
}
2,原理調用
????????虛函數的重寫在某種意義上來講也叫做覆蓋。我們通常所說的重寫是語法層上的概念,而覆蓋是原理層上的概念,這也就是父類虛函數覆蓋子類虛函數。
? ? ? ? 這里要說明一下,虛表是在編譯的時候就已經生成,但虛表指針是在構造函數中初始化,通常在初始化列表的最開始階段。若平臺將虛表指針放在最后面也不排除在初始化列表的最后才初始化。但一般情況下平臺都將虛表指針放在最前面,這里可通過監視窗口可觀察到。
? ? ? ? 多態以后的函數調用,不是在編譯時確定的,是運行起來以后到對象的中取找的。不滿足多態的函數調用時編譯時是確認好的。
? ? ? ? 多態調用的本質是在運行時去虛函數表中找函數的地址進行調用,因此調用前必須先確定虛表以及虛表指針,虛表指針又是在構造函數階段才生成,多態調用的機制也是在構造函數之后才生效,也就是說多態的調用是在構造函數之后才調用,所以指向父類調用的是父類的虛函數,指向子類調用的是子類的虛函數。普通調用的本質是系統直接通過調用者類型確定函數地址,也就是在某一具體空間作用域中去查找。對于多態調用而言,即便實例化出多個對象,它們的虛表指針以及存放的虛函數地址都是一樣的,如下:
測試一:
#include <iostream>
using namespace std;
class A
{
public:
? ? virtual void func(int val = 1)
? ? { std::cout << "A->" << val << std::endl; }
? ? virtual void test()?
? ? { func(); }
? ? A()
? ? {
? ? ? ? func();?
? ? }
};class B : public A
{
public:
? ? B()
? ? {
? ? ? ? func();
? ? }
? ? virtual void func(int val = 0)
? ? { std::cout << "B->" << val << std::endl; }
};int main(int argc, char* argv[])
{
? ? A* p = new B;
? ? p->test();
? ? //以次輸出A->1 ?B->0 ?B->1
? ? return 0;
}
? ? ? ? 首先,當new B時先調用父類A的構造函數執行func時,由于此時還處于對象構造階段,多態機制還沒有生效,所以,此時執行的func函數為父類的func函數,輸出A->1。構造完父類后執行子類構造函數,又要調用func,同理執行子類的func,輸出B->0。當構造函數結束后再次調用,多態機制生成,后面就正常調用。
測試二:
#include <iostream>
using namespace std;
class A
{
public:
? ? A()?
? ? : m_iVal(0) {?
? ? ? ? test();
? ? }
? ? virtual void func() {
? ? ? ? std::cout << m_iVal << " ";
? ? }
? ? void test() {
? ? ? ? func();
? ? }
public:
? ? int m_iVal;
};
class B : public A
{
public:
? ? B() {?
? ? ? ? test();
? ? }
? ? virtual void func()
? ? {
? ? ? ? ++m_iVal;
? ? ? ? std::cout << m_iVal << " ";
? ? }
};
int main()
{
? ? //以下輸出0 0 1
? ? A a;
? ? B b; //當調用完A的構造后,A中的基表就確定了。
? ? //子類B的虛表是拷貝父類的虛表內容,也就是說子類調用完構造函數之后多態機制就已經生成
? ? return 0;
}
測試三:
#include <iostream>
using namespace std;
class Base
{
public:
?? ?virtual void Fun1() {
?? ??? ?cout << "Base::Fun1()" << endl;
?? ?}
?? ?virtual void Fun2() {
?? ??? ?cout << "Base::Fun2()" << endl;
?? ?}
?? ?void Fun3() {
?? ??? ?cout << "Base::Fun3()" << endl;
?? ?}
private:
?? ?int _a = 1;
?? ?char _ch = 'a';
};
class Derive : public Base
{
public:
?? ?virtual void Func1()
?? ?{
?? ??? ?cout << "Derive::Func1()" << endl;
?? ?}
private:
?? ?int _d = 2;
};
int main()
{
?? ?Base bb1;
?? ?Base bb2;
?? ?Base bb3;?? ?Derive dd1;
?? ?Derive dd2;
?? ?Derive dd3;
?? ?return 0;
}
內部結構圖:
3,動態綁定與靜態綁定
????????1,靜態綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態多態,比如:函數重載
????????2,動態綁定又稱后期綁定(晚綁定),是在程序運行期間,根據具體拿到的類型并確定程序的具體行為,調用具體的函數,也稱為動態多態。
? ? ? ? 以上兩個概念只需了解即可。
二,抽象類
????????在虛函數的后面寫上 =0 ,則這個函數為純虛函數。包含純虛函數的類叫做抽象類(也叫接口 類),抽象類不能實例化出對象。派生類繼承后也不能實例化出對象,只有重寫純虛函數,派生 類才能實例化出對象。也就是說純虛函數規范了派生類必須重寫。
#include <iostream>
using namespace std;
class Car //抽象類
{
public:
?? ?virtual void Drive() = 0 { ?//純虛函數
?? ??? ?cout << "Car" << endl;
?? ?};
};
class Benz : public Car
{
public:
?? ?virtual void Drive() //重寫抽象類的純虛函數,可以被實例化
?? ?{
?? ??? ?cout << "Benz" << endl;
?? ?}
};
class BMW : public Car
{?? ?};
int main()
{
?? ?//重寫純虛函數,實例化成功
?? ?Benz a;
?? ?a.Drive();
?? ?//沒有重寫純虛函數,實例化報錯
?? ?BMW b;
?? ?return 0;
}
? ? ? ? 最后說明一下接口繼承和實現繼承。普通函數的繼承是一種實現繼承,派生類繼承了基類函數,可以使用函數,繼承的是函數的實現。虛函數的繼承是一種接口繼承,派生類繼承的是基類虛函數的接口,目的是為了重寫,達成多態,繼承的是接口。所以如果不實現多態,不要把函數定義成虛函數。
三,單繼承和多繼承關系的虛函數表
1,單繼承中的虛函數表
? ? ? ? 之前說過,在vs監視窗口下可能在虛表中看不到新增的虛函數,這里我們實踐下,如下:
class Base?
{
public:
?? ?virtual void func1() {
?? ??? ?cout << "Base::func1" << endl;
?? ?}
?? ?virtual void func2() {?
?? ??? ?cout << "Base::func2" << endl;?
?? ?}
private:
?? ?int a;
};
class Derive : public Base?
{
public:
?? ?virtual void func1() {?
?? ??? ?cout << "Derive::func1" << endl;?
?? ?}
?? ?virtual void func3() {
?? ??? ?cout << "Derive::func3" << endl;
?? ?}
?? ?virtual void func4() {
?? ??? ?cout << "Derive::func4" << endl;
?? ?}
private:
?? ?int b;
};
????????觀察上圖中的監視窗口中我們發現看不見func3和func4。這里是編譯器的監視窗口故意隱藏了這兩個函數,也可以認為是它的一個小bug。那么我們如何查看d的虛表呢?下面我們使用代碼打印出虛表中的函數。
#include <iostream>
using namespace std;
class Base?
{
public:
?? ?virtual void func1() {
?? ??? ?cout << "Base::func1" << endl;
?? ?}
?? ?virtual void func2() {?
?? ??? ?cout << "Base::func2" << endl;?
?? ?}
private:
?? ?int a;
};
class Derive : public Base?
{
public:
?? ?virtual void func1() {?
?? ??? ?cout << "Derive::func1" << endl;?
?? ?}
?? ?virtual void func3() {
?? ??? ?cout << "Derive::func3" << endl;
?? ?}
?? ?virtual void func4() {
?? ??? ?cout << "Derive::func4" << endl;
?? ?}
private:
?? ?int b;
};
typedef void(*VFPTR) (); //聲明函數指針VFPTR
void PrintVTable(VFPTR vTable[]) {
?? ?for (int i = 0; vTable[i] != nullptr; ++i) { ?//vs下的虛表最后一個存儲單元為nullptr
?? ??? ?cout << "vTable[" << i << "]: " << vTable[i] << endl;
?? ?} ? ?
?? ?cout << endl;
}
int main()
{
?? ?Base b;
?? ?Derive d;
?? ?VFPTR* vTableb = (VFPTR*)(*(int*)&b); ?//用函數二級指針來表示函數指針數組
?? ?PrintVTable(vTableb);
?? ?VFPTR* vTabled = (VFPTR*)(*(int*)&d); ?//與上同理
?? ?PrintVTable(vTabled);
?? ?return 0;
}
? ? ? ? 這里需說明的是這個打印虛表的代碼經常會崩潰,因為編譯器有時對虛表的處理不干凈,虛表最后面沒有放nullptr,導致越界,這是編譯器的問題。我們只需要點目錄欄的-生成-清理解決方案,再編譯就好了。
2,多繼承中的虛函數表
? ? ? ? 在多繼承中,要謹記派生類的虛表內容是將基類中的虛表內容拷貝一份到派生類虛表中。從這里不難發現,在多繼承中,派生類不止有一個虛表,即繼承多少個類就有多少個虛表。當派生類的虛函數往虛表中添加地址時,會往第一個繼承基類虛表中添加。當派生類對象/指針/引用賦值給基類時,這里會將基類以及包含基類虛表的那一部分一并切割。
#include <iostream>
using namespace std;
class Base1 {
public:
?? ?virtual void func1() {?
?? ??? ?cout << "Base1::func1" << endl;
?? ?}
?? ?virtual void func2() {?
?? ??? ?cout << "Base1::func2" << endl;
?? ?}
private:
?? ?int b1;
};
class Base2 {
public:
?? ?virtual void func1() {?
?? ??? ?cout << "Base2::func1" << endl;
?? ?}
?? ?virtual void func2() {?
?? ??? ?cout << "Base2::func2" << endl;
?? ?}
private:
?? ?int b2;
};
class Derive : public Base1, public Base2 {
public:
?? ?virtual void func1() {?
?? ??? ?cout << "Derive::func1" << endl;
?? ?}
?? ?virtual void func3() {?
?? ??? ?cout << "Derive::func3" << endl;
?? ?}
private:
?? ?int d1;
};
typedef void(*VFPTR)();
void PrintVTable(VFPTR vTable[])
{
?? ?cout << " 虛表地址>" << vTable << endl;
?? ?for (int i = 0; vTable[i] != nullptr; ++i) {
?? ??? ?printf(" 第%d個虛函數地址 :0X%x,->", i, vTable[i]);
?? ??? ?VFPTR f = vTable[i];
?? ??? ?f(); //函數指針的調用,調用類中的虛函數
?? ?}
?? ?cout << endl;
}
int main()
{
?? ?Derive d;
?? ?cout << sizeof(Derive) << endl; //輸出20,因為繼承了兩個父類,有兩個虛表?? ?//這里的p1和p2的值不一樣,因為這里要發生切片,將父類包含特有的子類切出來
?? ?Base1* p1 = &d; ?//切出Base1
?? ?Base2* p2 = &d; ?//切出Base2
?? ?
?? ?VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
?? ?PrintVTable(vTableb1);
?? ?//這里要先跳轉到包含Base2的虛表上,然后找到虛表指針
?? ?VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));?
?? ?PrintVTable(vTableb2);
?? ?return 0;
}
????????觀察下面結構圖可以看出:多繼承派生類的未重寫的虛函數放在第一個繼承基類部分的虛函數表中。
3,菱形繼承、菱形虛擬繼承
? ? ? ??實際中我們不建議設計出菱形繼承及菱形虛擬繼承,一方面太復雜容易出問題,另一方面使用這樣的模型訪問基類成員有一定得性能損耗。所以菱形繼承、菱形虛擬繼承的虛表我們可不做研究,一般我們也不需要研究清楚,因為實際中很少用。我們只需知道,菱形繼承往多繼承方向理解,菱形虛擬繼承就很復雜了,這里有很多問題,不必深思。
四,繼承和多態常見的經典題型
? ? ? ? 繼承與多態這方面有許多坑,由于內部結構多變,通常可設計出一些邏輯上的運算。這里我們來一一觀察這方面的典型問題和經典面試問題。
1,概念邏輯考察
1. 下面哪種面向對象的方法可以讓你變得富有(?A?)
????????A: 繼承 B : 封裝 C : 多態 D : 抽象
2.?(?D?)是面向對象程序設計語言中的一種機制。這種機制實現了方法的定義與具體的對象無關,
而對方法的調用則可以關聯于具體的對象。
????????A : 繼承 B : 模板 C : 對象的自身引用 D : 動態綁定
3. 面向對象設計中的繼承和組合,下面說法錯誤的是?(?C?)
????????A:繼承允許我們覆蓋重寫父類的實現細節,父類的實現對于子類是可見的,是一種靜態復
用,也稱為白盒復用
????????B:組合的對象不需要關心各自的實現細節,之間的關系是在運行時候才確定的,是一種動
態復用,也稱為黑盒復用
????????C:優先使用繼承,而不是組合,是面向對象設計的第二原則
????????D:繼承可以使子類能自動繼承父類的接口,但在設計模式中認為這是一種破壞了父類的封
裝性的表現
4. 以下關于純虛函數的說法, 正確的是(?A?)
????????A:聲明純虛函數的類不能實例化對象 ????????B:聲明純虛函數的類是虛基類
????????C:子類必須實現基類的純虛函數 ????????D:純虛函數必須是空函數
5. 關于虛函數的描述正確的是(?B?)
????????A:派生類的虛函數與基類的虛函數具有不同的參數個數和類型? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? B:內聯函數不能是虛函數
????????C:派生類必須重新定義基類的虛函數? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? D:虛函數可以是一個static型的函數
6. 關于虛表說法正確的是( D )? ? ? ? ? ???
????????A:一個類只能有一張虛表
????????B:基類中有虛函數,如果子類中沒有重寫基類的虛函數,此時子類與基類共用同一張虛表
????????C:虛表是在運行期間動態生成的
????????D:一個類的不同對象共享該類的虛表
7. 假設A類中有虛函數,B繼承自A,B重寫A中的虛函數,也沒有定義任何虛函數,則( D )
????????A:A類對象的前4個字節存儲虛表地址,B類對象前4個字節不是虛表地址
????????B:A類對象和B類對象前4個字節存儲的都是虛基表的地址
????????C:A類對象和B類對象前4個字節存儲的虛表地址相同
????????D:A類和B類虛表中虛函數個數相同,但A類和B類使用的不是同一張虛表
8. 下面程序輸出結果是什么? ( A )? ? ? ? ?
#include <iostream>
using namespace std;
class A {
public:
?? ?A(const char* s) { cout << s << endl; }
?? ?~A() {}
};
class B :virtual public A
{
public:
?? ?B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
?? ?C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:
?? ?D(const char* s1, const char* s2, const char* s3, const char* s4) :B(s1, s2), C(s1, s3), A(s1)
?? ?{
?? ??? ?cout << s4 << endl;
?? ?}
};
int main() {
?? ?D* p = new D("class A", "class B", "class C", "class D");
?? ?delete p;
?? ?return 0;
}? ? ?
//菱形虛擬繼承由于只有一個A,所以當開始調用D的構造函數時,會首先調用A的析構函數,在B、C中不會調用A的構造函數。
????????A:class A class B class C class D ????????B:class D class B class C class A
????????C:class D class C class B class A ????????D:class A class C class B class D
9. 多繼承中指針偏移問題?下面說法正確的是( C )? ? ?
class Base1 { public: ?int _b1; };
class Base2 { public: ?int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main() {
?? ?Derive d;
?? ?Base1* p1 = &d;
?? ?Base2* p2 = &d;
?? ?Derive* p3 = &d;
?? ?return 0;
}
????????A:p1 == p2 == p3 ????????B:p1 < p2 < p3 ????????C:p1 == p3 != p2 ????????D:p1 != p2 != p3
10. 以下程序輸出結果是什么( B )
class A
{
public:
?? ?virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
?? ?virtual void test() { func(); }
};class B : public A
{
public:
?? ?void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};int main(int argc, char* argv[])
{
?? ?B* p = new B;
?? ?p->test();
?? ?return 0;
}
????????A: A->0 ????????B: B->1 ????????C: A->1 ????????D: B->0 ????????E: 編譯出錯 ????????F: 以上都不正確
2,問答題
1. 什么是多態?答:多態分為靜態多態和動態多態,靜態多態是編譯器在編譯時就能確定函數調用的是哪個實現,如:函數重載,而動態多態則是在運行時才能確定,如:派生類重寫虛函數時的調用。
2. 什么是重載、重寫(覆蓋)、重定義(隱藏)?答:參考以上圖片內容
3. 多態的實現原理?答:多態的實現原理主要依賴于虛函數表和虛函數表指針,通過虛函數表指針找到虛函數,在運行時進行動態綁定,以具體確定調用哪個函數。
4. inline函數可以是虛函數嗎?答:可以,不過編譯器就忽略inline屬性,這個函數就不再是
inline,因為inline修飾后,若編譯器看成內聯函數,那么此函數是沒有地址的,無法存入虛表中,而虛函數是要放到虛表中去。
5. 靜態成員可以是虛函數嗎?答:不能,因為類中存儲的數據一切調用都需通過this指針,包括虛函數表。靜態成員函數沒有this指針,使用 類型::成員函數 的調用方式無法訪問虛函數表,所以靜態成員函數無法放進虛函數表。
6. 構造函數可以是虛函數嗎?答:不能,因為對象中的虛函數表指針是在構造函數初始化列表
階段才初始化的。
7. 析構函數可以是虛函數嗎?什么場景下析構函數是虛函數?答:可以,并且最好把基類的析
構函數定義成虛函數。因為這里可能存在內存泄漏問題。當使用基類指針指向派生類地址空間時,由于切割,指針指向的是派生類中基類的地址,所以這里結束時默認調用基類析構函數,不會調用派生類析構函數,這將會導致派生類中部分空間沒有釋放,導致內存泄漏問題。因此,這里需使用虛函數,使其調用派生類的析構函數。
8. 對象訪問普通函數快還是虛函數更快?答:首先如果是普通調用,是一樣快的。如果是多態調用,則普通函數快,因為多態調用構成多態,運行時調用虛函數需要到虛函數表中去查找指定地址才能調用,而普通函數直接調用。
9. 虛函數表是在什么階段生成的,存在哪的?答:虛函數表是在編譯階段就生成的,一般情況
下存在代碼段(常量區)的。
10. C++菱形繼承的問題?虛繼承的原理?答:菱形繼承有二義性和數據冗余的問題。在繼承前加上關鍵字virtual的繼承是虛繼承。虛繼承是一種解決多重繼承中菱形繼承問題的方法(注意這里不要把虛函數表和虛基表搞混了。)
11. 什么是抽象類?抽象類的作用?答:包含純虛函數(即在虛函數的后面寫上 =0 )的類叫做抽象類。作用:抽象類強制重寫了虛函數,另外抽象類體現出了接口繼承關系。