多態(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)起來抖部,是在編譯階段還是在運(yùn)行階段说贝,即函數(shù)地址是早綁定的還是晚綁定的。
- 在類之間滿足賦值兼容的前提下慎颗,實(shí)現(xiàn)動(dòng)態(tài)綁定必須滿足以下兩個(gè)條件:
- 必須聲明虛函數(shù)
- 通過基類類型的引用或者指針調(diào)用虛函數(shù)
虛函數(shù)
所謂“虛函數(shù)”乡恕,就是在函數(shù)聲明時(shí)前面加了
virtual
關(guān)鍵字的成員函數(shù)。virtual
關(guān)鍵字只在類定義中的成員函數(shù)聲明處使用俯萎,不能在類外部寫成員函數(shù)體時(shí)使用傲宜。靜態(tài)成員函數(shù)不能是虛函數(shù)。包含虛函數(shù)的類稱為“多態(tài)類”夫啊。-
聲明虛函數(shù)成員的一般格式如下:
virtual 函數(shù)返回值類型 函數(shù)名(行參表);
在類的定義中使用
virtual
關(guān)鍵字來限定的成員函數(shù)即稱為虛函數(shù)函卒。再次強(qiáng)調(diào)一下,虛函數(shù)的聲明只能出現(xiàn)在類定義中的函數(shù)原型聲明時(shí)撇眯,不能在類外成員函數(shù)實(shí)現(xiàn)的時(shí)候谆趾。派生類可以繼承基類的同名函數(shù),并且可以在派生類中重寫這個(gè)函數(shù)叛本。如果不使用虛函數(shù),當(dāng)使用派生類對(duì)象調(diào)用這個(gè)函數(shù)彤钟,且派生類中重寫了這個(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ù)。
- 派生類重寫基類的虛函數(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ù)。
通過基類指針實(shí)現(xiàn)多態(tài)
聲明虛函數(shù)后溶褪,派生類對(duì)象的地址可以賦值給基類指針币旧,也就是基類指針可以指向派生類對(duì)象。
對(duì)于通過基類指針調(diào)用基類和派生類中都有的同名猿妈、同參數(shù)表的虛函數(shù)的語句吹菱,編譯時(shí)系統(tǒng)并不確定要執(zhí)行的是基類還是派生類的虛函數(shù);
而當(dāng)程序運(yùn)行到該語句時(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;
};
通過基類引用實(shí)現(xiàn)多態(tài)
通過基類指針調(diào)用虛函數(shù)時(shí)可以實(shí)現(xiàn)多態(tài)旧蛾,通過基類的引用調(diào)用虛函數(shù)的語句也是多態(tài)的。
即通過基類的引用調(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)鍵在于通過基類指針或引用調(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í)查辩,必須滿足的條件是:使用基類指針或引用來調(diào)用基類中聲明的虛函數(shù)胖笛。
- 派生類中繼承自基類的虛函數(shù),可以寫
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ù)沒有返回值類型,沒有參數(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ù) - 一般來說舶治,一個(gè)類如果定了虛函數(shù)分井,則最好將析構(gòu)函數(shù)也定義成虛函數(shù)。不過切記霉猛,構(gòu)造函數(shù)不能是虛函數(shù)
純虛函數(shù)和抽象類
純虛函數(shù)
純虛函數(shù)的作用相當(dāng)于一個(gè)統(tǒng)一的接口形式尺锚,表明在基類的各派生類中應(yīng)該有這樣的一個(gè)操作,然后在各派生類中具體實(shí)現(xiàn)與本派生類相關(guān)的操作韩脏。
純虛函數(shù)是聲明在基類中的虛函數(shù)缩麸,沒有具體的定義,而由個(gè)派生類根據(jù)實(shí)際需要給出各自的定義赡矢。
-
聲明純虛函數(shù)的一般格式如下:
virtual 函數(shù)類型 函數(shù)名(參數(shù)表) = 0;
純虛函數(shù)沒有函數(shù)體杭朱,參數(shù)標(biāo)后要寫
= 0
。派生類中必須重寫這個(gè)函數(shù)吹散。按照純虛函數(shù)名調(diào)用時(shí)弧械,執(zhí)行的是派生類中重寫的語句,即調(diào)用的是派生類中的版本空民。
純虛函數(shù)不同于函數(shù)體為空的虛函數(shù)刃唐,
它們的不同之處如下:
- 純虛函數(shù)沒有函數(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ì)象薇宠。抽象類的派生類中,如果沒有給出全部純虛函數(shù)的定義艰额,則派生類繼續(xù)是抽象類澄港。直到派生類中給出全部純虛函數(shù)定義后,它才不再是抽象類柄沮,也才能實(shí)例化一個(gè)對(duì)象回梧。****雖然不能創(chuàng)建抽象類的對(duì)象,但可以定義抽象類的指針和引用铡溪。這樣的指針和引用可以指向并訪問派生類的成員漂辐,這種訪問具有多態(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;
};