湖州高端網(wǎng)站建設(shè)app推廣方案
【c++隨筆13】多態(tài)
- 多態(tài)性(Polymorphism)在面向?qū)ο缶幊讨惺且粋€(gè)重要概念,它允許以統(tǒng)一的方式處理不同類型的對(duì)象,并在運(yùn)行時(shí)動(dòng)態(tài)確定實(shí)際執(zhí)行的方法或函數(shù)。
- 一、什么是多態(tài)性?
- 1、關(guān)鍵概念:C++的多態(tài)性
- 2、多態(tài)定義
- 3、沒有 靜態(tài)多態(tài)、動(dòng)態(tài)多態(tài)
- 二、多態(tài)的詳細(xì)介紹
- 1、多態(tài)的構(gòu)成條件
- 2、覆蓋(override)——重寫
- 3、多態(tài)構(gòu)成的兩個(gè)意外
- 3.1、協(xié)變——構(gòu)成多態(tài)
- 3.2、父虛子非虛——構(gòu)成多態(tài)
- 4、析構(gòu)函數(shù)的重寫
- 1. 直接定義對(duì)象
- 2. 使用new操作符在堆上創(chuàng)建對(duì)象
- 3、結(jié)論:在堆上構(gòu)建對(duì)象,且基類指針指向派生類的情況下,如果不加virtual,會(huì)發(fā)生內(nèi)存泄漏,派生類不會(huì)析構(gòu)。
- 5、final (C++11)
- 6、override(C++11)
- 7、重載、覆蓋、隱藏的對(duì)比
- 三、抽象類
- 1、純虛函數(shù)
- 2、 抽象類(abstract class)
- 3、抽象類指針
- 4、- 抽象類實(shí)例化?
- 5、接口繼承(Interface Inheritance)和實(shí)現(xiàn)繼承(Implementation Inheritance)是面向?qū)ο缶幊讨械膬煞N繼承方式。
原創(chuàng)作者:鄭同學(xué)的筆記
原創(chuàng)地址:https://zhengjunxue.blog.csdn.net/article/details/131858812
qq技術(shù)交流群:921273910
多態(tài)性(Polymorphism)在面向?qū)ο缶幊讨惺且粋€(gè)重要概念,它允許以統(tǒng)一的方式處理不同類型的對(duì)象,并在運(yùn)行時(shí)動(dòng)態(tài)確定實(shí)際執(zhí)行的方法或函數(shù)。
一、什么是多態(tài)性?
1、關(guān)鍵概念:C++的多態(tài)性
我們查看《C++ Primer 第5版》第15.3章節(jié) 虛函數(shù)中的介紹(p537頁(yè))
OOP的核心思想是多態(tài)性(polymorphism)。多態(tài)性這個(gè)詞源自希臘語(yǔ),其含義是“多種形式”。我們把具有繼承關(guān)系的多個(gè)類型稱為多態(tài)類型,因?yàn)槲覀兡苁褂眠@些類型的“多種形式”而無(wú)須在意它們的差異。引用或指針的靜態(tài)類型與動(dòng)態(tài)類型不同這一事實(shí)正是C++語(yǔ)言支持多態(tài)性的根本所在。
當(dāng)我們使用基類的引用或指針調(diào)用基類中定義的一個(gè)函數(shù)時(shí),我們并不知道該函數(shù)真正作用的對(duì)象是什么類型,因?yàn)樗赡苁且粋€(gè)基類的對(duì)象也可能是一個(gè)派生類的對(duì)象。如果該函數(shù)是虛函數(shù),則直到運(yùn)行時(shí)才會(huì)決定到底執(zhí)行哪個(gè)版本,判斷的依據(jù)是引用或指針?biāo)壎ǖ膶?duì)象的真實(shí)類型。
另一方面,對(duì)非虛函數(shù)的調(diào)用在編譯時(shí)進(jìn)行綁定。類似的,通過對(duì)象進(jìn)行的函數(shù)(虛函數(shù)或非虛函數(shù))調(diào)用也在編譯時(shí)綁定。對(duì)象的類型是確定不變的,我們無(wú)論如何都不可能令對(duì)象的動(dòng)態(tài)類型與靜態(tài)類型不一致。因此,通過對(duì)象進(jìn)行的函數(shù)調(diào)用將在編譯時(shí)綁定到該對(duì)象所屬類中的函數(shù)版本上。
Note當(dāng)且僅當(dāng)對(duì)通過指針或引用調(diào)用虛函數(shù)時(shí),才會(huì)在運(yùn)行時(shí)解析該調(diào)用,也只有在這種情況下對(duì)象的動(dòng)態(tài)類型才有可能與靜態(tài)類型不同。
2、多態(tài)定義
我們依然查看《C++ Primer 第5版》第15章節(jié)末尾 術(shù)語(yǔ)表中的介紹(p576頁(yè))
多態(tài)性(polymorphism)當(dāng)用于面向?qū)ο缶幊痰姆懂爼r(shí),多態(tài)性的含義是指程序能通過引用或指針的動(dòng)態(tài)類型獲取類型特定行為的能力。
動(dòng)態(tài)類型(dynamic type)對(duì)象在運(yùn)行時(shí)的類型。引用所引對(duì)象或者指針?biāo)笇?duì)象的動(dòng)態(tài)類型可能與該引用或指針的靜態(tài)類型不同。基類的指針或引用可以指向一個(gè)派生類對(duì)象。在這樣的情況中,靜態(tài)類型是基類的引用(或指針),而動(dòng)態(tài)類型是派生類的引用(或指針)。
靜態(tài)類型(static type)對(duì)象被定義的類型或表達(dá)式產(chǎn)生的類型。靜態(tài)類型在編譯時(shí)是已知的。
3、沒有 靜態(tài)多態(tài)、動(dòng)態(tài)多態(tài)
我們看網(wǎng)上有很多資料介紹動(dòng)態(tài)時(shí),都會(huì)提到多態(tài)分為靜態(tài)多態(tài)(比如函數(shù)重載等)和動(dòng)態(tài)多態(tài),而當(dāng)我們看了上面書中的定義和介紹后會(huì)明白,網(wǎng)上的說法是有問題的。
在c++領(lǐng)域:
- 只有多態(tài)、不區(qū)分靜態(tài)多態(tài)和動(dòng)態(tài)多態(tài);
- 網(wǎng)上說的c++動(dòng)態(tài)多態(tài)就是指的c++中的多態(tài);
- 網(wǎng)上說的靜態(tài)多態(tài),不符合《C++ Primer 第5版》多態(tài)的概念;
- 靜態(tài)多態(tài)按照《C++ Primer 第5版》中書寫demo,無(wú)法實(shí)現(xiàn)多態(tài);
二、多態(tài)的詳細(xì)介紹
動(dòng)態(tài)多態(tài)性是在運(yùn)行時(shí)確定方法或函數(shù)的調(diào)用,根據(jù)實(shí)際對(duì)象的類型進(jìn)行動(dòng)態(tài)綁定。這種多態(tài)性通過虛函數(shù)和基類指針或引用來實(shí)現(xiàn)。
簡(jiǎn)單來說,
多態(tài): 就是多種形態(tài),不同的對(duì)象去完成同樣的事情會(huì)產(chǎn)生不同的結(jié)果。
舉個(gè)例子:就拿購(gòu)票系統(tǒng)來說,不同的人對(duì)于購(gòu)票這個(gè)行為產(chǎn)生的結(jié)果就是不同的,學(xué)生購(gòu)票時(shí)購(gòu)買的是半價(jià)票,普通人購(gòu)票的時(shí)候購(gòu)買的是全價(jià)票。
1、多態(tài)的構(gòu)成條件
繼承中想要構(gòu)成多態(tài),必須滿足以下兩個(gè)條件:
① 必須是子類的虛函數(shù)重寫成父類函數(shù)(重寫:三同 + 虛函數(shù))
② 必須是父類的指針或者引用去調(diào)用虛函數(shù)。
- 三同指的是:同函數(shù)名、同參數(shù)、同返回值。
- 虛函數(shù):即被 virtual 修飾的類成員函數(shù)。
- 指針調(diào)用
#include <iostream>
using namespace std;class Person {
public:Person(const char* name): _name(name) {}// 虛函數(shù)virtual void BuyTicket() {cout << _name << ": " << "Person-> 買票 全價(jià) 100¥" << endl;}protected:string _name;
};class Student : public Person {
public:Student(const char* name): Person(name) {}// 虛函數(shù) + 函數(shù)名/參數(shù)/返回 -> 重寫(覆蓋)virtual void BuyTicket() {cout << _name << ": " << "Student-> 買票 半價(jià) 50¥" << endl;}
};class Soldier : public Person {
public:Soldier(const char* name): Person(name) {}// 虛函數(shù) + 函數(shù)名/參數(shù)/返回 -> 重寫(覆蓋)virtual void BuyTicket() {cout << _name << ": " << "Soldier-> 優(yōu)先買預(yù)留票 全價(jià) 100¥" << endl;}
};/* 接收身份 */
void Pay(Person* ptr) {ptr->BuyTicket(); // 到底是誰(shuí)在買票,取決于傳來的是誰(shuí)delete ptr;
}int main()
{Person* p1 = new Person("小明爸爸");Student* stu = new Student("小明");Soldier* so = new Soldier("小明爺爺");Pay(p1);Pay(stu);Pay(so);return 0;
}
輸出
- 引用調(diào)用
/* 接收身份 */
void Pay(Person& ptr) {ptr.BuyTicket(); // 到底是誰(shuí)在買票,取決于傳來的是誰(shuí)
}int main()
{Person p1("小明爸爸");Student stu("小明");Soldier so("小明爺爺");Pay(p1);Pay(stu);Pay(so);return 0;
}
2、覆蓋(override)——重寫
我們依然查看《C++ Primer 第5版》第15章節(jié)末尾 術(shù)語(yǔ)表中的介紹(p576頁(yè))
- 覆蓋(override)派生類中定義的虛函數(shù)如果與基類中定義的同名虛函數(shù)有相同的形參列表,則派生類版本將覆蓋基類的版本。
覆蓋也被有的文章叫做”重寫“。用 virtual 虛函數(shù),并且做到函數(shù)名、參數(shù)和返回值相同,就能夠達(dá)到 “重寫” 的效果:
重寫是為了將一個(gè)已有的事物進(jìn)行某些改變以適應(yīng)新的要求。
重寫是子類對(duì)父類的允許訪問的方法的實(shí)現(xiàn)過程進(jìn)行重新編寫,返回值和形參都不能改變。 即:“外殼不變,核心重寫?!?/p>
3、多態(tài)構(gòu)成的兩個(gè)意外
剛才說了,三同+虛函數(shù),就能達(dá)到重寫的效果(也就是多態(tài))。但是,還有兩個(gè)意外,也能達(dá)成多態(tài)的效果。
3.1、協(xié)變——構(gòu)成多態(tài)
-
C++中的協(xié)變(Covariance)指的是派生類可以返回基類中相同函數(shù)簽名的返回類型的子類型。
-
在C++中,當(dāng)一個(gè)虛函數(shù)在基類中使用了virtual關(guān)鍵字聲明為虛函數(shù)時(shí),派生類可以對(duì)該虛函數(shù)進(jìn)行重寫,并且在派生類中返回類型可以是基類返回類型的子類型。這種返回類型的子類型關(guān)系稱為協(xié)變。
協(xié)變的類型必須是父子關(guān)系。
觀察下面的代碼,并沒有達(dá)到 “三同” 的標(biāo)準(zhǔn),它的返回值是不同的,但依舊構(gòu)成多態(tài):
class A {};
class B : public A {};class Person {
public:virtual A* f() {cout << "virtual A* Person::f()" << endl;return nullptr;}
};class Student : public Person {
public:virtual B* f() {cout << "virtual B* Student:::f()" << endl;return nullptr;};
};int main(void)
{Person p;Student s;Person* ptr = &p;ptr->f();ptr = &s;ptr->f();return 0;
}
輸出
當(dāng)class A、class B是父子關(guān)系時(shí),就不能協(xié)變:
3.2、父虛子非虛——構(gòu)成多態(tài)
現(xiàn)在來講第二個(gè)例外。
- 父類的虛函數(shù)沒了無(wú)法構(gòu)成多態(tài):
- 但是,子類的虛函數(shù)沒了卻能構(gòu)成多態(tài):
#include <iostream>
using namespace std;class A {};
class B : public A {};class Person {
public:virtual A* f() {cout << "virtual A* Person::f()" << endl;return nullptr;}
};class Student : public Person {
public:B* f() {cout << "virtual B* Student:::f()" << endl;return nullptr;};
};int main(void)
{Person p;Student s;Person* ptr = &p;ptr->f();ptr = &s;ptr->f();return 0;
}
輸出
4、析構(gòu)函數(shù)的重寫
1. 直接定義對(duì)象
#include <iostream>
using namespace std;class Person {
public:~Person() { //不加virtual// virtual ~Person() { //加virtualcout << "~Person()" << endl;}
};class Student : public Person {
public:~Student() {cout << "~Student()" << endl;}
};int main(void)
{Person p;Student s;return 0;
}
-
加virtual輸出
-
不加virtual輸出
2. 使用new操作符在堆上創(chuàng)建對(duì)象
#include <iostream>
using namespace std;class Person {
public:~Person() { //不加virtual//virtual ~Person() { //加virtualcout << "~Person()" << endl;}
};class Student : public Person {
public:~Student() {cout << "~Student()" << endl;}
};int main(void)
{cout << "=================不加virtual======================\n";Person *ptr = new Person();delete ptr;cout << "=======================================\n";Student *ptr2 = new Student();delete ptr2;cout << "=======================================\n";Person *ptr3 = new Student();delete ptr3;return 0;
}
-
不加virtual
-
加virtual
剛才我們看到了,如果這里不加 virtual,~Student 是沒有調(diào)用析構(gòu)的。
這其實(shí)是非常致命的,是不經(jīng)意間會(huì)發(fā)生的內(nèi)存泄露。
3、結(jié)論:在堆上構(gòu)建對(duì)象,且基類指針指向派生類的情況下,如果不加virtual,會(huì)發(fā)生內(nèi)存泄漏,派生類不會(huì)析構(gòu)。
5、final (C++11)
在C++中,final是一個(gè)關(guān)鍵字,用于修飾類、函數(shù)或虛函數(shù),具有不同的作用。
- 修飾類:使用final關(guān)鍵字修飾類時(shí),表示該類是最終類,不能被其他類繼承。例如:
class Base final {// ...
};class Derived : public Base { // 錯(cuò)誤,Derived不能繼承自final類Base// ...
};
在上述示例中,Base類被聲明為final,因此Derived類不能繼承自Base類。
- 修飾函數(shù):使用final關(guān)鍵字修飾成員函數(shù)時(shí),表示該函數(shù)是最終版本,不能被派生類重寫。例如:
class Base {
public:virtual void func() final {// ...}
};class Derived : public Base {
public:void func() override { // 錯(cuò)誤,無(wú)法重寫被聲明為final的函數(shù)// ...}
};
在上述示例中,Base類中的func()函數(shù)被聲明為final,因此Derived類無(wú)法對(duì)其進(jìn)行重寫。
- 修飾虛函數(shù):與修飾普通成員函數(shù)類似,使用final關(guān)鍵字修飾虛函數(shù)時(shí),表示該虛函數(shù)是最終版本,不能被派生類重寫。例如:
class Base {
public:virtual void func() final {// ...}
};class Derived : public Base {
public:void func() override { // 錯(cuò)誤,無(wú)法重寫被聲明為final的虛函數(shù)// ...}
};
在上述示例中,Base類中的虛函數(shù)func()被聲明為final,因此Derived類無(wú)法對(duì)其進(jìn)行重寫。
通過使用final關(guān)鍵字,可以顯式地阻止類、函數(shù)或虛函數(shù)被繼承、重寫或覆蓋,從而提高程序的安全性和可靠性。
6、override(C++11)
override是C++11引入的關(guān)鍵字,用于顯式地標(biāo)記派生類中對(duì)基類虛函數(shù)的重寫。它的主要作用是增加代碼的可讀性和可維護(hù)性,并提供編譯器的靜態(tài)檢查,避免錯(cuò)誤的重寫行為。
在C++中,當(dāng)派生類要重寫基類的虛函數(shù)時(shí),可以使用override關(guān)鍵字進(jìn)行標(biāo)記。通過使用override關(guān)鍵字,可以確保派生類的函數(shù)簽名與基類的虛函數(shù)完全匹配,否則編譯器會(huì)發(fā)出錯(cuò)誤。這有助于及時(shí)發(fā)現(xiàn)錯(cuò)誤的重寫行為。
以下是使用override關(guān)鍵字的示例:
class Base {
public:virtual void func() const {// ...}
};class Derived : public Base {
public:void func() const override {// ...}
};
在上述示例中,Base類中的虛函數(shù)func()被定義為virtual void func() const,而在Derived類中,重寫的函數(shù)也被定義為void func() const,并使用override關(guān)鍵字進(jìn)行標(biāo)記。如果Derived類的函數(shù)簽名與基類的虛函數(shù)不匹配,或者沒有正確使用override關(guān)鍵字,編譯器將會(huì)報(bào)錯(cuò)。
7、重載、覆蓋、隱藏的對(duì)比
三、抽象類
1、純虛函數(shù)
我們依然查看《C++ Primer 第5版》第15章節(jié)末尾 術(shù)語(yǔ)表中的介紹(p576頁(yè))
- 純虛函數(shù)(pure virtual)在類的內(nèi)部聲明虛函數(shù)時(shí),在分號(hào)之前使用了=0。一個(gè)純虛函數(shù)不需要(但是可以)被定義。含有純虛函數(shù)的類是抽象基類。如果派生類沒有對(duì)繼承而來的純虛函數(shù)定義自己的版本,則該派生類也是抽象的。
純虛函數(shù)是通過在函數(shù)聲明后面加上= 0來聲明的,表示該函數(shù)沒有實(shí)現(xiàn),派生類必須重寫它。
virtual void pureVirtualFunction() = 0;
在上述示例中,純虛函數(shù)pureVirtualFunction()。
- 純虛函數(shù)是否可以實(shí)現(xiàn)?
純虛函數(shù)也是可以實(shí)現(xiàn)的:
/* 抽象類 */
class Car {
public:// 實(shí)現(xiàn)沒有價(jià)值,因?yàn)閴焊鶝]有對(duì)象會(huì)調(diào)用它virtual void Drive() = 0 { // 純虛函數(shù)cout << "Drive()" << endl; }
};
2、 抽象類(abstract class)
- 包含純虛函數(shù)的類,就是 抽象類(abstract class),也叫接口類。
class AbstractClass {
public:virtual void pureVirtualFunction() = 0;
};
在上述示例中,AbstractClass是一個(gè)抽象類,它具有一個(gè)純虛函數(shù)pureVirtualFunction()。派生類必須重寫這個(gè)函數(shù)。
抽象類可以包含純虛函數(shù)(沒有實(shí)現(xiàn))和帶有實(shí)現(xiàn)的函數(shù)
3、抽象類指針
雖然父類是抽象類不能定義對(duì)象,但是可以定義指針。
定義指針時(shí)如果 new 父類對(duì)象因?yàn)槭羌兲摵瘮?shù),自然是 new 不出來的,但是可以 new 子類對(duì)象:
#include <iostream>
using namespace std;/* 抽象類 */
class Car {
public:virtual void Drive() = 0;
};class Benz : public Car {
public:virtual void Drive() {cout << "Benz-舒適" << endl;}
};int main(void)
{Car* pBenz1 = new Benz;pBenz1->Drive();Benz* pBenz2 = new Benz;pBenz2->Drive();return 0;
}
4、- 抽象類實(shí)例化?
抽象類不能實(shí)例化出對(duì)象,子類即使在繼承后也不能實(shí)例化出對(duì)象,除非子類重寫。
5、接口繼承(Interface Inheritance)和實(shí)現(xiàn)繼承(Implementation Inheritance)是面向?qū)ο缶幊讨械膬煞N繼承方式。
接口繼承指的是一個(gè)類從一個(gè)或多個(gè)接口中繼承方法聲明,但并不繼承這些方法的具體實(shí)現(xiàn)。接口只包含純虛函數(shù)(在C++中使用純虛函數(shù)定義接口)或者抽象方法(在其他語(yǔ)言中)。通過接口繼承,一個(gè)類可以實(shí)現(xiàn)多個(gè)接口,從而表達(dá)出它具備了多個(gè)行為或功能。
實(shí)現(xiàn)繼承指的是子類從父類中繼承方法聲明和實(shí)現(xiàn)。實(shí)現(xiàn)繼承建立了類的層次結(jié)構(gòu),允許子類繼承并重用父類的代碼。子類可以通過繼承父類的屬性和方法,并且可以根據(jù)需要添加新的屬性和方法,甚至可以重寫父類的方法來改變其行為。
- 普通函數(shù)的繼承是一種實(shí)現(xiàn)繼承,子類繼承了父類函數(shù),可以使用函數(shù),繼承的是函數(shù)的實(shí)現(xiàn)。
- 虛函數(shù)的繼承是一種接口繼承,子類繼承的是父類虛函數(shù)的接口,目的是為了重寫,
- 達(dá)成多態(tài),繼承的是接口。所以如果不實(shí)現(xiàn)多態(tài),不要把函數(shù)定義成虛函數(shù)。
- 出現(xiàn)虛函數(shù)就是為了提醒你重寫的,以實(shí)現(xiàn)多態(tài)。如果虛函數(shù)不重寫,那寫成虛函數(shù)就沒價(jià)值了。