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
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章