多態性學習(上)
多態是指同樣的消息被不同類型的對象接收時導致不同的行為。所謂消息是指對類的成員函數的調用,不同的行為是指不同的實現,也就是調用了不同的函數。雖然這看上去好像很高級的樣子,事實上我們普通的程序設計中經常用到多態的思想。最簡單的例子就是運算符,使用同樣的加號“+”,就可以實現整型數之間、浮點數之間、雙精度浮點數之間的加法,以及這幾種數據類型混合的加法運算。同樣的消息--加法,被不同類型的對象—不同數據類型的變量接收後,采用不同的方法進行相加運算。這些就是多態現象。
面向對象的多態性可以分為4類:重載多態、強制多態、包含多態和參數多態。我們對於C++了解的函數的重載就是屬於重載多態,上文講到的運算符重載也是屬於重載多態的范疇。包含多態是類族中定義於不同類中的同名成員函數的多態行為,主要是通過虛函數來實現的。這一次的總結中主要講解重載多態和包含多態,剩下的兩種多態我將在下文繼續講解。
運算符重載是對已有的運算符賦予多重含義,使同一個運算符作用於不同類型的數據時導致不同的行為。運算符重載的實質就是函數重載。C++中預定義的運算符的操作對象只能是基本的數據類型,那麼我們有時候需要對自定義的數據類型(比如類)也有類似的數據運算操作。所以,我們的運算符重載的這一多態形式就衍生出來了。
相信看到這裡,應該有很多像我這樣的大學生並不陌生了吧,在我們鐘愛的ACM/ICPC中是不是經常遇到過的啊?沒錯,特別是在計算幾何中我們定義完一個向量結構體之後,需要對“+”“-”實行運算符重載,這樣我們就可以直接對向量進行加減乘除了。
運算符重載的規則
運算符重載的實現
1 #include<iostream>
2 #include<cstdio>
3 #include<cstring>
4 #include<cstdlib>
5 #include<cmath>
6 #include<algorithm>
7 #define inf 0x7fffffff
8 using namespace std;
9
10 class Complex {
11 public:
12 Complex (double r=0.0 , double i=0.0):real(r),imag(i){}
13 Complex operator + (const Complex &c2) const;
14 Complex operator - (const Complex &c2) const;
15 void display() const;
16 private:
17 double real;
18 double imag;
19 };
20
21 Complex Complex::operator + (const Complex &c2) const {
22 return Complex(real+c2.real , imag+c2.imag);
23 }
24 Complex Complex::operator - (const Complex &c2) const {
25 return Complex(real-c2.real , imag-c2.imag);
26 }
27 void Complex::display() const {
28 cout<<"("<<real<<", "<<imag<<")"<<endl;
29 }
30
31 int main()
32 {
33 Complex c1(5,4),c2(2,10),c3;
34 cout<<"c1= ";
35 c1.display();
36 cout<<"c2= ";
37 c2.display();
38 c3=c1+c2;
39 cout<<"c3=c1+c2 :";
40 c3.display();
41 c3=c1-c2;
42 cout<<"c3=c1-c2 :";
43 c3.display();
44 return 0;
45 }

