本篇總結(jié)C++
中多態(tài)的基本內(nèi)容和原理實(shí)現(xiàn)和一些邊角內(nèi)容
多態(tài)的概念
首先要清楚多態(tài)是什么,是用來做什么的?
多態(tài)從字面意思來講,就是多種形態(tài),完成一個(gè)事情,不同的人去完成會(huì)有不同的結(jié)果和狀態(tài),這樣的情況就叫做多態(tài)
多態(tài)的定義
多態(tài)是不同繼承關(guān)系的類對(duì)象,在調(diào)用一個(gè)函數(shù)的時(shí)候會(huì)產(chǎn)生不同的行為,比如同樣是買票這個(gè)操作,普通人就是全票,學(xué)生就是半票,本篇的例子也會(huì)從這個(gè)例子出發(fā),進(jìn)行多態(tài)中具體的距離和深層次的理解
構(gòu)成多態(tài)的條件:
- 必須通過基類的指針或者引用調(diào)用虛函數(shù)
- 被調(diào)用的函數(shù)必須是虛函數(shù),并且派生類要對(duì)基類的虛函數(shù)進(jìn)行重寫
#include <iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "普通票全價(jià)" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "學(xué)生票半價(jià)" << endl;
}
};
void func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
func(p);
func(s);
}
上面是對(duì)多態(tài)的最初始定義,也是很基礎(chǔ)的定義,從中可以看出多態(tài)的基本用法和實(shí)現(xiàn)的功能
虛函數(shù)
虛函數(shù)的定義:
虛函數(shù)通俗來說,就是被virtual
修飾的類成員函數(shù)就是虛函數(shù)
虛函數(shù)的重寫:
虛函數(shù)的重寫就是,當(dāng)派生類中有一個(gè)和基類完全相同的虛函數(shù),那么就稱之為子類的虛函數(shù)重寫了基類的虛函數(shù),雖然子類可以不加virtual
,但是并不標(biāo)準(zhǔn),最好加上
虛函數(shù)的例外:
-
協(xié)變
協(xié)變就是,派生類重寫基類虛函數(shù)的時(shí)候,與基類虛函數(shù)返回值類型不同,比如基類的虛函數(shù)返回的是基類成員的指針和引用,派生類返回的是指針和引用的時(shí)候,也算是虛函數(shù)重寫,這種情況就叫做協(xié)變 -
析構(gòu)函數(shù)重寫
如果基類的析構(gòu)函數(shù)是虛函數(shù),那么派生類的析構(gòu)函數(shù)默認(rèn)會(huì)和基類的析構(gòu)函數(shù)構(gòu)成重寫,雖然名字和函數(shù)名不同,但是依舊是,這是因?yàn)榫幾g器進(jìn)行編譯后,把析構(gòu)函數(shù)的名稱統(tǒng)一處理為destructor
,這樣也算是重寫
class Person
{
public:
virtual void BuyTicket()
{
cout << "普通票全價(jià)" << endl;
}
virtual Person* f()
{
return new Person;
}
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "學(xué)生票半價(jià)" << endl;
}
virtual Student* f()
{
return new Student;
}
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
void func(Person& p)
{
p.BuyTicket();
}
override和final關(guān)鍵字
C++11
中,引入了兩個(gè)關(guān)鍵字,這兩個(gè)關(guān)鍵字就是用來輔助進(jìn)行虛函數(shù)多態(tài)的多種復(fù)雜情形,避免出現(xiàn)疏忽而導(dǎo)致錯(cuò)誤的情況出現(xiàn):
final:修飾虛函數(shù),表示這個(gè)虛函數(shù)不能被重寫了
class Student :public Person
{
public:
virtual void BuyTicket() final
{
cout << "學(xué)生票半價(jià)" << endl;
}
virtual Student* f()
{
return new Student;
}
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
class Child :public Student
{
public:
virtual void BuyTicket()
{
cout << "小孩免票" << endl;
}
};
override:檢查派生類虛函數(shù)是否重寫了基類某個(gè)虛函數(shù),如果沒有就報(bào)錯(cuò)
class Person
{
public:
/*virtual*/ void BuyTicket()
{
cout << "普通票全價(jià)" << endl;
}
virtual Person* f()
{
return new Person;
}
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTicket() override
{
cout << "學(xué)生票半價(jià)" << endl;
}
virtual Student* f()
{
return new Student;
}
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
class Child :public Student
{
public:
virtual void BuyTicket() override
{
cout << "小孩免票" << endl;
}
};
void func(Person& p)
{
p.BuyTicket();
}
多態(tài)在使用的過程中是十分復(fù)雜的,因此使用時(shí)需要注意邏輯能否清楚的表示,可能只是稍微變了一點(diǎn)點(diǎn)內(nèi)容,就使得整個(gè)意思全然變換,下面對(duì)比一下繼承多態(tài)中的一些概念:
重載、覆蓋、隱藏
- 重載指的是,函數(shù)名在一個(gè)作用域,并且函數(shù)名相同,參數(shù)不同的情況,那么這兩個(gè)函數(shù)就構(gòu)成了函數(shù)重載,編譯器在進(jìn)行處理的時(shí)候會(huì)根據(jù)參數(shù)形成不同的函數(shù)表,由此來對(duì)應(yīng)不同的情況
- 重寫指的是,兩個(gè)函數(shù)在基類和派生類的作用域下,前提是函數(shù)名、參數(shù)、返回值都一樣的情況下,如果是虛函數(shù),那么就構(gòu)成了重寫,其中子類可以不寫
virtual
,可以理解為虛函數(shù)的屬性被從基類中繼承了下來,但是并不推薦這樣寫,其中要注意特殊情況,比如協(xié)變和析構(gòu)函數(shù)的情況 - 隱藏指的是,兩個(gè)函數(shù)在基類和派生類的作用域下,當(dāng)函數(shù)名相同的時(shí)候,如果不符合重寫的定義那么就是重定義了,比如在繼承中見到的很多種情況
抽象類
抽象類的定義
在虛函數(shù)后面寫上等于0,就說明這個(gè)函數(shù)是純虛函數(shù),有純虛函數(shù)的類就叫做抽象類,抽象類的特點(diǎn)是不可以實(shí)例化出一個(gè)具體的對(duì)象,而派生類被繼承后也不能實(shí)例化對(duì)象,只有在重寫了虛函數(shù)的前提下,才能實(shí)例化對(duì)象
純虛函數(shù)體現(xiàn)了派生類要重寫的這個(gè)規(guī)則,同時(shí)也體現(xiàn)出了接口繼承的概念
接口繼承和實(shí)現(xiàn)繼承
- 接口繼承(
Interface Inheritance
)是指從一個(gè)純虛基類(pure virtual base class
)繼承而來,目的是為了實(shí)現(xiàn)一個(gè)類的接口,使得派生類必須實(shí)現(xiàn)該接口中定義的所有純虛函數(shù)。接口繼承的主要目的是實(shí)現(xiàn)類的接口復(fù)用,它并不關(guān)心實(shí)現(xiàn)細(xì)節(jié)。在接口繼承中,派生類只需要實(shí)現(xiàn)基類中定義的純虛函數(shù),不需要關(guān)心基類中其他的數(shù)據(jù)和函數(shù)
class Shape
{
public:
virtual void draw() = 0; // 純虛函數(shù)
};
class Circle : public Shape
{
public:
void draw() override
{
// 實(shí)現(xiàn)圓形的繪制
}
};
class Square : public Shape
{
public:
void draw() override
{
// 實(shí)現(xiàn)正方形的繪制
}
};
- 實(shí)現(xiàn)繼承(
Implementation Inheritance
)是指從一個(gè)普通的基類(非純虛基類)繼承而來,目的是為了實(shí)現(xiàn)基類中已有的函數(shù)或數(shù)據(jù)。實(shí)現(xiàn)繼承的主要目的是實(shí)現(xiàn)代碼復(fù)用,它關(guān)心基類中的實(shí)現(xiàn)細(xì)節(jié)。在實(shí)現(xiàn)繼承中,派生類會(huì)繼承基類中所有的成員函數(shù)和數(shù)據(jù)成員,并且可以重寫這些函數(shù)以改變它們的行為
class Person
{
public:
void sayHello()
{
std::cout << "Hello, I am a person." << std::endl;
}
};
class Student : public Person
{
public:
void sayHello() override
{
std::cout << "Hello, I am a student." << std::endl;
}
};
int main()
{
Student s;
s.sayHello(); // 輸出: "Hello, I am a student."
return 0;
}
接口繼承是指派生類只繼承了基類的接口(也就是純虛函數(shù)),而沒有繼承基類的實(shí)現(xiàn)。這種方式使得派生類必須實(shí)現(xiàn)基類中的所有純虛函數(shù),從而使得派生類和基類的實(shí)現(xiàn)是分離的,實(shí)現(xiàn)了接口和實(shí)現(xiàn)的分離。這種繼承方式常常用于實(shí)現(xiàn)抽象類和接口,強(qiáng)制要求派生類實(shí)現(xiàn)接口中的所有函數(shù)
實(shí)現(xiàn)繼承是指派生類繼承了基類的接口和實(shí)現(xiàn),包括數(shù)據(jù)成員和函數(shù)實(shí)現(xiàn)。這種方式使得派生類可以復(fù)用基類的代碼,從而減少了代碼的重復(fù)編寫,同時(shí)也保證了派生類和基類的一致性。但是,這也意味著派生類和基類的實(shí)現(xiàn)是緊密耦合的,基類的修改可能會(huì)影響到派生類的行為
多態(tài)的原理解析
虛函數(shù)表
對(duì)于一個(gè)使用了多態(tài)的類,創(chuàng)建一個(gè)對(duì)象看其內(nèi)部的內(nèi)容:
會(huì)發(fā)現(xiàn)這當(dāng)中和預(yù)想的結(jié)果并不一樣,原因就在于這當(dāng)中多了一個(gè)數(shù)組,這個(gè)指針數(shù)組實(shí)際上是叫做虛函數(shù)表指針數(shù)組,嚴(yán)格意義來說,這個(gè)數(shù)組中存放的是虛函數(shù)的函數(shù)地址,虛函數(shù)表也叫做虛表,那么問題來了:為什么要這么設(shè)計(jì)呢?
下面來做實(shí)驗(yàn),對(duì)前面的類進(jìn)行改造:
在Person
類中加一個(gè)虛函數(shù)和一個(gè)普通函數(shù),而在Student
類中只重寫一個(gè)虛函數(shù):
class Person
{
public:
// Person類中有兩個(gè)虛函數(shù)和一個(gè)普通函數(shù)
virtual void BuyTicket()
{
cout << "普通票全價(jià)" << endl;
}
virtual void func1()
{
cout << "void func1()" << endl;
}
void func2()
{
cout << "void func2()" << endl;
}
private:
int _person; // 定義一個(gè)變量
};
class Student :public Person
{
// 繼承類中只重寫一個(gè)虛函數(shù),剩下的不進(jìn)行重寫
public:
virtual void BuyTicket()
{
cout << "學(xué)生票半價(jià)" << endl;
}
private:
int _student; // 定義一個(gè)變量
};
void func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
func(p);
func(s);
}
實(shí)驗(yàn)結(jié)果如下:
對(duì)實(shí)驗(yàn)結(jié)果進(jìn)行分析得出下面的結(jié)論:
- 派生類的
Student
對(duì)象中也有一個(gè)虛表指針,其中是由兩個(gè)部分組成的,一個(gè)是父類成員和自己的成員,虛表指針中也是存在的一部分是自己的成員 - 在基類和派生類中的虛表地址是不一樣的,但是在虛表的具體內(nèi)部中會(huì)發(fā)現(xiàn),有一個(gè)函數(shù)指針地址是一樣的,還有一個(gè)不一樣,那么說明在
Student
類中重寫的函數(shù)發(fā)生了改變,因此虛函數(shù)的重寫才叫做覆蓋,覆蓋指的就是虛表中對(duì)于虛函數(shù)的覆蓋 - 對(duì)于虛表內(nèi)的內(nèi)容,只有被繼承下來的虛函數(shù)才會(huì)放到虛表中,其余函數(shù)不會(huì)放入虛表中
- 虛函數(shù)表本質(zhì)上就是一個(gè)存放虛函數(shù)指針的指針數(shù)組,這與一開始的結(jié)論是一樣的
虛函數(shù)表的生成過程:
- 基類中的虛表拷貝到派生類的虛表中
- 如果派生類中重寫了虛表的某個(gè)函數(shù),那么就進(jìn)行覆蓋的過程
- 派生類自己新有的虛函數(shù)按照在類內(nèi)的次序放到派生類虛表的最后
虛函數(shù)和虛表的存儲(chǔ)位置:
虛函數(shù)存放在虛表,虛表存放在對(duì)象中,這樣的回答是錯(cuò)誤的!
虛表中存的是虛函數(shù)的指針,并不是虛函數(shù),虛函數(shù)和普通的函數(shù)是一樣的,都是存放在代碼段中,只是將它的指針放到了虛表中,而在對(duì)象中存放的是虛表的指針,也不是虛表,虛表在vs
下也是存放在代碼段的位置中
多態(tài)的調(diào)用原理:
在知道了虛表的存在和原理后,其實(shí)可以理解前面的一些內(nèi)容了
當(dāng)指向的對(duì)象是Person
類的時(shí)候,此時(shí)會(huì)在Person
類的虛表中找到對(duì)應(yīng)的函數(shù)并進(jìn)行調(diào)用,當(dāng)對(duì)象是Student
類的時(shí)候,原理相同,借助這個(gè)原理就實(shí)現(xiàn)了多態(tài),用不同的對(duì)象去運(yùn)行會(huì)產(chǎn)生不同的結(jié)果,而多態(tài)的函數(shù)調(diào)用也不是直接確認(rèn)的,而是在運(yùn)行的過程中,在對(duì)象的內(nèi)部自動(dòng)取識(shí)別,去獲取的
動(dòng)態(tài)綁定和靜態(tài)綁定
靜態(tài)綁定也叫做前期綁定或者是早綁定:在程序編譯期間就確定了程序的行為,也叫做靜態(tài)多態(tài),比如說函數(shù)重載就是比較典型的例子文章來源:http://www.zghlxwxcb.cn/news/detail-718979.html
動(dòng)態(tài)綁定也叫做后期綁定或者是晚綁定:在程序運(yùn)行期間,根據(jù)具體拿到的類型來確定程序的具體行為和調(diào)用的具體函數(shù),比如說動(dòng)態(tài)多態(tài)就是這樣的例子文章來源地址http://www.zghlxwxcb.cn/news/detail-718979.html
到了這里,關(guān)于C++:多態(tài)的內(nèi)容和底層原理的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!