在《C++基類對象和派生類對象的賦值》一節中講到,基類的指針也可以指向派生類對象,請看下面的例子:
#include <iostream>
using namespace std;
//基類People
class People{
public:
People(char *name, int age);
void display();
protected:
char *m_name;
int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){
cout<<m_name<<"今年"<<m_age<<"歲了,是個無業游民。"<<endl;
}
//派生類Teacher
class Teacher: public People{
public:
Teacher(char *name, int age, int salary);
void display();
private:
int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){
cout<<m_name<<"今年"<<m_age<<"歲了,是一名教師,每月有"<<m_salary<<"元的收入。"<<endl;
}
int main(){
People *p = new People("王志剛", 23);
p -> display();
p = new Teacher("趙宏佳", 45, 8200);
p -> display();
return 0;
}
運行結果:
王志剛今年23歲了,是個無業游民。
趙宏佳今年45歲了,是個無業游民。
我們直觀上認為,如果指針指向了派生類對象,那麼就應該使用派生類的成員變量和成員函數,這符合人們的思維習慣。但是本例的運行結果卻告訴我們,當基類指針 p 指向派生類 Teacher 的對象時,雖然使用了 Teacher 的成員變量,但是卻沒有使用它的成員函數,導致輸出結果不倫不類(趙宏佳本來是一名老師,輸出結果卻顯示人家是個無業游民),不符合我們的預期。
換句話說,通過基類指針只能訪問派生類的成員變量,但是不能訪問派生類的成員函數。
為了消除這種尴尬,讓基類指針能夠訪問派生類的成員函數,C++ 增加了虛函數(Virtual Function)。使用虛函數非常簡單,只需要在函數聲明前面增加 virtual 關鍵字。
更改上面的代碼,將 display() 聲明為虛函數:
#include <iostream>
using namespace std;
//基類People
class People{
public:
People(char *name, int age);
virtual void display(); //聲明為虛函數
protected:
char *m_name;
int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){
cout<<m_name<<"今年"<<m_age<<"歲了,是個無業游民。"<<endl;
}
//派生類Teacher
class Teacher: public People{
public:
Teacher(char *name, int age, int salary);
virtual void display(); //聲明為虛函數
private:
int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){
cout<<m_name<<"今年"<<m_age<<"歲了,是一名教師,每月有"<<m_salary<<"元的收入。"<<endl;
}
int main(){
People *p = new People("王志剛", 23);
p -> display();
p = new Teacher("趙宏佳", 45, 8200);
p -> display();
return 0;
}
運行結果:
王志剛今年23歲了,是個無業游民。
趙宏佳今年45歲了,是一名教師,每月有8200元的收入。
和前面的例子相比,本例僅僅是在 display() 函數聲明前加了一個
virtual
關鍵字,將成員函數聲明為了虛函數(Virtual Function),這樣就可以通過 p 指針調用 Teacher 類的成員函數了,運行結果也證明了這一點(趙宏佳已經是一名老師了,不再是無業游民了)。
有了虛函數,基類指針指向基類對象時就使用基類的成員(包括成員函數和成員變量),指向派生類對象時就使用派生類的成員。換句話說,基類指針可以按照基類的方式來做事,也可以按照派生類的方式來做事,它有多種形態,或者說有多種表現方式,我們將這種現象稱為多態(Polymorphism)。
上面的代碼中,同樣是
p->display();
這條語句,當 p 指向不同的對象時,它執行的操作是不一樣的。同一條語句可以執行不同的操作,看起來有不同表現方式,這就是多態。
多態是面向對象編程的主要特征之一,C++中虛函數的唯一用處就是構成多態。
C++提供多態的目的是:可以通過基類指針對所有派生類(包括直接派生和間接派生)的成員變量和成員函數進行“全方位”的訪問,尤其是成員函數。如果沒有多態,我們只能訪問成員變量。
前面我們說過,通過指針調用普通的成員函數時會根據指針的類型(通過哪個類定義的指針)來判斷調用哪個類的成員函數,但是通過本節的分析可以發現,這種說法並不適用於虛函數,虛函數是根據指針的指向來調用的,指針指向哪個類的對象就調用哪個類的虛函數。
但是話又說回來,對象的內存模型是非常干淨的,沒有包含任何成員函數的信息,編譯器究竟是根據什麼找到了成員函數呢?我們將在《C++虛函數表vtable以及動態綁定》一節中給出答案。
借助引用也可以實現多態
引用在本質上是通過指針的方式實現的,這一點已在《引用在本質上是什麼,它和指針到底有什麼區別》中進行了講解,既然借助指針可以實現多態,那麼我們就有理由推斷:借助引用也可以實現多態。
修改上例中 main() 函數內部的代碼,用引用取代指針:
int main(){
People p("王志剛", 23);
Teacher t("趙宏佳", 45, 8200);
People &rp = p;
People &rt = t;
rp.display();
rt.display();
return 0;
}
運行結果:
王志剛今年23歲了,是個無業游民。
趙宏佳今年45歲了,是一名教師,每月有8200元的收入。
由於引用類似於常量,只能在定義的同時初始化,並且以後也要從一而終,不能再引用其他數據,所以本例中必須要定義兩個引用變量,一個用來引用基類對象,一個用來引用派生類對象。從運行結果可以看出,當基類的引用指代基類對象時,調用的是基類的成員,而指代派生類對象時,調用的是派生類的成員。
不過引用不像指針靈活,指針可以隨時改變指向,而引用只能指代固定的對象,在多態性方面缺乏表現力,所以以後我們再談及多態時一般是說指針。本例的主要目的是讓讀者知道,除了指針,引用也可以實現多態。
多態的用途
通過上面的例子讀者可能還未發現多態的用途,不過確實也是,多態在小項目中鮮有有用武之地。
接下來的例子中,我們假設你正在玩一款軍事游戲,敵人突然發動了地面戰爭,於是你命令陸軍、空軍及其所有現役裝備進入作戰狀態。具體的代碼如下所示:
#include <iostream>
using namespace std;
//軍隊
class Troops{
public:
virtual void fight(){ cout<<"Strike back!"<<endl; }
};
//陸軍
class Army: public Troops{
public:
void fight(){ cout<<"--Army is fighting!"<<endl; }
};
//99A主戰坦克
class _99A: public Army{
public:
void fight(){ cout<<"----99A(Tank) is fighting!"<<endl; }
};
//武直10武裝直升機
class WZ_10: public Army{
public:
void fight(){ cout<<"----WZ-10(Helicopter) is fighting!"<<endl; }
};
//長劍10巡航導彈
class CJ_10: public Army{
public:
void fight(){ cout<<"----CJ-10(Missile) is fighting!"<<endl; }
};
//空軍
class AirForce: public Troops{
public:
void fight(){ cout<<"--AirForce is fighting!"<<endl; }
};
//J-20隱形殲擊機
class J_20: public AirForce{
public:
void fight(){ cout<<"----J-20(Fighter Plane) is fighting!"<<endl; }
};
//CH5無人機
class CH_5: public AirForce{
public:
void fight(){ cout<<"----CH-5(UAV) is fighting!"<<endl; }
};
//轟6K轟炸機
class H_6K: public AirForce{
public:
void fight(){ cout<<"----H-6K(Bomber) is fighting!"<<endl; }
};
int main(){
Troops *p = new Troops;
p ->fight();
//陸軍
p = new Army;
p ->fight();
p = new _99A;
p -> fight();
p = new WZ_10;
p -> fight();
p = new CJ_10;
p -> fight();
//空軍
p = new AirForce;
p -> fight();
p = new J_20;
p -> fight();
p = new CH_5;
p -> fight();
p = new H_6K;
p -> fight();
return 0;
}
運行結果:
Strike back!
--Army is fighting!
----99A(Tank) is fighting!
----WZ-10(Helicopter) is fighting!
----CJ-10(Missile) is fighting!
--AirForce is fighting!
----J-20(Fighter Plane) is fighting!
----CH-5(UAV) is fighting!
----H-6K(Bomber) is fighting!
這個例子中的派生類比較多,如果不使用多態,那麼就需要定義多個指針變量,很容易造成混亂;而有了多態,只需要一個指針變量 p 就可以調用所有派生類的虛函數。
從這個例子中也可以發現,對於具有復雜繼承關系的大中型程序,多態可以增加其靈活性,讓代碼更具有表現力。