C++程式設計(八)—— 多型性和虛擬函式
一、多型性
靜態聯編所支援的多型性稱為編譯時的多型性,當呼叫過載函式時,編譯器可以根據呼叫時所使用的實參在編譯時就確定應該呼叫哪個函式;動態聯編所支援的多型性稱為執行時的多型性,這由虛擬函式來支援。虛擬函式類似於過載函式,但與過載函式的實現策略不同,即對虛擬函式的呼叫使用動態聯編。
1、靜態聯編中的賦值相容性及名字支配規律
派生一個類的原因並非總是為了新增新的資料成員或成員函式,有時是為了重新定義基類的成員函式。先看一個示例如下:
const double PI = 3.14159; class Point { private: double x, y; public: Point(double a, double b) : x(a), y(b) { } double area() { return 0; } }; class Circle: public Point { private: double radius; public: Circle(double a, double b, double c) :Point(a, b) { this->radius = c; } double area() { return PI * radius * radius; } };
#include "Example1.h" void example1(); int main() { example1(); return 0; } void example1() { Point a(10.5, 12.3); cout << a.area() << endl; //0 名字支配規律決定它們只調用自己的area()函式 Circle c(10.5, 12.3, 13.5); cout << c.area() << endl; //572.555 同上 Point *p1 = &c; cout << p1->area() << endl; //0 根據賦值相容規則,Point類的指標指向的是基類Point的area() Point &p2 = c; cout << p2.area() << endl; //0 根據賦值相容規則,Point類的引用跟指標一樣 }
物件的記憶體地址空間中只包含資料成員,並不儲存有關成員函式的資訊,這些成員函式的地址翻譯過程與其物件的記憶體地址無關,編譯器只根據資料型別來翻譯成員函式的地址並判斷其呼叫的合法性,這是由靜態聯編決定的。
宣告的基類指標只能指向基類,派生類指標只能指向派生類,它們的原始型別決定它們只能呼叫各自的同名函式,除非派生類沒有基類的同名函式,派生類的指標才根據繼承呼叫基類的成員函式。
2、動態聯編的多型性
如果讓編譯器進行動態聯編,這就需要使用到關鍵字virtual來宣告虛擬函式。當編譯系統編譯含有虛擬函式的類時,將為它建立一個虛擬函式表,表中的每一個元素都指向一個虛擬函式的地址。此外,編譯器也為類增加一個數據成員,這個資料成員是一個指向該虛擬函式表的指標,通常稱為vptr。
虛擬函式的地址翻譯取決於物件的記憶體地址,編譯器為含有虛擬函式的物件首先建立一個入口地址,這個地址用來存放指向虛擬函式表的指標vptr,然後按照類中虛擬函式的宣告次序,一一填入函式指標。當呼叫虛擬函式時,先通過vptr找到虛擬函式表,然後再找出虛擬函式的真正地址。
派生類可以繼承基類的虛擬函式表,而且只要和基類同名的成員函式,無論是否使用virtual宣告,它們都自動成為虛擬函式。如果派生類沒有改寫繼承基類的虛擬函式,則函式指標呼叫基類的虛擬函式;如果派生類改寫了基類的虛擬函式,編譯器將重新為派生類的虛擬函式建立地址,函式指標會呼叫改寫過的虛擬函式。
虛擬函式的呼叫規則是:根據當前物件,優先呼叫物件本身的虛成員函式。
示例如下:
const double PI1 = 3.14159;
class Point1 {
private:
double x, y;
public:
Point1(double a, double b) :
x(a), y(b) {
}
virtual double area() {
return 0;
}
};
class Circle1: public Point1 {
private:
double radius;
public:
Circle1(double a, double b, double c) :Point1(a, b) {
this->radius = c;
}
virtual double area() {
return PI1 * radius * radius;
}
};
#include "Example2.h"
void example2();
int main() {
example2();
return 0;
}
void example2(){
Point1 a(10.5, 12.3);
cout << a.area() << endl; //0 名字支配規律決定它們只調用自己的area()函式
Circle1 c(10.5, 12.3, 13.5);
cout << c.area() << endl; //572.555
Point1 *p1 = &c;
cout << p1->area() << endl; //572.555 虛擬函式優先呼叫物件本身的成員函式
Point1 &p2 = c;
cout << p2.area() << endl; //572.555 同上
}
二、虛擬函式
一旦基類定義了虛擬函式,該基類的派生類的同名函式也自動成為虛擬函式。
1、虛擬函式的定義
虛擬函式只能是類中的一個成員函式,但不能是靜態成員,關鍵字virtual用於類中該函式的宣告。如下:
class A{
public:
virtual void func(); //宣告虛擬函式
};
當在派生類中定義了一個同名的成員函式時,只要該成員函式的引數個數和函式型別以及它的返回型別同基類中同名虛擬函式的一樣,則無論是否為該成員函式使用virtual,它都將成為一個虛擬函式。
2、虛擬函式實現多型性的條件
關鍵字virtual指示C++編譯器對呼叫虛擬函式使用動態聯編,這種多型性是程式執行到需要的語句處才動態確定的,所以稱為執行時的多型性。產生執行時的多型性有如下三個前提:
① 類之間的繼承關係滿足賦值相容性規則;
② 改寫了同名虛擬函式;
③ 根據賦值相容性規則使用指標或引用。
下面設計一個外部函式,把指標或引用作為函式引數來實現動態聯編,示例如下:
const double PI2 = 3.14159;
class Point2 {
private:
double x, y;
public:
Point2(double a, double b) :
x(a), y(b) {
}
virtual double area() {
return 0;
}
};
class Circle2: public Point2 {
private:
double radius;
public:
Circle2(double a, double b, double c) :Point2(a, b) {
this->radius = c;
}
virtual double area() {
return PI2 * radius * radius;
}
};
void display(Point2 &p){
cout << p.area() << endl;
}
void display(Point2 *p){
cout << p->area() << endl;
}
#include "Example3.h"
void example3();
int main() {
example3();
return 0;
}
void example3(){
Point2 a(10.5, 12.3);
Circle2 c(10.5, 12.3, 13.5);
Point2 *p1 = &c;
Point2 &p2 = c;
display(a); //0
display(p1); //572.555
display(p2); //572.555
}
3、建構函式和解構函式呼叫虛擬函式
在建構函式和解構函式中呼叫虛擬函式採用靜態聯編,即它們所呼叫的虛擬函式是自己的類或基類中定義的函式,但不是任何在派生類中重定義的虛擬函式。示例如下:
class A {
public:
A() {
}
virtual ~A() {
}
virtual void fun1() {
cout << "建立類A的物件" << endl;
}
virtual void fun2() {
cout << "銷燬類A的物件" << endl;
}
};
class B: public A {
public:
B() {
fun1();
}
~B() {
fun2();
}
void fun3() {
cout << "程式執行到這裡然後...";
fun1();
}
};
class C: public B {
public:
C() {
}
~C() {
fun2();
}
void fun1(){
cout << "這是類C" << endl;
}
void fun2(){
cout << "銷燬類C的物件" << endl;
}
};
#include "Example4.h"
void example4();
int main() {
example4();
return 0;
}
void example4(){
C c;
/**
* 建立類C的物件時,會首先建立基類的物件,然後再建立派生類的物件,A類的建構函式沒有任何輸出,
* B類的建構函式呼叫了A類的虛擬函式,這裡輸出的是:“建立類A的物件”
*/
c.fun3();
/**
* 類C的物件呼叫fun3函式,而自己沒有就去呼叫基類的fun3函式,首先輸出:“程式執行到這裡然後...”,
* 然後呼叫fun1函式,由於類C中有定義這個函式,所以會呼叫自己的fun1函式輸出:“這是類C”
*/
}
/**
* 程式結束之後,會按照建立物件相反的順序,即先建立後析構的原則析構物件,則它會首先呼叫類C的解構函式,
* 類C的解構函式呼叫了fun2函式,則會先輸出:“銷燬類C的物件”,然後呼叫類B的解構函式,類B的解構函式
* 也呼叫了fun2函式,但類B中並沒有定義這個函式,所以它呼叫了基類A中的fun2函式,輸出了:“銷燬類A的物件”,
* 緊接著類A的解構函式被呼叫,但類A的解構函式並沒有進行任何輸出
*/
//執行結果如下:
//建立類A的物件
//程式執行到這裡然後...這是類C
//銷燬類C的物件
//銷燬類A的物件
目前推薦的C++標準並不支援虛建構函式,但是它支援虛解構函式。由於解構函式不允許有引數,因此一個類只能有一個虛解構函式。delete運算子和解構函式一起工作,當使用delete刪除一個物件時,delete隱含著對解構函式的一次呼叫,如果解構函式為虛擬函式,則這個呼叫採用動態聯編。一般來說,一個類如果定義了虛擬函式,解構函式也應說明為虛擬函式,尤其是在解構函式要完成一些有意義的任務時,例如釋放記憶體等。
只要基類中的解構函式定義為虛擬函式,則派生類中的解構函式無論是否宣告都自動成為虛擬函式。
4、純虛擬函式與抽象類
在有些情況下,不能在基類中為虛擬函式給出一個有意義的定義,這時可以將它說明為純虛擬函式,將其定義留給派生類去做。說明純虛擬函式的形式如下:
class 類名{
virtual 函式型別 函式名(引數列表) = 0;
};
//示例
class Square{
virtual double area() = 0;
};
一個類可以說明多個純虛擬函式,包含有純虛擬函式的類稱為抽象類。一個抽象類只能作為基類來派生新類,不能說明抽象類的物件,但可以說明指向抽象類物件的指標或引用。如下示例:
Point &p1;
Point *p2;
從一個抽象類派生的類必須提供純虛擬函式的實現程式碼,或在派生類中仍將它說明為純虛擬函式,否則編譯器將會報錯。這說明了純虛擬函式的派生類仍是抽象類,如果派生類中給出了所有基類中純虛擬函式的實現,則該派生類不再是抽象類。抽象類至少含有一個虛擬函式,而且至少有一個虛擬函式是純虛擬函式,以便將它與空的虛擬函式區分開來。下面是虛擬函式兩種不同的表示方法:
virtual void area() = 0; //純虛擬函式
virtual void area() {} //空的虛擬函式
下面是一個示例,計算正文形和長方形的面積:
class Shape{//抽象類:包含一個純虛擬函式
public:
virtual double area() = 0;
virtual ~Shape(){};
};
class Square:public Shape{
private:
double length;
public:
Square(double a):length(a){
}
~Square(){
cout << "析構Square類的物件" << endl;
}
virtual double area(){
return length*length;
}
};
class Rectangle:public Shape{
private:
double width,height;
public:
Rectangle(double a,double b):width(a),height(b){
}
~Rectangle(){
cout << "析構Rectangle類的物件" << endl;
}
virtual double area(){
return width*height;
}
};
#include "Example5.h"
void example5();
int main() {
example5();
return 0;
}
void example5(){
Shape *s[2];
s[0] = new Square(10);
s[1] = new Rectangle(5,10);
cout << "面積之和是:" << s[0]->area() + s[1]->area() << endl;
delete s[0];
delete s[1];
}
三、多重繼承與虛擬函式
多重繼承可以視為多個單一繼承的組合。示例如下:
class AA{
public:
virtual void fun(){
cout << "this is AA" << endl;
}
};
class BB{
public:
virtual void fun(){
cout << "this is BB" << endl;
}
};
class CC:public AA,public BB{
public:
void fun(){
cout << "this is CC" << endl;
}
};
#include "Example6.h"
void example6();
int main() {
example6();
return 0;
}
void example6(){
AA *a;
BB *b;
CC *c1,c2;
a = &c2;
b = &c2;
c1 = &c2;
a->fun();
b->fun();
c1->fun();
// this is CC
// this is CC
// this is CC
}