您好,登錄后才能下訂單哦!
這篇文章主要講解了“C++多態(tài)的實(shí)現(xiàn)與原理及抽象類實(shí)例分析”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“C++多態(tài)的實(shí)現(xiàn)與原理及抽象類實(shí)例分析”吧!
多態(tài): 從字面意思來看,就是事物的多種形態(tài)。用C++的語(yǔ)言說就是不同的對(duì)象去完成同一個(gè)行為會(huì)產(chǎn)生不同的效果。
虛函數(shù): 被virtual關(guān)鍵字修飾的類成員函數(shù)叫做虛函數(shù)。
實(shí)例演示: 看一下代碼,其中BuyTicket成員函數(shù)被virtual關(guān)鍵字修飾
class Person { public: // 虛函數(shù) virtual void BuyTicket() { cout << "買票全價(jià)" << endl; } };
多態(tài)是在不同繼承關(guān)系的類對(duì)象,去調(diào)用同一函數(shù),產(chǎn)生了不同的行為。
繼承中構(gòu)成多態(tài)有兩個(gè)條件:
必須有基類的指針或引用調(diào)用
被調(diào)用的函數(shù)必須是虛函數(shù),其派生類必須對(duì)基類的虛函數(shù)進(jìn)行重寫
虛函數(shù)的重寫是什么?
虛函數(shù)的重寫(覆蓋): 派生類中有一個(gè)跟基類完全相同的虛函數(shù)(即派生類虛函數(shù)與基類虛函數(shù)的返回值類型、函數(shù)名字、參數(shù)列表完全相同),稱子類的虛函數(shù)重寫了基類的虛函數(shù)。(重寫是對(duì)函數(shù)體進(jìn)行重寫)
實(shí)例演示:
class Person { public: virtual void BuyTicket() { cout << "買票全價(jià)" << endl; } }; class Student : public Person { public: virtual void BuyTicket() // 這里也可以不寫virtual,因?yàn)榛惖奶摵瘮?shù)屬性已經(jīng)被保留下來了,這里只是完成虛函數(shù)的重寫 { cout << "買票半價(jià)" << endl; } };
虛函數(shù)重寫的兩個(gè)例外:
1.協(xié)變:基類和派生類的虛函數(shù)的返回類型不同
派生類重寫基類虛函數(shù)時(shí),與基類虛函數(shù)返回值類型不同。即基類虛函數(shù)返回基類對(duì)象的指針或者引用,派生類虛函數(shù)返回派生類對(duì)象的指針或者引用時(shí),稱為協(xié)變。(也就是基類虛函數(shù)的返回類型和派生類的虛函數(shù)的返回類型是父子類型的指針或引用)
// 協(xié)變 返回值類型不同,但它們之間是父子或父父關(guān)系 返回類型是指針或者引用 // 基類虛函數(shù) 返回類型 是 基類的指針或者引用 // 派生類虛函數(shù) 返回類型 是 基類或派生類的返回類型是基類的指針或引用 class A {}; class B : public A {}; class Person { public: virtual A* f() { return new A; } }; class Student : public Person { public: virtual A* f() { return new B; } };
2.析構(gòu)函數(shù)的重寫 基類與派生類的析構(gòu)函數(shù)的函數(shù)名不同
我在上一篇博客中說到過,基類和派生類的析構(gòu)函數(shù)的函數(shù)名會(huì)被編譯器統(tǒng)一處理成destructor,所以只要基類的析構(gòu)函數(shù)加了關(guān)鍵字virtual,就會(huì)和派生類的析構(gòu)函數(shù)構(gòu)成重寫。
我們?cè)倩氐蕉鄳B(tài)構(gòu)成的兩個(gè)條件中,完成基類虛函數(shù)的重寫我已經(jīng)介紹了,還有一個(gè)必須由基類的指針或引用調(diào)用的條件,這個(gè)應(yīng)該很好理解吧。下面舉個(gè)例子: 實(shí)例演示:
class Person { public: virtual void BuyTicket() { cout << "買票全價(jià)" << endl; } }; class Student : public Person { public: virtual void BuyTicket() // 這里也可以不寫virtual,因?yàn)榛惖奶摵瘮?shù)屬性已經(jīng)被保留下來了,這里只是完成虛函數(shù)的重寫 { cout << "買票半價(jià)" << endl; } }; void Func1(Person& p) { p.BuyTicket(); } void Func2(Person* p) { p->BuyTicket(); } void Func3(Person p) { p.BuyTicket(); } int main() { Person p; Student s; // 滿足多態(tài)的條件:與類型無(wú)關(guān),父類指針指向的是誰(shuí)就調(diào)用誰(shuí)的成員函數(shù) // 不滿足多態(tài)的條件:與類型有關(guān),類型是誰(shuí)就調(diào)用誰(shuí)的成員函數(shù) cout << "基類的引用調(diào)用:" << endl; Func1(p); Func1(s); cout << "基類的指針調(diào)用:" << endl; Func2(&p); Func2(&s); cout << "基類的對(duì)象調(diào)用:" << endl; Func3(p); Func3(s); return 0; }
代碼運(yùn)行結(jié)果:
總結(jié):
滿足多態(tài)的條件:成員函數(shù)調(diào)用與對(duì)象類型無(wú)關(guān),指向那個(gè)對(duì)象就調(diào)用哪個(gè)的虛函數(shù)
不滿足多態(tài)的條件:成員函數(shù)的調(diào)用與對(duì)象類型有關(guān),是哪個(gè)對(duì)象類型就調(diào)用哪個(gè)對(duì)象的虛函數(shù)。
思考: 析構(gòu)函數(shù)是否要加virtual? 答案是需要的。先給大家看一個(gè)例子:
class Person { public: /*virtual*/ ~Person() { cout << "~Person()" << endl; } }; class Student: public Person { public: ~Student() { cout << "~Student()" << endl; } }; int main() { Person* p = new Person; Person* ps = new Student;// 不加virtual,不構(gòu)成多態(tài),父類指針只會(huì)根據(jù)類型去調(diào)用對(duì)于的析構(gòu)函數(shù) // 加了virtual,構(gòu)成多態(tài),父類指針會(huì)根據(jù)指向的對(duì)象去調(diào)用他的析構(gòu)函數(shù) delete p; delete ps; return 0; }
下面分別是基類析構(gòu)函數(shù)不加virtual和加virtual的代碼運(yùn)行結(jié)果:
可以看出,不加virtual關(guān)鍵字時(shí),第二個(gè)對(duì)象delete時(shí)沒有調(diào)用子類的析構(gòu)函數(shù)清理釋放空間。為什么呢?因?yàn)椴患觱irtual關(guān)鍵字時(shí),兩個(gè)析構(gòu)函數(shù)不構(gòu)成多態(tài),所以調(diào)用析構(gòu)函數(shù)時(shí)是與類型有關(guān)的,因?yàn)槎际嵌际歉割愵愋?,所以只?huì)調(diào)用父類的析構(gòu)函數(shù)。加了virtual關(guān)鍵字時(shí),因?yàn)閮蓚€(gè)析構(gòu)函數(shù)被編譯器處理成同名函數(shù)了,所以完成了虛函數(shù)的重寫,且是父類指針調(diào)用,所以此時(shí)兩個(gè)析構(gòu)函數(shù)構(gòu)成多態(tài),所以調(diào)用析構(gòu)函數(shù)時(shí)是與類型無(wú)關(guān)的,因?yàn)楦割愔羔樦赶虻氖亲宇悓?duì)象,所以會(huì)調(diào)用子類的析構(gòu)函數(shù),子類調(diào)用完自己的析構(gòu)函數(shù)又會(huì)自動(dòng)調(diào)用父類的析構(gòu)函數(shù)來完成對(duì)父類資源的清理。 所以總的來看,基類的析構(gòu)函數(shù)是要加virtual的。
final: 修飾虛函數(shù),表示該虛函數(shù)不可以被重寫(還可以修飾類,表示該類不可以被繼承)
實(shí)例演示:
class Car { public: // final 表示該虛函數(shù)不能被重寫 也可以修飾類,表示該類不可以被繼承 virtual void Drive() final {} }; class Benz :public Car { public: virtual void Drive() { cout << "Benz-舒適" << endl; } };
編譯器檢查結(jié)果: 由于dirve字母編寫錯(cuò)誤,所以編譯器檢查出沒有重寫基類的虛函數(shù)
2.overide: 檢查派生類虛函數(shù)是否重寫了基類的某個(gè)虛函數(shù) 實(shí)例演示:
class Car { public: // final 表示該虛函數(shù)不能被重寫 也可以修飾類,表示該類不可以被繼承 virtual void Drive() final {} }; class Benz :public Car { public: virtual void Drive() { cout << "Benz-舒適" << endl; } };
編譯器檢查結(jié)果:
名稱 | 作用域 | 函數(shù)名 | 其他 |
---|---|---|---|
重載 | 兩個(gè)函數(shù)在同一作用域 | 相同 | 參數(shù)類型不同 |
重寫 | 兩個(gè)函數(shù)分別再基類和派生類的作用域 | 相同 | 函數(shù)返回類型和參數(shù)類型一樣 |
重定義(隱藏) | 兩個(gè)函數(shù)分別再基類和派生類的作用域 | 相同 | 兩個(gè)基類和派生類的同名函數(shù)不是構(gòu)成重寫就是重定義 |
概念: 在虛函數(shù)的后面寫上 =0 ,則這個(gè)函數(shù)為純虛函數(shù)。包含純虛函數(shù)的類叫做抽象類(也叫接口類),抽象類不能實(shí)例化出對(duì)象。派生類繼承后也不能實(shí)例化出對(duì)象,只有重寫純虛函數(shù),派生類才能實(shí)例化象純虛函數(shù)規(guī)范了派生類必須重寫,另外純虛函數(shù)更體現(xiàn)出了接口繼承。
總結(jié)出幾個(gè)特點(diǎn):
虛函數(shù)后面加上=0
不能實(shí)例化出對(duì)象
派生類如果不重寫基類的純虛函數(shù)那么它也是抽象類,不能實(shí)例化出對(duì)象
抽象類嚴(yán)格限制派生類必須重寫基類的純虛函數(shù)
體現(xiàn)了接口繼承
實(shí)例演示:
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 () override { cout << "BMW" << endl; } }; int main() { Car* pBenZ = new Benz; pBenZ->Drive(); Car* pBMW = new BMW; pBMW->Drive(); delete pBenZ; delete pBMW; return 0; }
代碼運(yùn)行結(jié)果:
抽象類的意義?
強(qiáng)制子類完成父類虛函數(shù)的重寫
表示該類是抽象類,沒有實(shí)體(例如:花、車和人等)
接口繼承和實(shí)現(xiàn)繼承
普通函數(shù)的繼承是一種實(shí)現(xiàn)繼承,派生類繼承了基類函數(shù),可以使用函數(shù),繼承的是函數(shù)的實(shí)現(xiàn)。虛函數(shù)的繼承是一種接口繼承,派生類繼承的是基類虛函數(shù)的接口,目的是為了重寫,達(dá)成多態(tài),繼承的是接口。所以如果不實(shí)現(xiàn)多態(tài),不要把函數(shù)定義成虛函數(shù)。
概念: 一個(gè)含有虛函數(shù)的類中至少有一個(gè)虛函數(shù)指針,這個(gè)指針指向了一張表——虛函數(shù)表(簡(jiǎn)稱虛表),這張表中存放了這個(gè)類中所有的虛函數(shù)的地址。
計(jì)算一下下面這個(gè)類的大?。?/p>
class Base { public: virtual void func1() {} virtual void func2() {} public: int _a; }; int main() { cout << sizeof(Base) << endl; return 0; }
代碼運(yùn)行結(jié)果如下:
這個(gè)類中存放了一個(gè)虛表指針和一個(gè)成員變量,所以總大小就是8。給大家看一下它的類對(duì)象模型:
實(shí)例演示:
class Person { public: virtual void BuyTicket() { cout << "買票全價(jià)" << endl; } virtual void func() { cout << "func()" << endl; } int _p = 1; }; class Student : public Person { public: virtual void BuyTicket() // 這里也可以不寫virtual,因?yàn)榛惖奶摵瘮?shù)屬性已經(jīng)被保留下來了,這里只是完成虛函數(shù)的重寫 { cout << "買票半價(jià)" << endl; } int _s = 1; }; int main() { Person p; Student s; return 0; }
類對(duì)象模型如下:
可以看出,兩個(gè)虛函數(shù)地址是不一樣的,其實(shí)子類會(huì)先把父類的虛表拷貝一份下來,如果子類重寫了虛函數(shù),那么子類的虛函數(shù)的地址將會(huì)覆蓋虛表中的地址,如果沒有重寫,那么將不覆蓋。
總結(jié)幾點(diǎn):
子類對(duì)象由兩部分構(gòu)成,一部分是父類繼承下來的成員,虛表指針指向的虛表有父類的虛函數(shù),也有子類新增的虛函數(shù)
子類完成父類虛函數(shù)的重寫其實(shí)是對(duì)繼承下來的虛表的中重寫了的虛函數(shù)進(jìn)行覆蓋,把地址更換了,語(yǔ)法層是稱為覆蓋
虛函數(shù)表本質(zhì)是一個(gè)存虛函數(shù)指針的指針數(shù)組,一般情況這個(gè)數(shù)組最后面放了一個(gè)nullptr
虛表生成的過程:先將基類中的虛表內(nèi)容拷貝一份到派生類虛表中 b.如果派生類重寫了基類中某個(gè)虛函數(shù),用派生類自己的虛函數(shù)覆蓋虛表中基類的虛函數(shù) c.派生類自己新增加的虛函數(shù)按其在派生類中的聲明次序增加到派生類虛表的最后
下面我們來討論一下虛表存放的位置和虛表指針存放的位置
虛表指針肯定是存在類中的,從上面的類對(duì)象模型中可以看出。其次虛表存放的是虛函數(shù)的地址,這些虛函數(shù)和普通函數(shù)一樣,都會(huì)被編譯器編譯成指令,然后放進(jìn)代碼段。虛表也是存在代碼段的,因?yàn)橥愋偷膶?duì)象共用一張?zhí)摫?。下面帶大家?yàn)證一下(環(huán)境:vs2019)
驗(yàn)證代碼:
class Base { public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } virtual void func3() { cout << "Base::func3" << endl; } void func() {} int b = 0; }; class Derive :public Base { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func2() { cout << "Derive::func2" << endl; } virtual void func4() { cout << "Derive::func4" << endl; } virtual void func5() { cout << "Derive::func5" << endl; } int d = 0; }; void func() {} int globalVar = 10; int main() { Base b; Derive d; const char* pChar = "hello"; int c = 1; static int s = 20; int* p = new int; const int i = 10; printf("棧變量:%p\n", &c); printf("虛表指針:%p\n", (int*)&b); printf("對(duì)象成員:%p\n", ((int*)&b + 1)); printf("堆變量:%p\n", p); printf("代碼段常量:%p\n", pChar); printf("普通函數(shù)地址:%p\n", func); printf("成員函數(shù)地址:%p\n", &Base::func); printf("虛函數(shù):%p\n", &Base::func1); printf("虛函數(shù)表:%p\n", *(int*)&b); printf("數(shù)據(jù)段:%p\n", &s); printf("數(shù)據(jù)段:%p\n", &globalVar); delete p; return 0; }
代碼運(yùn)行結(jié)果如下:
容易看出,代碼段常量存放的地址和虛表存放的地址很接近,和數(shù)據(jù)段的地址也很接近,所以可以猜測(cè)虛表存放在數(shù)據(jù)段或代碼段,更可能是在代碼段。
多態(tài)是在運(yùn)行時(shí)到指向的對(duì)象中的虛表中查找要調(diào)用的虛函數(shù)的地址,然后進(jìn)行調(diào)用。
總結(jié):
多態(tài)滿足的兩個(gè)條件:一個(gè)是虛函數(shù)的覆蓋,一個(gè)是對(duì)象的指針和引用調(diào)用
滿足多態(tài)后,函數(shù)的調(diào)用不是編譯時(shí)確認(rèn)的,而是在運(yùn)行時(shí)確認(rèn)的。
動(dòng)態(tài)綁定和靜態(tài)綁定
靜態(tài)綁定: 發(fā)生在編譯時(shí),也就是早期綁定,就是我們之前說過的函數(shù)重載就是屬于靜態(tài)綁定,也稱靜態(tài)多態(tài)。
動(dòng)圖綁定: 發(fā)生在運(yùn)行時(shí),也就是后期綁定,多態(tài)就是發(fā)生在運(yùn)行時(shí),也稱動(dòng)態(tài)多態(tài)。
先看下面的代碼(單繼承)
class Base { public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } virtual void func3() { cout << "Base::func3" << endl; } void func() {} int b = 0; }; class Derive :public Base { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func2() { cout << "Derive::func2" << endl; } virtual void func4() { cout << "Derive::func4" << endl; } virtual void func5() { cout << "Derive::func5" << endl; } int d = 0; };
觀察它的類對(duì)象模型:
在上面的類對(duì)象模型中,派生類中只可以看見func1和func2,后面兩個(gè)函數(shù)看不見,這是因?yàn)榫幾g器把這兩個(gè)新增的虛函數(shù)給隱藏了,為了我們能夠更好的觀察,我們可以通過寫代碼來看。 先定義一個(gè)函數(shù)指針:
typedef void(*VF_PTR)(); // 給函數(shù)指針typedef
下面是打印虛表的代碼:
void PrintVFTable(VF_PTR* pTable) { for (size_t i = 0; pTable[i] != nullptr; ++i) { printf("vfTable[%d]:%p->", i, pTable[i]); VF_PTR f = pTable[i]; f();// 通過函數(shù)地址調(diào)用函數(shù) } cout << endl; }
下面我們只需要通過傳虛表地址的方式來調(diào)用函數(shù)打印虛表,虛表地址如何獲取呢?從上面的類對(duì)象模型可以知道,類對(duì)象的前四個(gè)地址存放的是虛表指針,虛表指針也就是虛表的指針,所以我們要獲取類對(duì)象的前四個(gè)字節(jié)。下面是獲取方法:
(VF_PTR*)*(int*)&b;
先將類對(duì)象的地址取出,然后強(qiáng)轉(zhuǎn)為整形,解引用就會(huì)按照四個(gè)字節(jié)來獲取內(nèi)容,這四個(gè)字節(jié)的內(nèi)容是虛表指針,其實(shí)也是虛表的地址,我們可以把這個(gè)整形強(qiáng)轉(zhuǎn)為函數(shù)地址的類型就可以了。
打印虛表:
int main() { Base b; Derive d; PrintVFTable((VF_PTR*)*(int*)&b); PrintVFTable((VF_PTR*)*(int*)&d); return 0; }
打印結(jié)果如下:
可以看出派生類對(duì)象中新增的虛函數(shù)會(huì)按照虛函數(shù)函數(shù)次序聲明放在虛表的最后。
看下面代碼(多繼承)
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 = 1; }; 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; };
類對(duì)象模型如下:
為了更好地觀察,我們還是通過打印虛表來觀察:
int main() { Derive d; cout << sizeof(Derive) << endl; cout << "Base1的虛表:" << endl; PrintVFTable((VF_PTR*)*(int*)&d); cout << "Base2的虛表:" << endl; PrintVFTable((VF_PTR*)*(int*)((char*)&d+sizeof(Base1))); cout << "Derive的成員變量d:" << endl; //PrintVFTable((VF_PTR*)*(int*)((char*)&d + sizeof(Base1) + sizeof(Base2))); cout << *(int*)((char*)&d + sizeof(Base1) + sizeof(Base2)) << endl; return 0; }
打印結(jié)果如下:
可以看出,派生類新增的虛函數(shù)放在了第一個(gè)繼承的對(duì)象的虛表中最后了。
感謝各位的閱讀,以上就是“C++多態(tài)的實(shí)現(xiàn)與原理及抽象類實(shí)例分析”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對(duì)C++多態(tài)的實(shí)現(xiàn)與原理及抽象類實(shí)例分析這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。