派生類繼承了基類除構造函數和析構函數外的所有數據成員和函數成員。派生類和基類存在一種特殊關係:派生類是一種基類,具有基類的所有功能。面向對象的程式設計利用派生類和基類之間的特殊關係,常常將派生類對象當作基類對象使用,或者用基類來代表派生類,其目的是提高代碼可重用性。由於C++對數據類型一致性要求比較 ...
派生類繼承了基類除構造函數和析構函數外的所有數據成員和函數成員。派生類和基類存在一種特殊關係:派生類是一種基類,具有基類的所有功能。面向對象的程式設計利用派生類和基類之間的特殊關係,常常將派生類對象當作基類對象使用,或者用基類來代表派生類,其目的是提高代碼可重用性。由於C++對數據類型一致性要求比較嚴格,一般不能調用處理A類對象的函數afun(A x)來處理B類對象數據。
一、認識對象的替換和多態
通過一個例子更直觀的理解對象的替換和多態:
class A
{
public:
void fun1() //普通函數成員fun1
{ cout<< "A::fun1 called\n"; }
virtual void fun2() //虛函數成員fun2
{ cout<< "Virtual A::fun2 called\n"; }
};
class B:public A //定義派生類B,公有繼承A
{
public:
void fun1() //新增基類同名函數成員fun1
{ cout<< "B::fun1 called\n"; }
virtual void fun2() //新增基類同名虛函數成員fun2
{ cout<< "Virtual B::fun2 called\n"; }
};
void exfun(A &x) //類外函數處理類對象
{
cout<<"exfun call class A\n";
x.fun1();
}
void exfun1(A &x) //類外函數調用類虛函數,實現對象多態性
{
cout<<"exfun call class A\n";
x.fun2();
}
下麵通過兩個類分別定義對象和引用來觀察它們所調用的函數是那些
A aobj;B bobj; //分別定義兩個類的對象
A &ra1=aobj;
A &ra2=bobj; //通過基類對象引用兩個對象
ra1.fun1(); //顯示 A::fun1 called
ra2.fun1(); //顯示 A::fun1 called
ra1.fun2(); //顯示 Virtual A::fun2 called
ra2.fun2(); //顯示 Virtual B::fun2 called
得到以下結論:
1)通過基類ra2來引用派生類對象bobj,相當於將派生類對象當作基類對象來使用,這被稱作對象替換。
2)通過基類引用調用虛函數成員fun2,基類對象aobj和派生類對象bobj會顯示不同信息。換言之,接收相同指令fun2,基類對象和派生類對象表現出不同行為,呈現多樣化形態,這就是對象的多態性。
二、類型相容語法規則(對象替換)
為了讓派生類對象可以與基類對象一起共用演算法代碼,C++語言專門指定瞭如下的類型相容語法規則:
1)派生類的對象可以賦值給基類對象。
2)派生類的對象可以初始化基類引用,或者說基類引用可以引用派生類對象。
3)派生類對象的地址可以賦值給基類指針,或者說基類的對象指針可指向派生類對象。
應用類型相容語法規則有一個前提條件和一個使用限制。
前提條件:派生類必須共有繼承基類。因為公有繼承下,派生類擁有基類的全部功能,派生類對象可以當作基類對象使用。
使用限制:通過基類的對象、引用或對象指針訪問派生類對象時,只能訪問到基類成員,賦值和訪問時只能接收基類成員。
示例:
A x1;B x2; A &x3 = x1; //定義類A,類B對象和類A的引用並初始化為類x1
x1 = x2; //派生類對象給基類對象賦值
x3 = x2; //基類引用可以引用派生類對象
A *p; p = &x2; //基類對象指針指向派生類對象
exfun(x2); //輸出exfun call class A[換行] A::fun1 called
//此時通過類A的外部處理函數處理派生類B對象,完成代碼重用
三、對象的多態性
通過上面示例思考,如何通過基類引用或指針訪問派生類中與基類中同名的函數?我們將調用對象的某個函數成員稱為向對象發送一條消息。將執行函數成員完成某種程式功能稱為對象響應該消息。不同對象接收相同消息,但會表現除不同的行為,這就是對象的多態性。對象多態性就是:調用不同對象的同名函數成員,所執行的函數不同,完成的程式功能也不同。導致對象多態性的同名函數有以下三種不同形式:
1) 不同類之間的同名函數。類成員有作用域,不同類之間的函數成員可以同名互不幹擾。
2)類中的重載函數。通過一類中的函數成員可以重名,只要他們的形參個數不同或類型不同。重載函數成員導致的多態本質上屬於重載函數多態。
3)派生類中的同名覆蓋。派生類中新增的函數成員可以與從基類繼承來函數成員同名,但他們不是重載函數。
引入對象多態性的目的是為了讓另外一些處理基類的代碼也能夠被派生類對象重用。“另外一些”代碼的作用也就是通過基類引用或對象指針訪問派生類代碼時可以根據實際引用或指向的對象類型,自動調用該類同名函數中新增成員。這是需要將這些同名函數聲明成虛函數。C++語言以虛函數的語法形式來實現對象多態性,例如示例中的exfun1(A &x)
函數。
四、虛函數
應用虛函數實現對象多態性的過程分為兩步:先聲明虛函數,在調用虛函數。
1、聲明虛函數
在定義基類時使用關鍵字virtual
將函數聲明成虛函數,然後通過公有繼承定義派生類,並重寫虛函數成員,也就是新增一個與虛函數同名的函數成員。此後使用基類或派生類定義對象,其函數成員中只有虛函數成員才會在調用時呈現出多態性。
為了更好的說明虛函數的聲明和使用,我們編寫一個簡單示例代碼如下:
class A //類聲明
{
public:
virtual void fun1(); //聲明虛函數成員fun1
void fun2(); //聲明非虛函數成員fun2
};
//類實現
void A::fun1() { cout<<"Base class A:virtual fun1() called"<<endl; }
void A::fun2() { cout<<"Base class A:non-virtual fun2() called"<<endl; }
class B:public A //定義派生類B,公有繼承A
{
public:
virtual void fun1(); //重寫虛函數成員fun1
void fun2(); //重寫非虛函數成員fun2
};
void B::fun1() { cout<<"Derived class B:virtual fun1() called"<<endl; }
void B::fun2() { cout<<"Derived class B:non-virtual fun2() called"<<endl; }
聲明虛函數的語法細則:
1)只能在類聲明部分聲明虛函數。在類實現部分定義函數成員時不能在使用關鍵字virtual
。
2)基類中聲明虛函數成員被繼承到派生類後,自動成為派生類的虛函數成員。
3)派生類可以重寫虛函數成員。如果重寫後的函數原型與基類虛函數成員完全一致,則該函數自動成為派生類的虛函數成員,無論聲明時加不加virtual
。
4)類函數成員中的靜態函數、構造函數不能是虛函數。析構函數可以是虛函數。
2、調用虛函數
下麵我們通過派生類對象、基類引用和基類對象指針分別調用派生類虛函數和非虛函數,得到的結果如下:
//通過對象名調用函數成員
A aObj; B bObj;
bObj.fun1(); //調用結果:調用派生類bObj的新增虛函數成員fun1
bObj.fun2(); //調用結果:調用派生類bObj的新增非虛函數成員fun2(同名覆蓋)
//通過基類引用調用函數成員
A &raObj = aObj; //定義基類引用,引用基類對象
raObj.fun1(); //調用結果:調用基類對象aObj的虛函數成員fun1
raObj.fun2(); //調用結果:調用基類對象aObj的非虛函數成員fun2
A &rbObj = bObj; //定義基類引用,引用基類對象
rbObj.fun1(); //調用結果:調用派生類對象bObj的新增虛函數成員fun1
rbObj.fun2(); //調用結果:調用派生類對象bObj的基類非虛函數成員fun2(類型相容規則)
//通過基類對象指針調用函數成員
A *paObj = &aObj;//定義基類對象指針paObj,指向基類對象aObj
paObj->fun1(); //調用結果:調用基類對象aObj的虛函數成員fun1
paObj->fun2(); //調用結果:調用基類對象aObj的非虛函數成員fun2
A *pbObj = &bObj;//定義基類對象指針paObj,指向基類對象aObj
pbObj->fun1(); //調用結果:調用派生類對象bObj的新增虛函數成員fun1
pbObj->fun2(); //調用結果:調用派生類對象bObj的基類非虛函數成員fun2(類型相容規則)
總結:通過基類的引用或對象指針訪問類族中對象的虛函數成員(例如:fun1),基類對象和派生類對象將分別調用各自的虛函數成員,呈現出多態性。如果訪問的是非虛函數成員(例如:fun2),則訪問的都是基類成員,不會呈現多態性。
實現基類對象與派生類對象之間的多態性要滿足以下三個條件:
1)在基類中聲明虛函數成員。
2)派生類需公有繼承基類,並重寫虛函數成員(屬於新增成員)。
3)通過基類的引用或對象指針調用虛函數成員。
只有滿足這三個條件,基類對象和派生類對象才會分別調用各自的虛函數,呈現出多態性。
將源程式中具有多態性的虛函數名轉換成某個具體的函數存儲地址,這種函數名到存儲地址的轉換被稱為是對函數的綁定。通過基類的引用或對象指針調用虛函數成員,到底是調用基類成員還是新增成員,這在編譯時還不能確定。其綁定過程需要在程式執行時才能完成。對象多態是一種執行時多態。
總結以下通過對象的多態性讓類族共用演算法代碼需按以下步驟進行編程:
1)聲明虛函數。定義基類時需要確定將那些函數成員聲明成虛函數。一般將可能被派生類修改或擴充的函數成員聲明成虛函數。
2)重寫虛函數。定義派生類時公用繼承基類,並重寫那些從基類繼承來的虛函數成員。主要是為了修改和擴充基類功能。
3)通過基類引用和對象指針訪問對象。訪問派生類對象,調用其中的虛函數成員將自動調用重寫的虛函數,否則自動調用從基類繼承來的函數成員(即類型相容語法規則)。
3、虛析構函數
構造函數不能聲明成虛函數。析構函數可以聲明成虛函數,析構函數無形參,無函數類型。其聲明語法形式如:virtual ~類名();
示例:
A *p1 = new A; //動態分配一個基類對象
A *p2 = new B; //動態分配一個派生類對象,使用基類的對象指針保存其地址
//使用對象略
delete p1; //自動調用基類析構函數
delete p2; //自動調用派生類析構函數
可以註意到,p1 和p2都是基類的對象指針,使用delete運算符刪除對象時,將根據所指向對象的類型自動調用不同的析構函數,呈現出多態性。刪除派生類對象時,先執行派生類析構函數來析構新增成員,再執行基類析構函數來析構基類成員。如果不採用虛析構函數,那麼刪除派生類對象時將只會調用基類析構函數。
五、抽象類和純虛函數
1、純虛函數
類定義中,“只聲明,未定義”的函數成員被稱為純虛函數。純虛函數的聲明語法形式為: virtual 函數類型 函數名(形參列表)=0;
純虛函數是一種虛函數,具有虛函數額特性,其中最重要的一條就是虛函數成員再調用時具有多態性。函數有純虛函數的類就是抽象類。
抽象類具有如下特性:
1)抽象類不能實例化
不能使用抽象類定義對象(即不能實例化),因為抽象類中含有未定義的純虛函數,其類型定義不完整。但可以定義抽象類的引用或對象指針,所定義引用、對象指針可以引用其派生類的實例化對象。
2)抽象類可以作為基類定義派生類
抽象類可以作為基類定義派生類。此時的派生類也會繼承抽象類的純虛函數,由於抽象類只是聲明瞭純虛函數的函數原型,沒有定義函數體代碼,因此其派生類只是繼承了其函數原型。派生類需要為純虛函數成員編寫函數體代碼,這稱為實現純虛函數成員。派生類如果實現了所有的純虛函數成員,那麼它就變成了一個普通的類,可以實例化。只要派生類還有一個為實現的純虛函數,那麼它就還是一個抽象類,不能實例化,這是它還是只能作為基類繼續往下派生,直到實現所有純虛函數成員後才能實現化。
2、抽象類的應用
1)統一類族介面
派生類繼承基類是為了重用基類的代碼。如果基類時抽象類,純虛函數成員只聲明函數原型,這樣類族中的所有派生類都具有相同的對外介面。統一介面可以方便類族的使用。
2)類族共用演算法代碼
抽象類中定義的純虛函數具有虛函數特性,不同派生類中的虛函數實現和作用功能不同,調用時呈現多態性。類外函數可以通過抽象類(基類)引用和對象指針調用調用不同派生類對象,使得不同派生類對象公用該類外函數(演算法)。
六、多繼承、重覆繼承、虛基類(挖坑)