多態(tài)是面向?qū)ο缶幊痰囊粋€(gè)特性阳藻。它允許一個(gè)對(duì)象在不同的條件下表現(xiàn)出不同的結(jié)果晰奖。在c++中有兩種多態(tài)的表現(xiàn)類型:
- 編譯時(shí)多態(tài)(Compile time Polymorphism)。這個(gè)也被稱為靜態(tài)綁定(static binding)或者早期綁定(early binding)腥泥。
- 運(yùn)行時(shí)多態(tài)(Runtime binding)匾南。這個(gè)也被稱為動(dòng)態(tài)綁定(dynamic binding)或者遲綁定(late binding)。
<1>Compile time Polymorphism
方法的重載和操作符的重載都屬于編譯時(shí)多態(tài)的例子蛔外。接下來我們介紹下什么是方法重載蛆楞。
方法重載(function overloading)
方法重載是c++編程的一個(gè)特性溯乒。它允許我們擁有多個(gè)方法名一樣的方法,只需要這些方法的參數(shù)表是不一樣的豹爹。所謂的參數(shù)表是指參數(shù)的數(shù)據(jù)類型和排列順序裆悄。比如:
myFunc(int a, float b)的參數(shù)表是(int, float)。
myFunc(float a, int b)的參數(shù)表是(float, int)臂聋。
上面兩個(gè)的方法名雖然都是myFunc光稼,但是兩個(gè)方法的參數(shù)表是不一樣的。
總結(jié):
判斷是否符合重載條件只需要判斷同名方法的參數(shù)是否滿足參數(shù)的類型孩等,數(shù)目艾君, 順序有存在不同。
注意:
如果參數(shù)表是一樣的肄方,即使方法的返回值不一樣也是不合法的冰垄。只要參數(shù)列表是不同的,方法的返回值可以相同也可以不同权她。所以重載的條件就是需要參數(shù)列表的不同虹茶。判定不同的方法就是上面注意中提到的。
例如:
// 不合法
int sum(int, int)
double sum(int, int)
例子1
#include <iostream>
using namespace std;
class Addition {
public:
int sum(int num1,int num2) {
return num1+num2;
}
int sum(int num1,int num2, int num3) {
return num1+num2+num3;
}
};
int main(void) {
Addition obj;
cout<<obj.sum(20, 15)<<endl;
cout<<obj.sum(81, 100, 10);
return 0;
}
輸出:
35
191
例子2
#include <iostream>
using namespace std;
class DemoClass {
public:
int demoFunction(int i) {
return i;
}
double demoFunction(double d) {
return d;
}
};
int main(void) {
DemoClass obj;
cout<<obj.demoFunction(100)<<endl;
cout<<obj.demoFunction(5005.516);
return 0;
}
輸出:
100
5006.52
方法重載的優(yōu)點(diǎn)
方法重載的主要是增加了代碼的可讀性和復(fù)用性(code readability and code reusability)伴奥。想象一下如果沒有方法重載這種機(jī)制写烤,我們可能要寫很多不同的函數(shù)名但是操作的本質(zhì)是一樣的翼闽。這樣代碼的可讀性就變得很差拾徙,而且也會(huì)在給函數(shù)起什么名字上面多耗精力。
方法重載的多態(tài)表現(xiàn)
從上面的例子中我們可以看到感局,方法重載是通過編譯時(shí)通過不同的參數(shù)表來確定不同的版本的函數(shù)尼啡。調(diào)用時(shí)根據(jù)參數(shù)表的內(nèi)容去找對(duì)應(yīng)的版本的方式來實(shí)現(xiàn)多態(tài)的。
<2>Runtime Polymorphism
方法的重寫就是運(yùn)行時(shí)多態(tài)的例子询微。接下來我們介紹下什么是方法的重寫崖瞭。
方法的覆蓋\重寫(function overriding)
方法的覆蓋也是c++編程的一種特性。這種機(jī)制允許我們?cè)谧宇愔袚碛幸粋€(gè)和父類一樣的方法撑毛。子類可以繼承了父類的數(shù)據(jù)成員和成員函數(shù)书聚。當(dāng)我們想要修改繼承于父類的某種方法的功能時(shí),我們就可以通過覆蓋的機(jī)制來實(shí)現(xiàn)藻雌。通過這種方式雌续,我們就好像在子類當(dāng)中創(chuàng)建了一個(gè)父類對(duì)應(yīng)該方法的新方法。
例子:
在重寫一個(gè)方法時(shí)胯杭,我們需要保證子類中的方法簽名和改寫的父類方法一致驯杜。這里所指的方法簽名是指參數(shù)的數(shù)據(jù)類型和順序。下面這個(gè)例子中做个,要覆蓋的父類方法中并沒有任何參數(shù)鸽心,因此我們子類中的重寫方法也不需要任何參數(shù)滚局。
#include <iostream>
using namespace std;
class BaseClass {
public:
void disp(){
cout<<"Function of Parent Class";
}
};
class DerivedClass: public BaseClass{
public:
void disp() {
cout<<"Function of Child Class";
}
};
int main() {
DerivedClass obj = DerivedClass();
obj.disp();
return 0;
}
輸出:
Function of Child Class
注意:
在方法重寫中,在父類中對(duì)應(yīng)的方法我們稱之為被重寫的方法(overridden function)顽频,在子類中的方法我們稱之為重寫方法(overriding function)藤肢。
如何通過子類調(diào)用被重寫的父類方法
上面的例子我們看到了子類調(diào)用重寫的方法。那么如何通過子類來調(diào)用父類中被重寫的方法(overridden function)呢冲九?我們可以通過用父類引用指向子類的實(shí)例的方式來調(diào)用我們的父類被重寫的方法谤草。用下面的例子來幫助我們理解:
#include <iostream>
using namespace std;
class BaseClass {
public:
void disp(){
cout<<"Function of Parent Class";
}
};
class DerivedClass: public BaseClass{
public:
void disp() {
cout<<"Function of Child Class";
}
};
int main() {
/* Reference of base class pointing to
* the object of child class.
*/
BaseClass obj = DerivedClass();
obj.disp();
return 0;
}
輸出:
Function of Parent Class
如果你想要在子類中重寫的函數(shù)(overriding function)中調(diào)用父類中被重寫的函數(shù)(overridden function),你只需要這樣做:
//父類類名::方法名
parent_class_name::function_name
如果用上面的例子來解釋上面這種用法就是:
BaseClass::disp();
虛函數(shù)
當(dāng)我們?cè)诟割愔猩昝髁艘粋€(gè)函數(shù)為虛函數(shù)時(shí)莺奸,所有子類中該方法的重寫函數(shù)都會(huì)默認(rèn)的被認(rèn)為是虛函數(shù)(不管是否有virtual關(guān)鍵字)〕蠛ⅲ現(xiàn)在的問題是我們?yōu)槭裁匆v一個(gè)函數(shù)聲明為虛函數(shù)呢?這是為了讓編譯器明白這個(gè)函數(shù)的調(diào)用必須到運(yùn)行時(shí)間才能確定灭贷。只有當(dāng)對(duì)象的類型被確定時(shí)温学,才知道調(diào)用哪個(gè)版本的函數(shù)。
接下來讓我們來看兩個(gè)例子來幫助理解甚疟。分別重寫非虛函數(shù)的父類函數(shù)和重寫虛函數(shù)的父類函數(shù)仗岖。
重寫一個(gè)非虛函數(shù)
#include<iostream>
using namespace std;
//Parent class or super class or base class
class Animal{
public:
void animalSound(){
cout<<"This is a generic Function";
}
};
//child class or sub class or derived class
class Dog : public Animal{
public:
void animalSound(){
cout<<"Woof";
}
};
int main(){
Animal *obj;
obj = new Dog();
obj->animalSound();
return 0;
}
輸出:
This is a generic Function
重寫一個(gè)虛函數(shù)
#include<iostream>
using namespace std;
//Parent class or super class or base class
class Animal{
public:
virtual void animalSound(){
cout<<"This is a generic Function";
}
};
//child class or sub class or derived class
class Dog : public Animal{
public:
void animalSound(){
cout<<"Woof";
}
};
int main(){
Animal *obj;
obj = new Dog();
obj->animalSound();
return 0;
}
輸出:
Woof
總結(jié):
很顯然,重寫虛函數(shù)的方式使得調(diào)用的時(shí)候览妖,程序是看對(duì)象的類型來決定調(diào)用的函數(shù)轧拄。在第二個(gè)覆蓋虛函數(shù)的例子中,我們可以看見雖然obj指針的指針類型是父類Animal讽膏,但是其指向的對(duì)象是子類Dog的對(duì)象檩电。因此這時(shí)候,調(diào)用的animalSound是子類中的重寫函數(shù)而不是父類里頭的該函數(shù)府树。接下來我們通過一個(gè)例子再來進(jìn)一步了解下它的特性俐末。
#include<iostream>
using namespace std;
class Base{
public:
Base(){
std::cout << "Base::Base()" << std::endl;
}
~Base(){
std::cout << "Base::~Base()" << std::endl;
}
void print1(){
std::cout << "Base::print1()" << std::endl;
}
void virtual print2(){
std::cout << "Base::print2()" << std::endl;
}
};
class Child:public Base{
public:
Child(){
std::cout << "Child::Child()" << std::endl;
}
~Child(){
std::cout << "Child::~Child()" << std::endl;
}
void print1(){
std::cout << "Child::print1()" << std::endl;
}
void print2(){
std::cout << "Child::print2()" << std::endl;
}
};
int main()
{
Base* base = new Base();
Child* child = new Child();
base->print1();
base->print2();
child->print1();
child->print2();
delete base;
delete child;
Child* child2 = nullptr;
child2->print1();
child2->print2();
return 0;
}
輸出:
Base::Base()
Base::Base()
Child::Child()
Base::print1()
Base::print2()
Child::print1()
Child::print2()
Base::~Base()
Child::~Child()
Base::~Base()
Child::print1()
Thread 1: EXC_BAD_ACCESS (code=1, address=0x0)//報(bào)錯(cuò)
我們通過打印的信息能夠更加清楚的了解到c++背后的運(yùn)行機(jī)制。
- 首先我們創(chuàng)建了兩個(gè)對(duì)象奄侠,分別是基類對(duì)象和子類對(duì)象卓箫,并分別用基類指針base和子類指針child指向它們。這個(gè)過程對(duì)應(yīng)輸出我們可以看出調(diào)用了構(gòu)造函數(shù)垄潮。其中基類的構(gòu)造函數(shù)被調(diào)用了兩次烹卒。說明在創(chuàng)建子類對(duì)象的時(shí)候,調(diào)用了子類的構(gòu)造函數(shù)弯洗,而子類的構(gòu)造函數(shù)會(huì)先默認(rèn)調(diào)用父類的構(gòu)造函數(shù)旅急,然后執(zhí)行自己的構(gòu)造函數(shù)。
- 通用基類和子類的指針分別調(diào)用print1和print2兩個(gè)函數(shù)涂召。這個(gè)地方?jīng)]什么問題坠非。基類指針指向基類對(duì)象果正,調(diào)用的必然是基類中的函數(shù)炎码。子類也一樣盟迟。
- 第三步刪除對(duì)象。這邊發(fā)現(xiàn)基類的析構(gòu)函數(shù)也被調(diào)用了兩次潦闲。和構(gòu)造函數(shù)相同攒菠,子類在調(diào)用析構(gòu)函數(shù)的時(shí)候,也會(huì)調(diào)用一次父類的析構(gòu)函數(shù)歉闰。不過不同的是調(diào)用時(shí)期辖众。析構(gòu)的時(shí)候,是在執(zhí)行完自己的析構(gòu)函數(shù)體后再調(diào)用父類的析構(gòu)和敬。
- 最后一個(gè)涉及到多態(tài)的知識(shí)凹炸。首先創(chuàng)建了一個(gè)子類的指針child2并賦值為空指針nullptr。第一次調(diào)用print1昼弟,通過輸出可以知道啤它,該語句可以正常執(zhí)行。并且調(diào)用的是子類的print1舱痘。說明了如果函數(shù)不是虛函數(shù)的情況下变骡,c++是直接根據(jù)指針類型來判斷調(diào)用的函數(shù)版本(到底是基類的,還是基類的版本)芭逝。第二句調(diào)用print2函數(shù)時(shí)報(bào)錯(cuò)塌碌。原因是print2是個(gè)虛函數(shù)(父類中聲明了是虛函數(shù),所有子類重寫該函數(shù)都默認(rèn)是虛函數(shù))旬盯。通過虛函數(shù)的機(jī)制我們可以知道台妆,要調(diào)用哪個(gè)版本(父類的版本還是子類的版本)需要在運(yùn)行的時(shí)候通過指針?biāo)赶虻膶?duì)象類型來確定。此時(shí)瓢捉,child2指針指向?yàn)榭罩羔槪]有指向任何實(shí)例對(duì)象)频丘,因此報(bào)錯(cuò)办成。
方法重載(function overloading)和方法重寫(function overriding)的不同
到此我們已經(jīng)理解了什么是c++編程中的方法重載和方法重寫泡态。讓我們來看看它們的不同:
- 方法重載是在同一個(gè)類里頭完成的。在同一個(gè)類中迂卢,我們聲明方法名相同但是參數(shù)列表不一樣的方法稱為方法重載某弦。
- 在方法重載時(shí),我們必須確認(rèn)函數(shù)的簽名是不同的而克。但是在方法重寫中靶壮,我們必須保證重寫方法和被重寫方法具有相同的簽名。
- 方法重載發(fā)生在編譯期間员萍,因此也被成為編譯時(shí)多態(tài)腾降。而方法覆蓋發(fā)生在運(yùn)行期間,因此也被稱為運(yùn)行時(shí)多態(tài)碎绎。
- 方法重載時(shí)螃壤,在一個(gè)類中重載的方法數(shù)量沒有限制抗果。方法重寫時(shí),一個(gè)子類對(duì)應(yīng)父類的某個(gè)方法只能被重寫一次奸晴。