文章目錄
- 前言
- 1. 虛函數
- 1.1 現象
- 1.2 多態
- 1.3 析構函數
- 1.4 override和final
- 1.5 重載、隱藏、重寫對比
- 2. 抽象類
- 2.1 抽象類特性
- 2.2 抽象類的應用場景
- 3. 多態實現的底層原理
- 4. 靜態綁定和動態綁定
- 5. 總結
前言
多態是面向對象三大特性之一,也是細節最多的語法之一。學習類對象的多態,我們不僅僅要看到基礎語法,也要體會到底層原理。從而我們結合類與對象的其它性質,才能更好地體會到多態的魅力。
下面小編會和大家一起探討多態的基礎語法細節!
關于虛函數,還有涉及很多很多的問題(例如:構造中的多態調用、初始化順序……),小編會在一篇講解習題的文章中談到!
注:本文章的測試用例均在VS2022 x86環境下進行。
1. 虛函數
小編會由現象引入多態
1.1 現象
來看下面一個例子,例1:
#include<iostream>
#include<string>
using namespace std;class animal
{
public:animal(const string& name):_name(name){}virtual void call(){cout << "name is : " << _name << " call : sound" << endl;}
protected:string _name;
};class cat : public animal
{
public:cat(const string& name = ""):animal(name){}virtual void call(){cout << "name is : " << _name << " call : meow" << endl;}
};class dog : public animal
{
public:dog(const string& name = ""):animal(name){}virtual void call(){cout << "name is : "<< _name << " call : growl" << endl;}
};int main()
{cat c("Lucy");dog d("Juck")animal* ptr1 = &c;animal *ptr2 = &d;ptr1->call();ptr2->call();return 0;
}
說明:
上面代碼定義了一個animal
的類,其中成員是animal
的名字和叫聲。
由dog
和cat
來繼承這個animal
的類,并且重寫了叫聲方法。
創建了一個cat
和dog
對象并且由animal
的指針來接受這兩個指針。然后調用call
方法,觀察現象!
結果是我們可以正常得到cat
和dog
的call
函數調用結果!!
這就是多態。
1.2 多態
多態的字面意思:對于同一種行為,不同對象去完成產生了不同的結果(形態)!
- 多態:是在不同繼承的類對象,去調用同一個函數產生了不同的行為。
根據剛剛的例1,我們可以得到的事實是:
cat
和dog
繼承于同一個基類animal
。animal
中用virtual
聲明了函數call
。cat
和dog
中都對函數call
再進行了重寫。
看到的現象:
- 同一個類型的
animal*
指針指向了不同的對象,調用同一個call
函數,產生了不同的結果。
接下來我們正式步入多態語法的講解:
-
多態的兩個條件:
- 基類的指針或者引用,指向派生類。(原因后面會談到)
- 被調用的函數一定是虛函數,派生類必須對虛函數進行重寫。
-
虛函數:在類里被
virtual
關鍵字修飾的函數被稱為虛函數。- 注意:這個
virtual
關鍵字和繼承的虛擬繼承沒有關系。
- 注意:這個
-
重寫(也叫覆蓋):
- 函數必須是虛函數。
- 函數名完全和基類相同、函數參數列表完全和基類相同、函數返回值和基類相同。(三同)
-
需要注意的是:
virtual
關鍵字可以在派生類的時候不用聲明,但是基類必須顯示聲明。- 協變:返回值可以不同。但是必須滿足父子關系的同類型的返回值。例如:基類返回基類的指針,派生類虛函數就可以返回派生類的指針(不可以是引用)。(只要滿足父子關系即可,并不用管是什么類型)
- 派生類方法的訪問權限與基類方法的訪問權限只會影響當前類型的訪問
建議:基類的訪問可以公有。 - 了解:派生類方法的拋出的異常不能大于基類方法的異常范圍。
下面我們來進行解析:
- 必須是指針或者引用。那如果是基類對象呢?
例2:
#include<iostream>
#include<string>
using namespace std;
class animal
{
public:animal(const string& name):_name(name){}virtual void call(){cout << "name is : " << _name << " call : sound" << endl;}
protected:string _name;
};class cat : public animal
{
public:cat(const string& name = ""):animal(name){}virtual void call(){cout << "name is : " << _name << " call : meow" << endl;}
};int main()
{cat c("Lucy");animal a = c; //用基類的對象得到對象ca.call(); //嘗試調用call()函數return 0;
}
沒有產生預期的結果!(底層解析的時候會告訴讀者為什么)
這就給我們提示:普通對象的調用看的是類型。(千萬不要和隱藏弄混了)
- 一定是需要調用虛函數。如果不是調用虛函數,那么就是滿足繼承體系中的隱藏(重定義)關系了!
例3:
#include<iostream>
#include<string>
using namespace std;
class animal
{
public:animal(const string& name):_name(name){}void func(){//無關鍵字virtual聲明—>普通的成員函數cout << "1" << endl;}virtual void call(){cout << "name is : " << _name << " call : sound" << endl;}
protected:string _name;
};class cat : public animal
{
public:cat(const string& name = ""):animal(name){}void func(){cout << "2" << endl;}void call(){cout << "name is : " << _name << " call : meow" << endl;}
};int main()
{cat c("Lucy");animal* ptr = &c;ptr->func(); //普通調用ptr->call(); //多態調用return 0;
}
上面代碼func函數的調用構成普通的隱藏關系。
- 協變演示:
例4:
#include<iostream>
#include<string>
using namespace std;
class A
{};class B : public A
{};class animal
{
public:animal(const string& name):_name(name){}virtual A* call(){cout << "name is : " << _name << " call : sound" << endl;A* ptr = new A; //僅僅由于演示return ptr;}
protected:string _name;
};class cat : public animal
{
public:cat(const string& name = ""):animal(name){}virtual B* call(){cout << "name is : " << _name << " call : meow" << endl;B* ptr = new B;return ptr;}
};int main()
{cat c("Lucy");animal* ptr1 = &c;ptr->call();return 0;
}
小編是為了演示而
new
了A
、B
對象,大家不要寫出這樣的代碼。
- 訪問權限問題。
例5:
#include<iostream>
#include<string>
using namespace std;
class animal
{
public:animal(const string& name):_name(name){}virtual void call(){cout << "name is : " << _name << " call : sound" << endl; }
protected:string _name;
};class cat : public animal
{
public:cat(const string& name = ""):animal(name){}private:virtual void call(){cout << "name is : " << _name << " call : meow" << endl;}
};int main()
{cat c("Lucy");animal* ptr1 = &c;ptr->call(); //沒有影響return 0;
}
上面代碼的
animal
的call
方法訪問權限為public
,而cat
的call
方法訪問權限為private
,但是不影響animal*ptr
調用。
1.3 析構函數
說到多態,不得不提及一個默認成員函數了:析構函數。
來看下面一個場景:
例6(錯誤示例):
#include<iostream>
using namespace std;
class Base
{
public:~Base(){cout << "~Base()" << endl;}
};class Derived : public Base
{
public:~Derived(){cout << "~Derived()" << endl;}
};int main()
{Base *ptr = new Derived;delete ptr;return 0;
}
說明:
上面代碼Derived
繼承Base
類,并且由Base
指針指向new
的Derived
對象,然后delete
該指針指向的對象。
來看運行結果:
我們明明是想調用Derived
的析構函數,為什么會調用Base
的呢?
-
下面我們來分析一下:
delete
:調用析構 + 銷毀空間。- 上面我們談到:普通函數的調用只能看類型。(1.2的例3)
Base
和Derived
的析構函數很顯然沒有被聲明為一個虛函數,那么調用就被當作普通函數來對待。- 那么對于一個
Base
的指針來說,如果以該指針類型來調用析構函數的話,就只會調用Base
類型的析構函數。
如果想要解決這個問題:
- 析構函數要被處理為統一的名字 — 滿足三同。這個任務已經幫助我們完成了,統一被處理為:
destructor
。 - 析構函數要被聲明為虛函數 。
例7:
#include<iostream>
using namespace std;class Base
{
public:virtual ~Base(){cout << "~Base()" << endl;}
};class Derived : public Base
{
public:virtual ~Derived(){cout << "~Derived()" << endl;}
};int main()
{Base *ptr = new Derived;delete ptr;return 0;
}
來看運行結果:
-
注意:
編譯默認生成的析構函數不會為你聲明為一個虛函數。如果涉及以上想要使用
delete
的場景,還請顯示聲明析構函數為虛函數!
1.4 override和final
C++11提出這兩個關鍵字,更好地規范了虛函數的重寫
-
override:
- 用途:聲明在派生類函數的參數列表后。檢查派生類虛函數是否重寫了基類的某個虛函數。如果沒有重寫,編譯器會報錯。
例8:
#include<iostream>
using namespace std;class Base
{
public:virtual ~Base(){cout << "~Base()" << endl;}virtual void func(){cout << "Base" << endl;}
};class Derived : public Base
{
public:virtual ~Derived(){cout << "~Derived()" << endl;}virtual void func() override //聲明override,并且完成了重寫:三同{cout << "Derived" << endl;}
};int main()
{Base *ptr = new Derived;ptr->func();delete ptr;return 0;
}
-
final:
- 用途:修飾虛函數,表示該虛函數不能再被重寫。
例9(錯誤樣例,代碼final
檢查錯誤):
#include<iostream>
using namespace std;
class Base
{
public:virtual ~Base(){cout << "~Base()" << endl;}virtual void func() final //聲明{cout << "Base" << endl;}
};class Derived : public Base
{
public:virtual ~Derived(){cout << "~Derived()" << endl;}virtual void func(){cout << "Derived" << endl;}
};
1.5 重載、隱藏、重寫對比
這三個是比較容易混淆的概率,小編在這里為大家對比一下
名稱 | 特性 |
---|---|
重載 | 1、兩個函數在同一作用域下 2、函數名和參數列表相同 |
隱藏(重定義) | 1、兩個函數分別在父子類域中 2、函數名相同 |
重寫(覆蓋) | 1、兩個函數分別在父子類域中 2、三同(協變例外)3、兩個函數都是虛函數 |
-
我們可以得到一個小結論:
- 父子類域中的兩個同名函數,不是構成重寫就是隱藏!
2. 抽象類
有時候我們描述一個事物,但是這個事物在現實生活中是“抽象”的。那么對于它的方法而言就是一個抽象的方法,但是這個事物可以得到延展。例如:形狀。
-
抽象類:含有純虛函數的類被稱為“抽象類”。
-
純虛函數:
是在基類中聲明的函數,它在基類中沒有定義,但要求:任何該類的派生類都要重寫自己的實現方法。
在基類中實現純虛函數的方法是:在虛函數的方法函數原型后面添加
= 0
。
-
例10:
#include<iostream>
using namespace std;
class shape //該類為抽象類
{
public:virtual double area() const = 0 // 聲明為純虛函數并且const修飾{}
};class circle : public shape
{
public:virtual double area() const //const修飾指針也是參數列表中的一環{//業務處理}
};
上面代碼中,就是一個抽象類
shape
,由circle
繼承這個抽象類,并且重寫方法area
2.1 抽象類特性
-
抽象類是不能實例化對象出來的!
我們可以理解為:類似這樣的抽象類在現實生活中也找不到實體!
- 也就是意味著:抽象類只能作為其它類的基類。
-
抽象類的本身類型也不能作為函數參數或者函數返回值,也不能作為顯示類型轉換的類型。
原因同上:抽象類不能示例化出對象!
-
如果派生類沒有重寫該純虛函數,那么該派生類也不能實例化出對象。
4. 可以定義抽象類類型的指針或者引用來指向其派生類。這里小編就不再列舉例子了。
2.2 抽象類的應用場景
- 純虛函數常用于定義接口規范,強制派生類實現特定功能。抽象基類僅提供接口聲明,不包含具體實現,確保所有派生類遵循統一的接口標準。抽象類體現了一種:接口繼承的理念。
我們可以將抽象類用于描述一些抽象的事物:
- 圖像
- 游戲角色
- ……
這些還是需要大家實戰體會。
3. 多態實現的底層原理
關于這個問題,小編打算另起一文,如下:
C++類對象多態底層原理及擴展問題
在這篇文章中小編會和大家探討:
- 多態調用的原理:解析虛函數指針,虛函數表,多態成立的兩個條件
- 拓展虛函數在虛函數表中的存放位置:單繼承和多繼承
- ……
4. 靜態綁定和動態綁定
上面鏈接那篇文章談到了一個話題:多態調用的消耗。實際上這是一個動態綁定的。匯編層面上的差異我們已經可以得知了。
-
靜態綁定(前期綁定):
- 在程序編譯期間就已經確定了程序的行為,這就是靜態綁定。
- 例如:函數重載(也是一種靜態多態)
-
動態綁定(后期綁定):
- 在程序運行期間根據具體的類型確定程序的行為,這就是動態綁定。
- 例如:虛函數重寫多態調用(動態多態)
5. 總結
我們總結一下這個部分的知識點
virtual
關鍵字聲明虛函數,建議析構函數聲明為虛函數。- 構成多態的兩個條件:a、重寫 b、指針和引用調用。
- 重載、隱藏、重寫對比。
- 什么是抽象類?具體的性質?
- 多態調用的底層原理?虛函數指針、虛函數表?
- 了解靜態多態和動態多態 。
完……
- 希望這篇文章能夠幫助你!