引言
了解c++的三大特性是對c++的整體的認識。
-
封裝性: 類將成員變量和成員函數封裝在類的內部,根據需要設置訪問權限,通過成員函數管理內部狀態(用訪問修飾符設置)
- 繼承:繼承所表達的是類之間相關的關系,這種關系使得對象可以繼承另外一類對象的特征和能力。作用:避免公用代碼的重復開發,減少代碼和數據冗余。
-
多態:多態性可以簡單地概括為“一個接口,多種方法”,字面意思為多種形態。程序在運行時才決定調用的函數,它是面向對象編程領域的核心概念。比如函數重載、運算符重載、虛函數等
前些章已經介紹了繼承,重載,本篇就在此基礎上詳說一下多態。
一,C++ 多態
多態按字面的意思就是多種形態。當類之間存在層次結構,並且類之間是通過繼承關聯時,就會用到多態。
C++ 多態意味着調用成員函數時,會根據調用函數的對象的類型來執行不同的函數。
下面的實例中,基類 Shape 被派生為Rectangle類,如下所示:
#include <iostream> using namespace std; class Shape { protected: int width, height; public: Shape(int a,int b):width(a),height(b){} int area() { cout << "Parent class area :" << endl; return 0; } }; //將Rectangle類繼承Shape類 class Rectangle : public Shape { public: Rectangle(int a,int b) :Shape(a, b) { } int area() { cout << "Rectangle class area :" <<width*height<< endl; return 0; } }; // 程序的主函數 int main() { Shape* shape;//定義shpae類指針 Rectangle rec(10, 7);//派生類對象 // 基類指針指向派生類對象(存儲矩形的地址) shape = &rec; // 調用矩形的求面積函數 area shape->area(); return 0; }
可以發現運行結果和我們期望的不一樣。什么原因造成的呢?
我們直觀上認為,如果指針指向了派生類對象,那么就應該使用派生類的成員變量和成員函數,這符合人們的思維習慣。但是本例的運行結果卻告訴我們,當基類指針 shape指向派生類 Rectangle的對象時,雖然使用了 Rectangle的成員變量,但是卻沒有使用它的成員函數,導致輸出結果不符合我們的預期。
換句話說,通過基類指針只能訪問派生類的成員變量,但是不能訪問派生類的成員函數。
為了消除這種尷尬,讓基類指針能夠訪問派生類的成員函數,C++ 增加了虛函數(Virtual Function)。使用虛函數非常簡單,只需要在函數聲明前面增加 virtual 關鍵字。
但現在,讓我們對程序稍作修改,在 Shape 類中,area() 的聲明前放置關鍵字 virtual,如下所示:
class Shape { protected: int width, height; public: Shape(int a,int b):width(a),height(b){} virtual int area() { cout << "Parent class area :" << endl; return 0; } };
修改后,當編譯和執行前面的實例代碼時,它會產生以下結果:(運行成功!)
有了虛函數,基類指針指向基類對象時就使用基類的成員(包括成員函數和成員變量),指向派生類對象時就使用派生類的成員。換句話說,基類指針可以按照基類的方式來做事,也可以按照派生類的方式來做事,它有多種形態,或者說有多種表現方式,我們將這種現象稱為多態(Polymorphism)。
二,虛函數
虛函數對於多態具有決定性的作用,有虛函數才能構成多態,這節我們來重點說一下虛函數的注意事項。
- 只需要在虛函數的聲明處加上 virtual 關鍵字,函數定義處可以加也可以不加。
- 為了方便,可以只將基類中的函數聲明為虛函數,這樣所有派生類中具有遮蔽(覆蓋)關系的同名函數都將自動成為虛函數。
C++繼承時的名字遮蔽
1️⃣如果派生類中的成員(包括成員變量和成員函數)和基類中的成員重名,那么就會遮蔽從基類繼承過來的成員。(即使用派生類新增的成員)
2️⃣基類成員和派生類成員的名字一樣時會造成遮蔽,這句話對於成員變量很好理解,對於成員函數要引起注意,不管函數的參數如何,只要名字一樣就會造成遮蔽。換句話說,基類成員函數和派生類成員函數不會構成重載,如果派生類有同名函數,那么就會遮蔽基類中的所有同名函數,不管它們的參數是否一樣。
- 當在基類中定義了虛函數時,如果派生類沒有定義新的函數來遮蔽此函數,那么將使用基類的虛函數。
- 只有派生類的虛函數遮蔽基類的虛函數(函數原型相同)才能構成多態(通過基類指針訪問派生類函數)。例如基類虛函數的原型為
virtual void func();
,派生類虛函數的原型為virtual void func(int);
,那么當基類指針 p 指向派生類對象時,語句p -> func(100);
將會出錯,而語句p -> func();
將調用基類的函數。 - 構造函數不能是虛函數。對於基類的構造函數,它僅僅是在派生類構造函數中被調用,這種機制不同於繼承。也就是說,派生類不繼承基類的構造函數,將構造函數聲明為虛函數沒有什么意義。
- 析構函數可以聲明為虛函數,而且有時候必須要聲明為虛函數。
🧡構成多態的條件
下面是構成多態的條件:
- 必須存在繼承關系;
- 繼承關系中必須有同名的虛函數,並且它們是遮蔽(覆蓋)關系。
- 存在基類的指針,通過該指針調用虛函數。
下面的例子對各種混亂情形進行了演示:
#include <iostream> using namespace std; //基類Base class base { public: virtual void func() { cout << "void Base::func()" << endl; } virtual void func(int) { cout << "void Base::func(int)" << endl; } }; //派生類Derived class Dervied :public base { public: void func() { cout << "void Derived::func()" << endl; } void func(char*str) { cout << "void Derived::func(char *shr)" << endl; } }; int main() { base* p = new Dervied();//創建基類指針*p指向派生類對象 p->func(); p->func(10); //p->func("學習c++");//報錯 }
輸出結果:
在基類 Base 中我們將void func()
聲明為虛函數,這樣派生類 Derived 中的void func()
就會自動成為虛函數。p 是基類 Base 的指針,但是指向了派生類 Derived 的對象。
語句p -> func();
調用的是派生類的虛函數,構成了多態(由於派生類遮蔽了基類函數)
語句p -> func(10);
調用的是基類的虛函數,因為派生類中沒有函數遮蔽它。
語句p -> func("學習c++");
出現編譯錯誤,因為通過基類的指針只能訪問從基類繼承過去的成員,不能訪問派生類新增的成員。
💙純虛函數和抽象類
如果我們想要在基類中定義虛函數,以便在派生類中重新定義該函數更好地適用於對象,但是在基類中又不能對虛函數給出有意義的實現,這個時候就會用到純虛函數,語法格式為:
virtual 返回值類型 函數名 (函數參數) = 0;
我們可以把基類中的虛函數 area() 改寫如下:
class Shape { protected: int width, height; public: Shape(int a,int b):width(a),height(b){} //純虛函數 virtual int area() = 0; };
area()= 0並不表示函數返回值為0,它只起形式上的作用,告訴編譯系統“這是純虛函數”。
包含純虛函數的類稱為抽象類(Abstract Class)。之所以說它抽象,是因為它無法實例化,也就是無法創建對象。原因很明顯,純虛函數沒有函數體,不是完整的函數,無法調用,也無法為其分配內存空間。
抽象類通常是作為基類,讓派生類去實現純虛函數。派生類必須實現純虛函數才能被實例化。
純虛函數使用舉例:
#include <iostream> using namespace std; //基類 line class line { public: line(float len):m_len(len){}//初始化列表 virtual float area() = 0;//純虛函數 virtual float volume() = 0;//純虛函數 protected: float m_len; }; //派生類 rect:基類 line class rect :public line { public: rect(float len,float width):line(len),m_width(width){} float area() { return m_len * m_width; } protected: float m_width; }; //派生類 cuboid:基類 rect class cuboid :public rect { public: cuboid(float len, float width, float height) :rect(len, width), m_height(height) {} float volume() { return m_len * m_width * m_height; } protected: float m_height; }; int main() { line* p = new cuboid(10,20,30);//基類指針指向派生類對象 cout << "The area of Cuboid is " << p->area() << endl; cout << "The volume of Cuboid is " << p->volume() << endl; }
本例定義了三個類,其繼承關系為:line->rect->cuboid。
line是一個抽象類,也是最頂層的基類,在 line類中定義了兩個純虛函數 area() 和 volume()。
- 在 rect類中,實現了 area() 函數;所謂實現,就是定義了純虛函數的函數體。但這時 rect類仍不能被實例化,因為它沒有實現繼承來的 volume() 函數,volume() 仍然是純虛函數,所以 rect也仍然是抽象類。
- 直到 cuboid類,才實現了 volume() 函數,才是一個完整的類,才可以被實例化。
可以發現,line類表示“線”,沒有面積和體積,但它仍然定義了 area() 和 volume() 兩個純虛函數。這樣的用意很明顯:line類不需要被實例化,但是它為派生類提供了“約束條件”,派生類必須要實現這兩個函數,完成計算面積和體積的功能,否則就不能實例化。
在實際開發中,你可以定義一個抽象基類,只完成部分功能,未完成的功能交給派生類去實現(誰派生誰實現)。這部分未完成的功能,往往是基類不需要的,或者在基類中無法實現的。雖然抽象基類沒有完成,但是卻強制要求派生類完成,這就是抽象基類的“霸王條款”。
抽象基類除了約束派生類的功能,還可以實現多態。指針 p 的類型是 line,但是它卻可以訪問派生類中的 area() 和 volume() 函數,正是由於在 line類中將這兩個函數定義為純虛函數;如果不這樣做,后面的代碼都是錯誤的。我想,這或許才是C++提供純虛函數的主要目的。
關於純虛函數的幾點說明:
1) 一個純虛函數就可以使類成為抽象基類,但是抽象基類中除了包含純虛函數外,還可以包含其它的成員函數(虛函數或普通函數)和成員變量。
2) 只有類中的虛函數才能被聲明為純虛函數,普通成員函數和頂層函數均不能聲明為純虛函數。