多態(tài)是面向對象編程的一個特性。它允許一個對象在不同的條件下表現(xiàn)出不同的結果。在c++中有兩種多態(tài)的表現(xiàn)類型:
- 編譯時多態(tài)(Compile time Polymorphism)。這個也被稱為靜態(tài)綁定(static binding)或者早期綁定(early binding)。
- 運行時多態(tài)(Runtime binding)。這個也被稱為動態(tài)綁定(dynamic binding)或者遲綁定(late binding)。
<1>Compile time Polymorphism
方法的重載和操作符的重載都屬于編譯時多態(tài)的例子。接下來我們介紹下什么是方法重載。
方法重載(function overloading)
方法重載是c++編程的一個特性。它允許我們擁有多個方法名一樣的方法,只需要這些方法的參數(shù)表是不一樣的。所謂的參數(shù)表是指參數(shù)的數(shù)據(jù)類型和排列順序。比如:
myFunc(int a, float b)的參數(shù)表是(int, float)。
myFunc(float a, int b)的參數(shù)表是(float, int)。
上面兩個的方法名雖然都是myFunc,但是兩個方法的參數(shù)表是不一樣的。
總結:
判斷是否符合重載條件只需要判斷同名方法的參數(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)點
方法重載的主要是增加了代碼的可讀性和復用性(code readability and code reusability)。想象一下如果沒有方法重載這種機制,我們可能要寫很多不同的函數(shù)名但是操作的本質是一樣的。這樣代碼的可讀性就變得很差,而且也會在給函數(shù)起什么名字上面多耗精力。
方法重載的多態(tài)表現(xiàn)
從上面的例子中我們可以看到,方法重載是通過編譯時通過不同的參數(shù)表來確定不同的版本的函數(shù)。調用時根據(jù)參數(shù)表的內容去找對應的版本的方式來實現(xiàn)多態(tài)的。
<2>Runtime Polymorphism
方法的重寫就是運行時多態(tài)的例子。接下來我們介紹下什么是方法的重寫。
方法的覆蓋\重寫(function overriding)
方法的覆蓋也是c++編程的一種特性。這種機制允許我們在子類中擁有一個和父類一樣的方法。子類可以繼承了父類的數(shù)據(jù)成員和成員函數(shù)。當我們想要修改繼承于父類的某種方法的功能時,我們就可以通過覆蓋的機制來實現(xiàn)。通過這種方式,我們就好像在子類當中創(chuàng)建了一個父類對應該方法的新方法。
例子:
在重寫一個方法時,我們需要保證子類中的方法簽名和改寫的父類方法一致。這里所指的方法簽名是指參數(shù)的數(shù)據(jù)類型和順序。下面這個例子中,要覆蓋的父類方法中并沒有任何參數(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
注意:
在方法重寫中,在父類中對應的方法我們稱之為被重寫的方法(overridden function),在子類中的方法我們稱之為重寫方法(overriding function)。
如何通過子類調用被重寫的父類方法
上面的例子我們看到了子類調用重寫的方法。那么如何通過子類來調用父類中被重寫的方法(overridden function)呢?我們可以通過用父類引用指向子類的實例的方式來調用我們的父類被重寫的方法。用下面的例子來幫助我們理解:
#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)中調用父類中被重寫的函數(shù)(overridden function),你只需要這樣做:
//父類類名::方法名
parent_class_name::function_name
如果用上面的例子來解釋上面這種用法就是:
BaseClass::disp();
虛函數(shù)
當我們在父類中申明了一個函數(shù)為虛函數(shù)時,所有子類中該方法的重寫函數(shù)都會默認的被認為是虛函數(shù)(不管是否有virtual關鍵字)?,F(xiàn)在的問題是我們?yōu)槭裁匆v一個函數(shù)聲明為虛函數(shù)呢?這是為了讓編譯器明白這個函數(shù)的調用必須到運行時間才能確定。只有當對象的類型被確定時,才知道調用哪個版本的函數(shù)。
接下來讓我們來看兩個例子來幫助理解。分別重寫非虛函數(shù)的父類函數(shù)和重寫虛函數(shù)的父類函數(shù)。
重寫一個非虛函數(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
重寫一個虛函數(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
總結:
很顯然,重寫虛函數(shù)的方式使得調用的時候,程序是看對象的類型來決定調用的函數(shù)。在第二個覆蓋虛函數(shù)的例子中,我們可以看見雖然obj指針的指針類型是父類Animal,但是其指向的對象是子類Dog的對象。因此這時候,調用的animalSound是子類中的重寫函數(shù)而不是父類里頭的該函數(shù)。接下來我們通過一個例子再來進一步了解下它的特性。
#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)//報錯
我們通過打印的信息能夠更加清楚的了解到c++背后的運行機制。
- 首先我們創(chuàng)建了兩個對象,分別是基類對象和子類對象,并分別用基類指針base和子類指針child指向它們。這個過程對應輸出我們可以看出調用了構造函數(shù)。其中基類的構造函數(shù)被調用了兩次。說明在創(chuàng)建子類對象的時候,調用了子類的構造函數(shù),而子類的構造函數(shù)會先默認調用父類的構造函數(shù),然后執(zhí)行自己的構造函數(shù)。
- 通用基類和子類的指針分別調用print1和print2兩個函數(shù)。這個地方沒什么問題?;愔羔樦赶蚧悓ο?,調用的必然是基類中的函數(shù)。子類也一樣。
- 第三步刪除對象。這邊發(fā)現(xiàn)基類的析構函數(shù)也被調用了兩次。和構造函數(shù)相同,子類在調用析構函數(shù)的時候,也會調用一次父類的析構函數(shù)。不過不同的是調用時期。析構的時候,是在執(zhí)行完自己的析構函數(shù)體后再調用父類的析構。
- 最后一個涉及到多態(tài)的知識。首先創(chuàng)建了一個子類的指針child2并賦值為空指針nullptr。第一次調用print1,通過輸出可以知道,該語句可以正常執(zhí)行。并且調用的是子類的print1。說明了如果函數(shù)不是虛函數(shù)的情況下,c++是直接根據(jù)指針類型來判斷調用的函數(shù)版本(到底是基類的,還是基類的版本)。第二句調用print2函數(shù)時報錯。原因是print2是個虛函數(shù)(父類中聲明了是虛函數(shù),所有子類重寫該函數(shù)都默認是虛函數(shù))。通過虛函數(shù)的機制我們可以知道,要調用哪個版本(父類的版本還是子類的版本)需要在運行的時候通過指針所指向的對象類型來確定。此時,child2指針指向為空指針(沒有指向任何實例對象),因此報錯。
方法重載(function overloading)和方法重寫(function overriding)的不同
到此我們已經理解了什么是c++編程中的方法重載和方法重寫。讓我們來看看它們的不同:
- 方法重載是在同一個類里頭完成的。在同一個類中,我們聲明方法名相同但是參數(shù)列表不一樣的方法稱為方法重載。
- 在方法重載時,我們必須確認函數(shù)的簽名是不同的。但是在方法重寫中,我們必須保證重寫方法和被重寫方法具有相同的簽名。
- 方法重載發(fā)生在編譯期間,因此也被成為編譯時多態(tài)。而方法覆蓋發(fā)生在運行期間,因此也被稱為運行時多態(tài)。
- 方法重載時,在一個類中重載的方法數(shù)量沒有限制。方法重寫時,一個子類對應父類的某個方法只能被重寫一次。