多態(tài)的基本概念
多態(tài)
- 多態(tài)分為編譯時(shí)多態(tài)和運(yùn)行時(shí)多態(tài)。
- 編譯時(shí)多態(tài)主要是指函數(shù)的重載(包括運(yùn)算符的重載)。對(duì)重載函數(shù)的調(diào)用,在編譯時(shí)就可以根據(jù)實(shí)參確定應(yīng)該調(diào)用哪個(gè)函數(shù),因此稱為編譯時(shí)多態(tài)。
- 運(yùn)行時(shí)多態(tài)則和繼承、虛函數(shù)等概念有關(guān)。本章中提及的多態(tài)主要是指運(yùn)行時(shí)多態(tài)。
- 程序編譯階段都早于程序運(yùn)行階段,所以靜態(tài)綁定稱為早綁定,動(dòng)態(tài)綁定稱為晚綁定。靜態(tài)多態(tài)和動(dòng)態(tài)多態(tài)的區(qū)別,只在于在什么時(shí)候?qū)⒑瘮?shù)實(shí)現(xiàn)和函數(shù)調(diào)用關(guān)聯(lián)起來(lái),是在編譯階段還是在運(yùn)行階段,即函數(shù)地址是早綁定的還是晚綁定的。
- 在類之間滿足賦值兼容的前提下,實(shí)現(xiàn)動(dòng)態(tài)綁定必須滿足以下兩個(gè)條件:
- 必須聲明虛函數(shù)
- 通過(guò)基類類型的引用或者指針調(diào)用虛函數(shù)
虛函數(shù)
所謂“虛函數(shù)”,就是在函數(shù)聲明時(shí)前面加了
virtual關(guān)鍵字的成員函數(shù)。virtual關(guān)鍵字只在類定義中的成員函數(shù)聲明處使用,不能在類外部寫(xiě)成員函數(shù)體時(shí)使用。靜態(tài)成員函數(shù)不能是虛函數(shù)。包含虛函數(shù)的類稱為“多態(tài)類”。-
聲明虛函數(shù)成員的一般格式如下:
virtual 函數(shù)返回值類型 函數(shù)名(行參表); 在類的定義中使用
virtual關(guān)鍵字來(lái)限定的成員函數(shù)即稱為虛函數(shù)。再次強(qiáng)調(diào)一下,虛函數(shù)的聲明只能出現(xiàn)在類定義中的函數(shù)原型聲明時(shí),不能在類外成員函數(shù)實(shí)現(xiàn)的時(shí)候。派生類可以繼承基類的同名函數(shù),并且可以在派生類中重寫(xiě)這個(gè)函數(shù)。如果不使用虛函數(shù),當(dāng)使用派生類對(duì)象調(diào)用這個(gè)函數(shù),且派生類中重寫(xiě)了這個(gè)函數(shù)時(shí),則調(diào)用派生類中的同名函數(shù),即“隱藏”了基類中的函數(shù)。
當(dāng)然,如果還想調(diào)用基類的函數(shù),只需在調(diào)用函數(shù)時(shí),在前面加上基類名及作用域限定符即可。
關(guān)于虛函數(shù),有以下幾點(diǎn)需要注意:
- 雖然將虛函數(shù)聲明為內(nèi)聯(lián)函數(shù)不會(huì)引起錯(cuò)誤,但因?yàn)閮?nèi)聯(lián)函數(shù)是在編譯階段進(jìn)行靜態(tài)處理的,而對(duì)虛函數(shù)的調(diào)用是動(dòng)態(tài)綁定的,所以虛函數(shù)一般不聲明為內(nèi)聯(lián)函數(shù)。
- 派生類重寫(xiě)基類的虛函數(shù)實(shí)現(xiàn)多態(tài),要求函數(shù)名。參數(shù)列表及返回值類型要完全相同。
- 基類中定義了虛函數(shù),在派生類中該函數(shù)始終保持虛函數(shù)的特性。
- 只有類的非靜態(tài)成員函數(shù)才能定義為虛函數(shù),靜態(tài)成員函數(shù)和友元函數(shù)不能定義為虛函數(shù)。
- 如果虛函數(shù)的定義是在類體外,則只需在聲明函數(shù)時(shí)添加
virtual關(guān)鍵字,定義時(shí)不加virtual關(guān)鍵字。 - 構(gòu)造函數(shù)不能定義為虛函數(shù)。最好也不要將
operator=定義為虛函數(shù),因?yàn)槭褂脮r(shí)容易混淆。 - 不要在構(gòu)造函數(shù)和析構(gòu)函數(shù)中調(diào)用虛函數(shù)。在構(gòu)造函數(shù)和析構(gòu)函數(shù)中,對(duì)象是不完整的,可能會(huì)出現(xiàn)未定義的行為。
- 最好將基類的析構(gòu)函數(shù)聲明為虛函數(shù)。
通過(guò)基類指針實(shí)現(xiàn)多態(tài)
聲明虛函數(shù)后,派生類對(duì)象的地址可以賦值給基類指針,也就是基類指針可以指向派生類對(duì)象。
對(duì)于通過(guò)基類指針調(diào)用基類和派生類中都有的同名、同參數(shù)表的虛函數(shù)的語(yǔ)句,編譯時(shí)系統(tǒng)并不確定要執(zhí)行的是基類還是派生類的虛函數(shù);
而當(dāng)程序運(yùn)行到該語(yǔ)句時(shí),
如果基類指針指向的是一個(gè)基類對(duì)象,則調(diào)用基類的虛函數(shù);
如果基類指針指向的是一個(gè)派生類對(duì)象,則調(diào)用派生類的虛函數(shù)。
#include <iostream>
using namespace std;
class A {
public:
virtual void Print() {
cout << "A::Print" << endl;
}
};
class B : public A {
public:
virtual void Print() {
cout << "B::Print" << endl;
}
};
class D : public A {
public:
virtual void Print() {
cout << "D::Print" << endl;
}
};
class E : public B {
public:
virtual void Print() {
cout << "E::Print" << endl;
}
};
int main() {
A a;
B b;
D d;
E e;
A *pa = &a;//基類pa指針指向基類對(duì)象a
B *pb = &b;//派生類pb指針指向基類對(duì)象b
pa->Print();//多態(tài),目前指向基類對(duì)象a,調(diào)用a.Print()
pa = pb;//派生類指針賦值給基類指針,pa指向派生類對(duì)象b
pa->Print();//多態(tài),目前指向派生類對(duì)象b,調(diào)用b.Print()
pa = &d;//基類指針pa指向派生類對(duì)象d
pa->Print();//多態(tài),目前指向派生類對(duì)象d,調(diào)用d.Print()
pa = &e;//基類指針pa指向派生類對(duì)象e
pa->Print();//多態(tài),目前指向派生類對(duì)象e,調(diào)用e.Print()
return 0;
};
通過(guò)基類引用實(shí)現(xiàn)多態(tài)
通過(guò)基類指針調(diào)用虛函數(shù)時(shí)可以實(shí)現(xiàn)多態(tài),通過(guò)基類的引用調(diào)用虛函數(shù)的語(yǔ)句也是多態(tài)的。
即通過(guò)基類的引用調(diào)用基類和派生類中同名、同參數(shù)表的虛函數(shù)時(shí),
若其引用的是一個(gè)基類的對(duì)象,則調(diào)用的是基類的虛函數(shù);
若其引用的是一個(gè)派生類的對(duì)象,則調(diào)用的是派生類的虛函數(shù)。
#include <iostream>
using namespace std;
class A {
public:
virtual void Print() {
cout << "A::Print" << endl;
}
};
class B : public A {
public:
virtual void Print() {
cout << "B:Print" << endl;
}
};
void PrintInfo(A &r) {
//多態(tài),使用基類引用調(diào)用哪個(gè)Print(),取決于r引用了哪個(gè)類的對(duì)象
r.Print();
}
int main() {
A a;
B b;
PrintInfo(a);//使用基類對(duì)象,調(diào)用基類中的函數(shù)
PrintInfo(b);//使用派生類對(duì)象,調(diào)用派生類中的函數(shù)
return 0;
}
多態(tài)的實(shí)現(xiàn)原理
多態(tài)的關(guān)鍵在于通過(guò)基類指針或引用調(diào)用一個(gè)虛函數(shù)時(shí),編譯階段不能確定到底調(diào)用的是基類還是派生類的函數(shù),運(yùn)行時(shí)才能確定。
派生類對(duì)象占用的存儲(chǔ)空間大小,等于基類成員變量占用的存儲(chǔ)空間大小加上派生類對(duì)象自身成員變量占用的存儲(chǔ)空間大小。
多態(tài)的使用
在普通成員函數(shù)(靜態(tài)成員函數(shù)、構(gòu)造函數(shù)和析構(gòu)函數(shù)除外)中調(diào)用其他虛成員函數(shù)也是允許的,并且是多態(tài)的。
#include <iostream>
using namespace std;
class CBase {
public:
void func1() {
cout << "CBase::func1()" << endl;
func2();//在成員函數(shù)中調(diào)用虛函數(shù)
func3();
};
virtual void func2() {
cout << "CBase::func2()" << endl;
};
void func3() {
cout << "CBase::func3()" << endl;
};
};
class CDerived : public CBase {
public:
virtual void func2() {
cout << "CDerived::func2()" << endl;
};
void func3() {
cout << "CDerived::func3()" << endl;
};
};
int main() {
CDerived d;
d.func1();
//CBase::func1()
//CDerived::func2()
//CBase::func3()
return 0;
};
不僅能在成員函數(shù)中調(diào)用虛函數(shù),還可以在構(gòu)造函數(shù)和析構(gòu)函數(shù)中調(diào)用虛函數(shù),但這樣調(diào)用的虛函數(shù)不是多態(tài)的。
#include <iostream>
using namespace std;
class A {
public:
virtual void hello() {
cout << "A::hello" << endl;
};
virtual void bye() {
cout << "A::bye" << endl;
};
};
class B : public A {
public:
virtual void hello() {
cout << "B::hello" << endl;
};
B() {
hello();//調(diào)用虛函數(shù),但不是多態(tài)
};
~B() {
bye();//調(diào)用虛函數(shù),但不是多態(tài)
};
};
class C : public B {
public:
virtual void hello() {
cout << "C::hello" << endl;
};
};
int main() {
C c;
//B::hello
//A::bye
return 0;
};
- 在構(gòu)造函數(shù)中調(diào)用的,編譯系統(tǒng)可以據(jù)此決定調(diào)用哪個(gè)類中的版本,所以它不是多態(tài)的;
- 在析構(gòu)函數(shù)中調(diào)用的,所以也不是多態(tài);
- 實(shí)現(xiàn)多態(tài)時(shí),必須滿足的條件是:使用基類指針或引用來(lái)調(diào)用基類中聲明的虛函數(shù)。
- 派生類中繼承自基類的虛函數(shù),可以寫(xiě)
virtual關(guān)鍵字,也可以省略這個(gè)關(guān)鍵字,這不影響派生類中的函數(shù)也是虛函數(shù)。
#include <iostream>
using namespace std;
class A {
public:
void func1() {
cout << "A::func1" << endl;
};
virtual void func2() {//虛函數(shù)
cout << "A::func2" << endl;
};
};
class B : public A {
public:
virtual void func1() {
cout << "B::func1" << endl;
};
void func2() {//自動(dòng)成為虛函數(shù)
cout << "B::func2" << endl;
};
};
class C : public B {
public:
void func1() {//自動(dòng)成為虛函數(shù)
cout << "C::func1" << endl;
};
void func2() {//自動(dòng)成為虛函數(shù)
cout << "C::func2" << endl;
};
};
int main() {
C c;
A *pa = &c;
B *pb = &c;
pa->func2();//多態(tài) C::func2
pa->func1();//因?yàn)榛惖膄unc1不是虛函數(shù),這也的調(diào)用也不是多態(tài) A::func1
pb->func1();//多態(tài) C::func1
return 0;
};
虛析構(gòu)函數(shù)
如果一個(gè)基類指針指向的對(duì)象是用
new運(yùn)算符動(dòng)態(tài)生成的派生類對(duì)象,那么釋放該對(duì)象所占用的空間時(shí),如果僅調(diào)用基類的析構(gòu)函數(shù),則只會(huì)完成該析構(gòu)函數(shù)內(nèi)的空間釋放,不會(huì)涉及派生類析構(gòu)函數(shù)內(nèi)的空間釋放,容易造成內(nèi)存泄露。-
聲明虛析構(gòu)函數(shù)的一般格式如下:
virtual ~類名(); 虛析構(gòu)函數(shù)沒(méi)有返回值類型,沒(méi)有參數(shù),所以它的格式非常簡(jiǎn)單。
如果一個(gè)類的虛構(gòu)函數(shù)是虛函數(shù),則由他派生的所有子類的析構(gòu)函數(shù)也是虛析構(gòu)函數(shù)。使用虛析構(gòu)函數(shù)的目的是為了在對(duì)象消亡時(shí)實(shí)現(xiàn)多態(tài)。
#include <iostream>
using namespace std;
class ABase {
public:
ABase() {
cout << "ABase構(gòu)造函數(shù)" << endl;
};
virtual ~ABase() {
cout << "ABase::析構(gòu)函數(shù)" << endl;
};
};
class Derived : public ABase {
public:
Derived() {
cout << "Derived構(gòu)造函數(shù)" << endl;
};
~Derived() {
cout << "Derived::析構(gòu)函數(shù)" << endl;
};
};
int main() {
ABase *a = new Derived();
delete a;
//ABase構(gòu)造函數(shù)
//Derived構(gòu)造函數(shù)
//Derived::析構(gòu)函數(shù)
//ABase::析構(gòu)函數(shù)
return 0;
};
- 可以看出,不僅調(diào)用了基類的析構(gòu)函數(shù),也調(diào)用了派生類的析構(gòu)函數(shù)
- 只要基類的析構(gòu)函數(shù)是虛函數(shù),那么派生類的析構(gòu)函數(shù)不論是否用
virtual關(guān)鍵字聲明,都自動(dòng)成為虛析構(gòu)函數(shù) - 一般來(lái)說(shuō),一個(gè)類如果定了虛函數(shù),則最好將析構(gòu)函數(shù)也定義成虛函數(shù)。不過(guò)切記,構(gòu)造函數(shù)不能是虛函數(shù)
純虛函數(shù)和抽象類
純虛函數(shù)
純虛函數(shù)的作用相當(dāng)于一個(gè)統(tǒng)一的接口形式,表明在基類的各派生類中應(yīng)該有這樣的一個(gè)操作,然后在各派生類中具體實(shí)現(xiàn)與本派生類相關(guān)的操作。
純虛函數(shù)是聲明在基類中的虛函數(shù),沒(méi)有具體的定義,而由個(gè)派生類根據(jù)實(shí)際需要給出各自的定義。
-
聲明純虛函數(shù)的一般格式如下:
virtual 函數(shù)類型 函數(shù)名(參數(shù)表) = 0; 純虛函數(shù)沒(méi)有函數(shù)體,參數(shù)標(biāo)后要寫(xiě)
= 0。派生類中必須重寫(xiě)這個(gè)函數(shù)。按照純虛函數(shù)名調(diào)用時(shí),執(zhí)行的是派生類中重寫(xiě)的語(yǔ)句,即調(diào)用的是派生類中的版本。
純虛函數(shù)不同于函數(shù)體為空的虛函數(shù),
它們的不同之處如下:
- 純虛函數(shù)沒(méi)有函數(shù)體,而空的虛函數(shù)的函數(shù)體為空
- 純虛函數(shù)所在的類是抽象類,不能直接進(jìn)行實(shí)例化;而空的虛函數(shù)所在的類是可以實(shí)例化的。
它們的共同特點(diǎn)是:
純虛函數(shù)與函數(shù)體為空的虛函數(shù)都可以派生出新的類,然后在新類中給出虛函數(shù)的實(shí)現(xiàn),而且這種新的實(shí)現(xiàn)具有多態(tài)特征。
抽象類
包含純虛函數(shù)的類稱為抽象類。因?yàn)槌橄箢愔杏猩形赐瓿傻暮瘮?shù)定義,所以它不能實(shí)例化一個(gè)對(duì)象。抽象類的派生類中,如果沒(méi)有給出全部純虛函數(shù)的定義,則派生類繼續(xù)是抽象類。直到派生類中給出全部純虛函數(shù)定義后,它才不再是抽象類,也才能實(shí)例化一個(gè)對(duì)象。****雖然不能創(chuàng)建抽象類的對(duì)象,但可以定義抽象類的指針和引用。這樣的指針和引用可以指向并訪問(wèn)派生類的成員,這種訪問(wèn)具有多態(tài)性。
#include <iostream>
using namespace std;
class A {
public:
virtual void Print() = 0;//純虛函數(shù)
void func1() {
cout << "A_func1" << endl;
};
};
class B : public A {
public:
void Print();
void func1() {
cout << "B_func1" << endl;
};
};
void B::Print() {
cout << "B_print" << endl;
};
int main() {
//A a; //?,抽象類不能實(shí)例化
//A *pa = new A; //?,不能創(chuàng)建抽象類類A的示例
//B b[2]; //?,不能聲明抽象類的數(shù)組
A *pa; //?,可以聲明抽象類的指針
A *pb = new B; //使用基類指針指向派生類對(duì)象
pb->Print(); //多態(tài),調(diào)用的是類B中的函數(shù),B_print
B b;
A *pb1 = &b;
pb1->func1();//不是虛函數(shù),調(diào)用的是類A中的函數(shù),A_func1
return 0;
};
虛基類
定義虛基類的一般格式如下:
class 派生類名 : virtual 派生方式 基類名 {
派生類體
};
多重繼承的模型結(jié)構(gòu)圖如下:

為了避免產(chǎn)生二義性,C++提供虛基類機(jī)制,使得在派生類中,繼承同一個(gè)間接基類的成員僅保留一個(gè)版本。
#include <iostream>
using namespace std;
class A {
public:
int a;
void showa() {
cout << "a = " << a << endl;
};
};
class B : virtual public A {//對(duì)類A進(jìn)行了虛繼承
public:
int b;
};
class C : virtual public A {//對(duì)類A進(jìn)行了虛繼承
public:
int c;
};
class D : public B, public C {
//派生類D的兩個(gè)基類B、C具有共同的基類A
//采用了虛繼承,從而使類D的對(duì)象中只包含著類A的一個(gè)示例
public:
int d;
};
int main() {
D dObj; //聲明派生類D的對(duì)象
dObj.a = 11;//若不是虛繼承,這里會(huì)報(bào)錯(cuò)!因?yàn)椤癉::a”具有二義性
dObj.b = 22;
dObj.showa();//a = 11
//若不是虛繼承,這里會(huì)報(bào)錯(cuò)!因?yàn)椤癉::showa”具有二義性
cout << "dObj.b = " << dObj.b << endl;//dObj.b = 22
return 0;
};