在本例中,將復數的加減法這樣的運算重載為復數類的成員函數,可以看出,除了在函數聲明及實現的時候使用了關鍵字operator之外,運算符重載成員函數與類的普通成員函數沒有什麼區別。在使用的時候,可以直接通過運算符、操作數的方式來完成函數調用。這時,運算符“+”、“-”原有的功能都不改變,對整型數、浮點數等基本類型數據的運算仍然遵循C++預定義的規則,同時添加了新的針對復數運算的功能。
1 #include<iostream>
2 #include<cstdio>
3 #include<cstring>
4 #include<cstdlib>
5 #include<cmath>
6 #include<algorithm>
7 #define inf 0x7fffffff
8 using namespace std;
9
10 class Clock {
11 public:
12 Clock(int hour=0,int minute=0,int second=0);
13 void showTime() const;
14 Clock& operator ++ ();
15 Clock operator ++ (int);
16 private:
17 int hour,minute,second;
18 };
19
20 Clock::Clock(int hour,int minute,int second) {
21 if (hour>=0&&hour<24 && minute>=0&&minute<60 && second>=0&&second<60) {
22 this->hour = hour;
23 this->minute = minute;
24 this->second = second;
25 }
26 else {
27 cout<<"Time error!"<<endl;
28 }
29 }
30 void Clock::showTime() const {
31 cout<<hour<<":"<<minute<<":"<<second<<endl;
32 }
33 Clock & Clock::operator ++ () {
34 second ++ ;
35 if (second >= 60) {
36 second -= 60;
37 minute ++ ;
38 if (minute >= 60) {
39 minute -= 60;
40 hour = (hour+1)%24;
41 }
42 }
43 return *this;
44 }
45 Clock Clock::operator ++ (int) {
46 Clock old= *this;
47 ++(*this);
48 return old;
49 }
50
51 int main()
52 {
53 Clock myClock(23,59,59);
54 cout<<"First time output: ";
55 myClock.showTime();
56 cout<<"show myClock++: ";
57 (myClock++).showTime();
58 cout<<"show ++myClock: ";
59 (++myClock).showTime();
60 return 0;
61 }

這個例子中,我們把時間自增前置++和後置++運算重載為時鐘類的成員函數,前置單目運算符和後置單目運算符的重載最主要的區別就在於重載函數的形參。
語法規定:前置單目運算符重載為成員函數時沒有形參,後置單目運算符重載為成員函數時需要有一個int型形參。
1 #include<iostream>
2 #include<cstdio>
3 #include<cstring>
4 #include<cstdlib>
5 #include<cmath>
6 #include<algorithm>
7 #define inf 0x7fffffff
8 using namespace std;
9
10 class Complex {
11 public:
12 Complex (double r=0.0,double i=0.0):real(r),imag(i){}
13 friend Complex operator + (const Complex &c1,const Complex &c2);
14 friend Complex operator - (const Complex &c1,const Complex &c2);
15 friend ostream & operator << (ostream &out,const Complex &c);
16 private:
17 double real;
18 double imag;
19 };
20
21 Complex operator + (const Complex &c1,const Complex &c2) {
22 return Complex(c1.real+c2.real , c1.imag+c2.imag);
23 }
24 Complex operator - (const Complex &c1,const Complex &c2) {
25 return Complex(c1.real-c2.real , c1.imag-c2.imag);
26 }
27 ostream & operator << (ostream &out,const Complex &c) {
28 cout<<"("<<c.real<<", "<<c.imag<<")"<<endl;
29 return out;
30 }
31
32 int main()
33 {
34 Complex c1(5,4),c2(2,10),c3;
35 cout<<"c1= "<<c1<<endl;
36 cout<<"c2= "<<c2<<endl;
37 c3=c1+c2;
38 cout<<"c3=c1+c2 :"<<c3<<endl;
39 c3=c1-c2;
40 cout<<"c3=c1-c2 :"<<c3<<endl;
41 return 0;
42 }
這一次我們將運算符重載為類的非成員函數,就必須把操作數全部通過形參的方式傳遞給運算符重載函數,“<<”操作符的左操作數為ostream類型的引用,ostream是cout類型的一個基類,右操作數是Complex類型的引用,這樣在執行cout<<c1時,就會調用operator<<(cout,c1)。
剛才就有說到,虛函數是包含多態的主要內容。那麼,我們就來看看什麼是虛函數。
虛函數是動態綁定的基礎。虛函數經過派生之後,在類族中就可以實現運行過程中的多態。
根據賦值兼容規則,可以使用派生類的對象來代替基類對象。如果用基類類型的指針指向派生類對象,就可以通過這個指針來訪問該對象,但是我們訪問到的只是從基類繼承來的同名成員。解決這一問題的方法是:如果需要通過基類的指針指向派生類的對象,並訪問某個與基類同名的成員,那麼首先在基類中將這個同名函數說明為虛函數。這樣,通過基類類型的指針,就可以使屬於不同派生類的不同對象產生不同的行為,從而實現運行過程的多態。
上面這一段文字初次讀來有點生拗,希望讀者多讀兩遍,因為這是很重要也是很核心的思想。接下來,我們看看兩段代碼,體會一下基類中虛函數的作用。
1 #include<iostream>
2 #include<cstdio>
3 #include<cstring>
4 #include<cstdlib>
5 #include<cmath>
6 #include<algorithm>
7 #define inf 0x7fffffff
8 using namespace std;
9
10 class A {
11 public:
12 A() {}
13 virtual void foo() {
14 cout<<"This is A."<<endl;
15 }
16 };
17 class B:public A {
18 public:
19 B(){}
20 void foo() {
21 cout<<"This is B."<<endl;
22 }
23 };
24
25 int main()
26 {
27 A *a=new B();
28 a->foo();
29 if (a != NULL) delete a;
30 return 0;
31 }

