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