目錄
一、多態的概念
二、靜態的多態
三、動態的多態
3.1多態的定義
3.2虛函數
四、虛函數的重寫(覆蓋)
4.1虛函數
4.2三同
4.3兩種特殊情況
(1)協變
(2)析構函數的重寫
五、C++11中的final和override
5.1-C++98防止一個類被繼承的方法
5.2-C++11final關鍵字
5.3-C++11override關鍵字
六、重載、覆蓋(重寫)、隱藏(重定義)的對比
七、抽象類(接口類)
7.1概念
7.2接口繼承和實現繼承
八、多態的實現原理
8.1虛函數表
8.2虛函數表的生成
8.3多態的原理
8.4動態綁定與靜態綁定
九、單繼承和多繼承關系的虛函數表
9.1單繼承中的虛函數表
9.2多繼承中的虛函數表
十、問答題
1. 什么是多態?
2. 什么是重載、重寫(覆蓋)、重定義(隱藏)?
3、多態的實現原理?
4、inline可以是虛函數嗎?
5、靜態成員可以是虛函數嗎?
6、構造函數可以是虛函數嗎?
7、析構函數可以是虛函數嗎?
8、對象調用普通成員函數快還是虛函數快?
9.、虛函數表是在什么階段生成的,存在哪的?
10、C++菱形繼承的問題?虛繼承的原理??
11、什么是抽象類?抽象類的作用??
一、多態的概念
?多態指多種形態。不同的對象完成同一件事情,但是結果不同。例如公交刷卡行為:成人刷卡全價,學生刷卡半價。亦或是不同的客戶來消費,金卡會員8折,銀卡會員9折,普通會員無折扣。
二、靜態的多態
靜態的多態是在編譯時產生不同。例如函數重載就是一種靜態的多態行為。看上去是在調用同一個函數,但是會產生不同的行為。
int main()
{int a=1;double b=2.3;std::cout<<a<<std::endl;std::cout<<b<<std::endl;return 0;
}
三、動態的多態
3.1多態的定義
????????動態的多態是在運行時產生不同。
????????構成多態的條件:缺一不可,否則就不構成多態。
1. 必須通過基類的指針或者引用調用虛函數
2. 被調用的函數必須是虛函數,且派生類必須對基類的虛函數進行重寫
3.2虛函數
虛函數:即被virtual修飾的類成員函數稱為虛函數。
class Person
{
public:virtual void BuyTicket() { cout << "買票-全價" << endl;}
};
class Student : public Person
{
public:
virtual void BuyTicket(){cout<<"買票-半價"<<endl;}
};
注意虛函數和虛繼承雖然都使用了virtual關鍵字,但是它們沒有關系。
四、虛函數的重寫(覆蓋)
總結:構成重寫的條件:虛函數 + 三同 + 兩種特殊情況;
4.1虛函數
子類重寫父類虛函數時,子類“三同”函數不寫virtual也構成重寫,但是不規范。C++設計者的初衷是父類寫了virtual,即使子類不寫,也構成多態,那就不會出現內存泄漏的情況了。
父類的 virtual 一定不能省略 – 虛函數的繼承是接口繼承,也就是說,子類中繼承得到的虛函數和父類虛函數的函數接口是完全相同的,而子類如果對虛函數進行重寫,重寫的也只是虛函數的實現,并沒有改變虛函數的接口,所以即使我們不加 virtual 子類虛函數的類型也和父類一樣,是虛函數類型。為了程序的可讀性,我們建議子類虛函數也加上 virtual。
我們在繼承關系中,建議子類虛函數加上 virtual,而可以無腦地將父類析構定義為虛函數。(理由下面會講解)
4.2三同
返回值類型、函數名稱、參數列表均相同。
4.3兩種特殊情況
(1)協變
構成重寫需要成員函數返回值相同,但是存在例外,當返回值是構成父子關系的指針或引用時,它們也構成重寫。但是父類返回值一定要用父指針/父引用,子必須用子指針/子引用,不能顛倒。
class A{};class B : public A {};class Person {public:virtual A* f() {return new A;}};class Student : public Person {public:virtual B* f() {return new B;}};
(2)析構函數的重寫
基類與派生類析構函數的名字不同,其實不然,這里可以理解為編譯器對析構函數的名稱做了特殊處 理,編譯后析構函數的名稱統一處理成destructor。
如果基類的析構函數為虛函數,此時派生類析構函數只要定義,無論是否加virtual關鍵字, 都與基類的析構函數構成重寫,雖然基類與派生類析構函數名字不同。
class Person {public:virtual ~Person() {cout << "~Person()" << endl;}};class Student : public Person {public:virtual ~Student() { cout << "~Student()" << endl; }};只有派生類Student的析構函數重寫了Person的析構函數,下面的delete對象調用析構函
數,才能構成多態,才能保證p1和p2指向的對象正確的調用析構函數。
int main(){Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;}return 0;
父子構造函數構成多態,那就不看p1p2的類型了,p1p2指向哪個對象,就調用哪個對象的析構函數。
五、C++11中的final和override
從上面可以看出,C++對函數重寫的要求比較嚴格,但是有些情況下由于疏忽,可能會導致函數 名字母次序寫反而無法構成重載,而這種錯誤在編譯期間是不會報出的,只有在程序運行時沒有 得到預期結果才來debug會得不償失,因此:C++11提供了override和final兩個關鍵字,可以幫 助用戶檢測是否重寫。
5.1-C++98防止一個類被繼承的方法
class Person
{
public:static Person CreateObj(){//new Person;return Person();}
private:Person(){}
};
class Student : public Person
{
public:
};
int main()
{Person p=Person::CreateObj();return 0;
}
C++98通過把構造函數變為私有的方式,讓子類繼承后根本構造不出父類對象。
但是父類卻可以通過靜態的“偷家”函數構造對象。
5.2-C++11final關鍵字
(1)final修飾類,防止該類被繼承
class Person final
{
public:
};
(2)final:修飾虛函數,表示該虛函數不能再被重寫
class Car{public:virtual void Drive() final {}};class Benz :public Car{public:virtual void Drive() {cout << "Benz-舒適" << endl;}};
5.3-C++11override關鍵字
檢查派生類虛函數是否重寫了基類某個虛函數,如果沒有重寫編譯報錯
class Car{public:virtual void Drive(){}};class Benz :public Car {public:virtual void Drive() override {cout << "Benz-舒適" << endl;}};
六、重載、覆蓋(重寫)、隱藏(重定義)的對比
七、抽象類(接口類)
7.1概念
在虛函數的后面寫上 =0 ,則這個函數為純虛函數。包含純虛函數的類叫做抽象類(也叫接口 類),抽象類不能實例化出對象。派生類繼承后也不能實例化出對象。
只有重寫純虛函數,派生 類才能實例化出對象。純虛函數規范了派生類必須重寫,另外純虛函數更體現出了接口繼承。
class Car{public:virtual void Drive() = 0;};class Benz :public Car{public:virtual void Drive(){cout << "Benz-舒適" << endl;}};class BMW :public Car{public:virtual void Drive(){cout << "BMW-操控" << endl;}};void Test(){Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();}
抽象類的作用是強制子類進行重寫。
7.2接口繼承和實現繼承
普通函數繼承是一種實現繼承,派生類繼承了基類函數,可以使用函數,繼承的是函數的實現。
虛函數的繼承是一種接口繼承,派生類繼承的是基類虛函數的接口,目的是為了重寫,達成 多態,繼承的是接口。接口繼承會繼承父類的類作用限定符和缺省參數。
所以如果不實現多態,不要把函數定義成虛函數。
八、多態的實現原理
8.1虛函數表
這里常考一道筆試題:sizeof(Base)是多少?
class Base{public:virtual void Func1(){cout << "Func1()" << endl;}private:int _b = 1;};
通過觀察測試我們發現b對象是8bytes,除了_b成員,還多一個__vfptr放在對象的前面(注意有些 平臺可能會放到對象的最后面,這個跟平臺有關),對象中的這個指針我們叫做虛函數表指針(v代 表virtual,f代表function)。
一個含有虛函數的類中都至少都有一個虛函數表指針,因為虛函數 的地址要被放到虛函數表中,虛函數表也簡稱虛表。
8.2虛函數表的生成
針對上面的代碼我們做出以下改造
1、我們增加一個派生類Derive去繼承Base
2、Derive中重寫Func1
3、Base再增加一個虛函數Func2和一個普通函數Func3class Base{public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}private:int _b = 1;};class Derive : public Base{public:virtual void Func1(){cout << "Derive::Func1()" << endl;}private:int _d = 2;};int main(){Base b;Derive d;cout << "Derive::Func1()" << endl;return 0;}
通過觀察和測試,我們發現了以下幾點問題:
1. 派生類對象d中也有一個虛表指針,d對象由兩部分構成,一部分是父類繼承下來的成員,虛 表指針也就是存在部分的另一部分是自己的成員。
2. 基類b對象和派生類d對象虛表是不一樣的,這里我們發現Func1完成了重寫,所以d的虛表 中存的是重寫的Derive::Func1,所以虛函數的重寫也叫作覆蓋,覆蓋就是指虛表中虛函數 的覆蓋。重寫是語法的叫法,覆蓋是原理層的叫法。
3. 另外Func2繼承下來后是虛函數,所以放進了虛表,Func3也繼承下來了,但是不是虛函 數,所以不會放進虛表。
4. 虛函數表本質是一個存虛函數指針的指針數組,一般情況這個數組最后面放了一個nullptr。
5. 總結一下派生類的虛表生成:
a.先將基類中的虛表內容拷貝一份到派生類虛表中
b.如果派生 類重寫了基類中某個虛函數,用派生類自己的虛函數覆蓋虛表中基類的虛函數
c.派生類自己 新增加的虛函數按其在派生類中的聲明次序增加到派生類虛表的最后。
6. 這里還有一個童鞋們很容易混淆的問題:虛函數存在哪的?虛表存在哪的? 答:
注意 虛表存的是虛函數指針,不是虛函數,虛函數和普通函數一樣的,都是存在代碼段的,只是 他的指針又存到了虛表中。另外對象中存的不是虛表,存的是虛表指針。那么虛表存在哪的 呢?實際我們去驗證一下會發現vs下是存在代碼段的。
8.3多態的原理
class Person {public:virtual void BuyTicket() { cout << "買票-全價" << endl; }};class Student : public Person {public:virtual void BuyTicket() { cout << "買票-半價" << endl; }};void Func(Person& p){p.BuyTicket();}int main(){Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;}
?1. 觀察下圖的紅色箭頭我們看到,p是指向mike對象時,p->BuyTicket在mike的虛表中找到虛 函數是Person::BuyTicket。
2. 觀察下圖的藍色箭頭我們看到,p是指向johnson對象時,p->BuyTicket在johson的虛表中 找到虛函數是Student::BuyTicket。
3. 這樣就實現出了不同對象去完成同一行為時,展現出不同的形態。
4. 反過來思考我們要達到多態,有兩個條件,一個是虛函數覆蓋,一個是對象的指針或引用調 用虛函數。反思一下為什么?
5. 滿足多態以后的函數調用,不是在編譯時確定的,是運行 起來以后到對象的中取找的。不滿足多態的函數調用時編譯時確認好的。
8.4動態綁定與靜態綁定
?1. 靜態綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態多態, 比如:函數重載
2. 動態綁定又稱后期綁定(晚綁定),是在程序運行期間,根據具體拿到的類型確定程序的具體 行為,調用具體的函數,也稱為動態多態。
九、單繼承和多繼承關系的虛函數表
9.1單繼承中的虛函數表
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;};
9.2多繼承中的虛函數表
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;};
觀察上圖可以看出:多繼承派生類的未重寫的虛函數放在第一個繼承基類部分的虛函數表中。
十、問答題
1. 什么是多態?
多態指多種形態。不同的對象完成同一件事情,但是結果不同。例如公交刷卡行為:成人刷卡全價,學生刷卡半價。亦或是不同的客戶來消費,金卡會員8折,銀卡會員9折,普通會員無折扣。
2. 什么是重載、重寫(覆蓋)、重定義(隱藏)?
函數重載:(1)兩個函數在同一作用域
? ? ? ? ? ? ? ? ? (2)函數名相同,參數列表不同,返回值沒有要求;
重寫: (1)兩個函數必須位于子類和父類中
? ? ? ? ? ?(2)函數名、參數列表、返回值必須相同(協變除外)
? ? ? ? ? ?(3)兩個函數均為虛函數;
隱藏:(1)兩個函數必須位于子類和父類中
? ? ? ? ? ?(2)函數名相同
? ? ? ? ? ?(3)不構成重寫,就構成隱藏。
3、多態的實現原理?
對于多態的實現原理,必須先從構成多態的條件說起:
1、必須通過父類對象的引用或指針當做形參調用虛函數。
2、子類必須完成對父類虛函數的重寫且被調用的函數是虛函數。
? ? ? ? 子類和父類的虛函數表指針、虛函數表、重寫的虛函數的地址均不相同,我們傳入一個父類對象,它使用的是源自父類的虛函數,傳入一個從子類切片而來的父類對象,這個對象中的虛函數是子類重寫的虛函數。雖說這兩個都是父類對象,但是對象體內的虛函數并不是同一個,所以會產生不同的行為,這便是多態的原理。
4、inline可以是虛函數嗎?
?inline可以是虛函數。調用時,如果不構成多態,這個函數就保持inline屬性。如果構成多態,就不具備inline屬性,因為多態是要在運行時去對象的虛函數表里面找虛函數,所以在編譯時,不能使用inline進行展開。
5、靜態成員可以是虛函數嗎?
靜態成員不能是虛函數。因為靜態成員沒有this指針,在外部可以直接使用類名::成員函數的方式對靜態成員函數進行調用,但是調用虛函數需要通過對象才能找到虛函數表,所以靜態成員不能是虛函數。
6、構造函數可以是虛函數嗎?
構造函數不能是虛函數。因為對象的虛函數表指針是在構造函數的初始化列表中進行初始化。(先有雞還是先有蛋的問題)
7、析構函數可以是虛函數嗎?
構函數可以是虛函數,用于處理子類對象交給父類的指針管理的情況。
8、對象調用普通成員函數快還是虛函數快?
如果不構成多態,即使是虛函數,也是在編譯階段確定調用地址,速度一樣快;但是一旦構成多態,編譯器在運行時通過對象去虛函數表中確定虛函數的調用地址,這個時候就是普通函數快了。
9.、虛函數表是在什么階段生成的,存在哪的?
?虛函數表是在編譯階段就生成的,一般情況 下存在代碼段(常量區)的。
10、C++菱形繼承的問題?虛繼承的原理??
注意這里不要把虛函數表和虛基表搞混了。
菱形繼承有數據冗余和二義性的問題。
通過虛基表指針找到虛基表,虛基表中存的數據偏移量。再通過偏移量可以找到有冗余和二義性的數據。
11、什么是抽象類?抽象類的作用??
抽象類又稱接口類。包含純虛函數的類被稱為抽象類,在虛函數后邊加個=0,這個虛函數就被叫做純虛函數。抽象類不能實例化出對象。在現實世界中沒有對應的實物,就可以定義為抽象類。例如職能類、Person類等。
?抽象類強制重寫了虛函數,另外體現接口繼承的關系。子類繼承抽象類后,也變成了抽象類。這就強制用戶對純虛函數進行重寫,對虛函數的重寫是一種接口繼承,子類會繼承虛函數的函數名及缺省值,但不會繼承實現。