1 #include<iostream>
2 #include<cstdio>
3 #include<cstring>
4 #include<cstdlib>
5 #include<cmath>
6 #include<algorithm>
7 #define inf 0x7fffffff
8 using namespace std;
9
10 class Base1 {
11 public:
12 virtual void display() const;
13 };
14 void Base1::display() const {
15 cout<<"Base1::display()"<<endl;
16 }
17
18 class Base2:public Base1 {
19 public:
20 void display() const;
21 };
22 void Base2::display() const {
23 cout<<"Base2::display()"<<endl;
24 }
25
26 class Derived:public Base2 {
27 public:
28 void display() const;
29 };
30 void Derived::display() const {
31 cout<<"Derived::display()"<<endl;
32 }
33
34 void fun(Base1 *ptr) {
35 ptr->display();
36 }
37
38 int main()
39 {
40 Base1 base1;
41 Base2 base2;
42 Derived derived;
43 fun(&base1);
44 fun(&base2);
45 fun(&derived);
46 return 0;
47 }

在後面的一段程序中,派生類並沒有顯式的給出虛函數的聲明,這時系統就會遵循以下規則來判斷派生類的一個函數成員是否是虛函數:
虛析構函數
在C++中,不能聲明虛構造函數,但是可以聲明虛析構函數。如果一個類的析構函數是虛函數,那麼由它派生而來的所有子類的析構函數也是虛函數。在析構函數設置為虛函數之後,在使用指針引用時可以動態綁定,實現運行時的多態,保證使用基類類型的指針就能夠調用適當的析構函數針對不用的對象進行清理工作。
1 #include<iostream>
2 #include<cstdio>
3 #include<cstring>
4 #include<cstdlib>
5 #include<cmath>
6 #include<algorithm>
7 #define inf 0x7fffffff
8 using namespace std;
9
10 class Base {
11 public:
12 ~Base();
13 };
14 Base::~Base() {
15 cout<<"Base destructor"<<endl;
16 }
17
18 class Derived:public Base {
19 public:
20 Derived();
21 ~Derived();
22 private:
23 int *p;
24 };
25 Derived::Derived() {
26 p=new int(0);
27 }
28 Derived::~Derived() {
29 cout<<"Derived destructor"<<endl;
30 delete p;
31 }
32
33 void fun(Base *b) {
34 delete b;
35 }
36
37 int main()
38 {
39 Base *b=new Derived();
40 fun(b);
41 return 0;
42 }

這說明,通過基類指針刪除派生類對象時調用的是基類的析構函數,派生類的析構函數沒有被執行,因此派生類對象中動態分配的內存空間沒有得到釋放,造成了內存洩露。
避免上述錯誤的有效方法就是將析構函數聲明為虛函數:
1 class Base {
2 public:
3 virtual ~Base();
4 };
此時,我們再次運行這一份代碼,得到的結果就如下圖所示。

這說明派生類的析構函數被調用了,派生類對象中動態申請的內存空間被正確地釋放了。這是由於使用了虛析構函數,實現了多態。