版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡介
第10章多態(tài)與虛函數(shù)10.1概述10.2多態(tài)10.3晚綁定機(jī)制本章小結(jié)習(xí)題
10.1概述多態(tài)性是面向?qū)ο笤O(shè)計(jì)語言中數(shù)據(jù)抽象和繼承之外的第三個(gè)特征。
C++中的多態(tài)是通過虛函數(shù)實(shí)現(xiàn)的。多態(tài)性改善了程序的組織性和可讀性,同時(shí)也使創(chuàng)建的程序具有可擴(kuò)展性。多態(tài)性是面向?qū)ο蟮暮诵模渥钪饕乃枷刖褪强梢圆捎枚喾N形式的一組方法,通過一個(gè)用戶名或者用戶接口完成不同的實(shí)現(xiàn)。通常多態(tài)性被簡單地描述為“一個(gè)接口,多個(gè)實(shí)現(xiàn)”,在C++中具體表現(xiàn)為通過基類指針訪問派生類的函數(shù)和方法。本章將討論多態(tài)的概念及其作用。首先看下面的例子:
【程序10.1】
#include<iostream>
usingnamespacestd;
classVehicle
{
public:
Vehicle(floatspeed,inttotal)
{
Vehicle::speed=speed;
Vehicle::total=total;
}
voidShowMember()
{
cout<<speed<<"|"<<total<<endl;
}
protected:
floatspeed;
inttotal;
};
classCar:publicVehicle
{
public:
Car(intaird,floatspeed,inttotal):Vehicle(speed,total)
{
Car::aird=aird;
}
voidShowMember()
{
cout<<speed<<"|"<<total<<"|"<<aird<<endl;
}
protected:
intaird;
};
voidtest(Vehicle&temp)
{
temp.ShowMember();
}
intmain()
{
Vehiclea(120,4);
Carb(180,110,4);
test(a);
test(b); cin.get(); return0;
}運(yùn)行結(jié)果如下:本例中,對象a與b分別是基類和派生類的對象,而函數(shù)test的形參卻只是Vehicle類的引用,按照類繼承的特點(diǎn),系統(tǒng)把Car類對象看做是一個(gè)Vehicle類對象,因?yàn)镃ar類的覆蓋范圍包含Vehicle類,所以test函數(shù)的定義并沒有錯(cuò)誤。該程序中想利用test函數(shù)達(dá)到的目的是,傳遞不同類對象的引用,分別調(diào)用不同類的、重載過的ShowMember成員函數(shù),但是程序的運(yùn)行結(jié)果卻出乎人的意料,系統(tǒng)分不清傳遞過來的是基類對象還是派生類對象,無論是基類對象還是派生類對象調(diào)用的都是基類的ShowMember成員函數(shù)。為了解決上述不能正確分辨對象類型的問題,C++?提供了一種叫做多態(tài)性的技術(shù)。10.2多態(tài)10.2.1多態(tài)的概念多態(tài)(polymorphism)一詞最初來源于希臘語polumorphos,含義是具有多種形式或形態(tài)的情形。在程序設(shè)計(jì)領(lǐng)域,一個(gè)被廣泛認(rèn)可的定義是“多態(tài)是一種將不同的特殊行為和單個(gè)泛化記號相關(guān)聯(lián)的能力”。通俗地講,多態(tài)性是指用一個(gè)名字定義不同的函數(shù),這些函數(shù)執(zhí)行不同但又類似的操作,即用同樣的接口訪問不同功能的函數(shù),從而實(shí)現(xiàn)“一個(gè)接口,多種方法”。和純粹的面向?qū)ο蟪绦蛟O(shè)計(jì)語言不同,C++中的多態(tài)有著更廣泛的含義。除了常見的通過類繼承和虛函數(shù)機(jī)制生效于運(yùn)行期的動態(tài)多態(tài)(dynamicpolymorphism)外,模板也允許將不同的特殊行為和單個(gè)泛化記號相關(guān)聯(lián),由于這種關(guān)聯(lián)處理于編譯期而非運(yùn)行期,因此被稱為靜態(tài)多態(tài)(staticpolymorphism)。事實(shí)上,帶變量的宏和函數(shù)重載機(jī)制也允許將不同的特殊行為和單個(gè)泛化記號相關(guān)聯(lián)。然而,習(xí)慣上我們并不將它們展現(xiàn)出來的行為稱為多態(tài)(或靜態(tài)多態(tài))。在這里,如果沒有明確所指,默認(rèn)就是動態(tài)多態(tài),其技術(shù)基礎(chǔ)在于繼承機(jī)制和虛函數(shù)。10.2.2虛函數(shù)與重寫簡單地說,那些被virtual關(guān)鍵字修飾的成員函數(shù)就是虛函數(shù)。聲明虛函數(shù)的格式如下:
class類名
{
public:
virtual<返回值類型><函數(shù)名>(<參數(shù)表>);
}
class類名:基類名
{
public:
virtual<返回值類型><函數(shù)名>(<參數(shù)表>);
}僅僅在聲明的時(shí)候需要使用關(guān)鍵字virtual,定義的時(shí)候并不需要。如果一個(gè)函數(shù)在基類中被聲明為虛函數(shù),那么在所有的派生類中它都是virtual的。在派生類中虛函數(shù)的重定義通常稱為重寫(overriding)。僅僅在聲明的時(shí)候需要使用關(guān)鍵字virtual,定義的時(shí)候并不需要。如果一個(gè)函數(shù)在基類中被聲明為虛函數(shù),那么在所有的派生類中它都是virtual的。在派生類中虛函數(shù)的重定義通常稱為重寫(overriding)。注意,僅需要在基類中聲明一個(gè)函數(shù)為虛函數(shù)。調(diào)用所有匹配基類聲明行為的派生類都將使用虛機(jī)制。雖然可以在派生類聲明中使用關(guān)鍵字virtual(這是無害的),但這樣做會使程序顯得冗余和混亂。為了從程序10.1中得到所希望的結(jié)果,只需簡單地在基類中的ShowMember()之前增加virtual關(guān)鍵字,如下所示:
【程序10.2】
#include<iostream>
usingnamespacestd;
classVehicle
{
public:
Vehicle(floatspeed,inttotal)
{
Vehicle::speed=speed;
Vehicle::total=total;
}
virtualvoidShowMember()
{//虛函數(shù)
cout<<speed<<"|"<<total<<endl;
}
protected:
floatspeed;
inttotal;
};
classCar:publicVehicle
{
public:
Car(intaird,floatspeed,inttotal):Vehicle(speed,total)
{
Car::aird=aird;
}
//派生自基類的virtual函數(shù)
voidShowMember()
{
cout<<speed<<"|"<<total<<"|"<<aird<<endl;
}
public:
intaird;
};
voidtest(Vehicle&temp)
{
temp.ShowMember();
}
intmain()
{
Vehiclea(120,4);
Carb(180,110,4);
test(a);
test(b);
cin.get();
return0;
}運(yùn)行結(jié)果如下:從上例代碼運(yùn)行的結(jié)果看,系統(tǒng)成功地分辨出了對象的真實(shí)類型,調(diào)用了各自的成員函數(shù)。虛函數(shù)的定義應(yīng)遵循以下規(guī)則:
(1)如果虛函數(shù)在基類與派生類中出現(xiàn),僅僅是名字相同,而形式參數(shù)不同,或者是返回類型不同,那么即使加上了virtual關(guān)鍵字,也是不會進(jìn)行晚綁定的。
(2)只有類的成員函數(shù)才能說明為虛函數(shù),因?yàn)樘摵瘮?shù)僅適合用于有繼承關(guān)系的類對象,所以普通函數(shù)不能說明為虛函數(shù)。
(3)靜態(tài)成員函數(shù)不能是虛函數(shù),因?yàn)殪o態(tài)成員函數(shù)的特點(diǎn)是不受某個(gè)對象的限制,而虛函數(shù)調(diào)用要靠特定的對象來決定該激活哪個(gè)函數(shù)。
(4)內(nèi)聯(lián)(inline)函數(shù)不能是虛函數(shù),因?yàn)閮?nèi)聯(lián)函數(shù)不能在運(yùn)行中動態(tài)地確定位置。即使虛函數(shù)在類的內(nèi)部定義,但是在編譯的時(shí)候系統(tǒng)仍然將它看做非內(nèi)聯(lián)的。
(5)構(gòu)造函數(shù)不能是虛函數(shù),因?yàn)闃?gòu)造的時(shí)候,對象還是一片未定型的空間,只有構(gòu)造完成后,對象才是具體類的實(shí)例。
(6)析構(gòu)函數(shù)可以是虛函數(shù),而且在基類中通常聲名為虛函數(shù)。將析構(gòu)函數(shù)定義為虛函數(shù)的目的在于:使用delete運(yùn)算符刪除一個(gè)對象時(shí),能確保析構(gòu)函數(shù)被正確地執(zhí)行。這是因?yàn)?,設(shè)置虛析構(gòu)函數(shù)后,可以利用晚綁定方式選擇析構(gòu)函數(shù)。注:關(guān)于晚綁定見10.3.1節(jié),虛構(gòu)造函數(shù)和虛析構(gòu)函數(shù)見10.2.3節(jié)。10.2.3虛析構(gòu)與虛構(gòu)造如上一節(jié)所講,析構(gòu)函數(shù)可以是虛函數(shù),而且在基類中通常聲明為虛函數(shù)。這是為了能夠正確調(diào)用對象的析構(gòu)函數(shù),一般要求具有層次結(jié)構(gòu)的基類定義其析構(gòu)函數(shù)為虛函數(shù),因?yàn)楫?dāng)用戶刪除一個(gè)抽象類指針時(shí),必須通過虛函數(shù)找到真正的析構(gòu)函數(shù)。
C++語言標(biāo)準(zhǔn)關(guān)于這個(gè)問題的闡述非常清楚:當(dāng)通過基類的指針去刪除派生類的對象,而基類又沒有虛析構(gòu)函數(shù)時(shí),結(jié)果將是不可確定的。實(shí)際運(yùn)行時(shí)經(jīng)常發(fā)生的情況是,派生類的析構(gòu)函數(shù)永遠(yuǎn)不會被調(diào)用,可能造成內(nèi)存泄漏。
【程序10.3】
#include<iostream>
usingnamespacestd;
classVehicle
{
public:
Vehicle(floatspeed,inttotal)
{
Vehicle::speed=speed;
Vehicle::total=total;
}
virtualvoidShowMember()
{
cout<<speed<<"|"<<total<<endl;
}
virtual~Vehicle()
{
cout<<"載入Vehicle基類析構(gòu)函數(shù)"<<endl;
}
protected:
floatspeed;
inttotal;
};
classCar:publicVehicle
{
public:
Car(intaird,floatspeed,inttotal):Vehicle(speed,total) {
Car::aird=aird;
}
virtualvoidShowMember()
{
cout<<speed<<"|"<<total<<"|"<<aird<<endl;
}
protected:
intaird;
};
voidtest(Vehicle&temp)
{
temp.ShowMember();
}
voidDelPN(Vehicle*temp)
{
deletetemp;
}
intmain()
{
Car*a=newCar(100,1,1);
a->ShowMember();
DelPN(a);
cin.get();
return0;
}運(yùn)行結(jié)果如下:從上例代碼的運(yùn)行結(jié)果來看,當(dāng)調(diào)用DelPN(a)后,在析構(gòu)的時(shí)候,系統(tǒng)成功地確定了先調(diào)用Car類的析構(gòu)函數(shù)。但如果將析構(gòu)函數(shù)的virtual修飾去掉,再次運(yùn)行代碼則會得到不同的結(jié)果,如下所示:
【程序10.4】
#include<iostream>
usingnamespacestd;
classVehicle
{
public:
Vehicle(floatspeed,inttotal)
{
Vehicle::speed=speed;
Vehicle::total=total;
}
virtualvoidShowMember()
{
cout<<speed<<"|"<<total<<endl;
}
~Vehicle()
{
cout<<"載入Vehicle基類析構(gòu)函數(shù)"<<endl;
}
protected:
floatspeed;
inttotal;
};
classCar:publicVehicle
{
public:
Car(intaird,floatspeed,inttotal):Vehicle(speed,total)
{
Car::aird=aird;
}
virtualvoidShowMember()
{
cout<<speed<<"|"<<total<<"|"<<aird<<endl;
}
~Car()
{
cout<<"載入Car派生類析構(gòu)函數(shù)"<<endl;
}
protected:
intaird;
};
voidtest(Vehicle&temp)
{
temp.ShowMember();
}
voidDelPN(Vehicle*temp)
{
deletetemp;
}
intmain()
{
Car*a=newCar(100,1,1);
a->ShowMember();
DelPN(a); cin.get(); return0;
}運(yùn)行結(jié)果如下:觀察結(jié)果會發(fā)現(xiàn),析構(gòu)的時(shí)候,始終只調(diào)用了基類的析構(gòu)函數(shù)。由此我們發(fā)現(xiàn),多態(tài)特性的virtual修飾,不單單對基類和派生類的普通成員函數(shù)非常必要,而且對于基類和派生類的析構(gòu)函數(shù)同樣重要。如上一節(jié)所講,構(gòu)造函數(shù)不能是虛函數(shù)。因?yàn)橐獦?gòu)造一個(gè)對象,構(gòu)造函數(shù)必須掌握所創(chuàng)建的對象的確切類型。進(jìn)一步講,構(gòu)造函數(shù)并不是一個(gè)普通的函數(shù),特別是它需要以常規(guī)成員函數(shù)所沒有的方式與存儲管理例程打交道。因此,用戶不能得到一個(gè)構(gòu)造函數(shù)的指針。但是,創(chuàng)建一個(gè)對象而又不必知道其確切類型,常常也是很有意義的。為了達(dá)到這個(gè)目的,可以定義一個(gè)函數(shù),由它調(diào)用構(gòu)造函數(shù)并返回構(gòu)造起來的對象,或者通過克隆已有對象或創(chuàng)建有關(guān)類型的新對象,將一個(gè)類的對象提供給用戶。例如:
classExpr
{
public:
Expr(); //默認(rèn)構(gòu)造函數(shù)
Expr(constExpr&); //復(fù)制構(gòu)造函數(shù)
virtualExpr*new_expr(){returnnewExpr();}
virtualExpr*clone(){returnnewExpr(*this);}
//...
};在這里,因?yàn)橄駈ew_expr()和clone()這樣的函數(shù)是虛函數(shù),且它們(間接地)創(chuàng)建對象,所以也常常被稱為“虛構(gòu)造函數(shù)”,這和前面所講的“構(gòu)造函數(shù)不能是虛函數(shù)”的概念并不矛盾。它們利用某個(gè)構(gòu)造函數(shù)創(chuàng)建一個(gè)合適的對象。派生類可以重寫new_expr()和(或)clone()去返回它們自己類型的對象,例如:
classCond:publicExpr
{
public:
Cond();
Cond(constCond&);
Cond*new_expr(){returnnewCond();}
Cond*clone(){returnnewCond(*this);}
//...
};這也就意味著,給了一個(gè)Expr類的對象,用戶就可以創(chuàng)建一個(gè)“恰好與它類型一樣”的新對象,例如:
voiduser(Expr*p)
{
Expr*p2=p->new_expr();
//...
}賦值給p2的是一個(gè)合適的指針,但它的類型卻是未知的。
Cond::new_expr()和Cond::clone()的返回類型為Cond*,而不是Expr*。這就使Cond可以克隆而不會丟失類型信息。例如:
voiduser2(Cond*pc,Expr*pe)
{
Cond*p2=pc->clone();
Cond*p3=pe->clone(); //錯(cuò)誤
//...
}重寫函數(shù)的返回類型必須和它要去重寫的那個(gè)虛函數(shù)的返回類型一樣,只有返回值的類型可以被放寬限制。在這里,如果基類函數(shù)的返回值類型是B*,B是D的一個(gè)公用基類,那么派生類的重寫函數(shù)的返回值類型可以是D*。與此類似,返回類型B&也可以放寬為D&。
10.2.4純虛函數(shù)和抽象基類
純虛函數(shù)是一種特殊的虛函數(shù),它的一般格式如下:
class<類名>
{
virtual<返回值類型><函數(shù)名>(<參數(shù)表>)=0;
…
};在許多情況下,常常希望基類僅僅作為其派生類的一個(gè)接口。也就是說,僅想對基類進(jìn)行向上類型轉(zhuǎn)換,使用它的接口,而不希望用戶實(shí)際創(chuàng)建一個(gè)基類的對象。要做到這點(diǎn),可以在基類中加入至少一個(gè)純虛函數(shù),使基類成為抽象類。抽象類是一種特殊的類,它是為了抽象和設(shè)計(jì)的目的而建立的,它處于繼承層次結(jié)構(gòu)的較上層。抽象類是不能定義對象的,在實(shí)際中為了強(qiáng)調(diào)一個(gè)類是抽象類,可將該類的構(gòu)造函數(shù)說明為受保護(hù)的訪問控制權(quán)限。抽象類的主要作用是將有關(guān)屬性、方法、事件等組織在一個(gè)繼承層次結(jié)構(gòu)中,由它來為它們提供一個(gè)公共的根,相關(guān)的派生類是從這個(gè)根派生出來的。抽象類刻畫了一組派生類操作接口的通用語義,這些語義也傳給派生類。一般而言,抽象類只描述這組派生類共同的操作接口,而完整的實(shí)現(xiàn)留給派生類。抽象類只能作為基類來使用,其純虛函數(shù)的實(shí)現(xiàn)由派生類給出。如果派生類沒有重新定義純虛函數(shù),而派生類只是繼承基類的純虛函數(shù),則這個(gè)派生類仍然是一個(gè)抽象類。如果派生類中給出了基類純虛函數(shù)的實(shí)現(xiàn),則它是一個(gè)可以建立對象的具體類。下面給出一個(gè)純虛函數(shù)和抽象基類的例子。
【程序10.5】
#include<iostream>
usingnamespacestd;
classpoint
{
public:
point(inti=0,intj=0){x0=i;y0=j;}
virtualvoidset()=0;
virtualvoiddraw()=0;
protected: intx0,y0;
};
classline:publicpoint
{
public: line(inti=0,intj=0,intm=0,intn=0):point(i,j) { x1=m;y1=n;
}
voidset(){cout<<"line::set()called.\n";}
voiddraw(){cout<<"line::draw()called.\n";}
protected: intx1,y1;
};
classellipse:publicpoint
{
public:
ellipse(inti=0,intj=0,intp=0,intq=0):point(i,j)
{ x2=p;y2=q;
}
voidset(){cout<<"ellipse::set()called.\n";} voiddraw(){cout<<"ellipse::draw()called.\n";}
protected: intx2,y2;
};
voiddrawobj(point*p)
{
p->draw();
}
voidsetobj(point*p)
{ p->set();
}
intmain()
{
line*lineobj=newline;
ellipse*elliobj=newellipse;
drawobj(lineobj);
drawobj(elliobj);
cout<<endl;
setobj(lineobj);
setobj(elliobj);
cout<<"\nRedrawtheobject...\n";
drawobj(lineobj); drawobj(elliobj); return0;
}運(yùn)行結(jié)果如下:在使用純虛函數(shù)與抽象類時(shí),C++有如下規(guī)定:
(1)抽象類只能作為其他類的基類,不能聲明抽象類的對象,但可以聲明指向抽象類的指針變量和引用變量。也就是說,抽象類只能用作基類來派生新類,而不能用來創(chuàng)建對象。
2)抽象類中可以有多個(gè)純虛函數(shù),也可以定義其他非純虛函數(shù)。
(3)如果在派生類中沒有重新定義基類中的純虛函數(shù),則必須再將該虛函數(shù)聲明為純虛函數(shù),這時(shí),這個(gè)派生類仍然是一個(gè)抽象類;如果在派生類中給出了所有純虛函數(shù)的具體實(shí)現(xiàn),則該派生類就不再是抽象類。
(4)從抽象類可以派生出具體類或抽象類,但不能從具體類派生出抽象類。
(5)在一個(gè)復(fù)雜的類繼承結(jié)構(gòu)中,越上層的類抽象程度越高,有時(shí)甚至無法給出某些成員函數(shù)的具體實(shí)現(xiàn)。顯然,抽象類是一種特殊的類,它一般處于類繼承結(jié)構(gòu)的頂層。
(6)在基類中,對純虛函數(shù)提供定義是可能的。由于一般不允許產(chǎn)生抽象基類的對象,而且如果要?jiǎng)?chuàng)建對象,則純虛函數(shù)必須在派生類中定義。然而,我們可能希望一段公共代碼,使一些或所有派生類定義都能調(diào)用,而不必在每個(gè)函數(shù)中重復(fù)這段代碼。這個(gè)特點(diǎn)的另一個(gè)好處是,它允許我們從虛函數(shù)到純虛函數(shù)的改變,而無需打亂已存在的代碼。(這是一個(gè)處理不用重新定義虛函數(shù)的類的方法。)下面對純虛函數(shù)、虛函數(shù)和非虛函數(shù)作一比較。純虛函數(shù)最顯著的特征是:它們必須在繼承了它們的任何具體類中重新聲明,而且它們在抽象類中往往沒有定義。把這兩個(gè)特征放在一起,就會認(rèn)識到:
(1)定義純虛函數(shù)的目的在于,使派生類僅僅繼承函數(shù)的接口。虛函數(shù)的情況和純虛函數(shù)不同。派生類繼承了函數(shù)的接口,但虛函數(shù)一般還提供了實(shí)現(xiàn),派生類可以選擇改寫它們或不改寫它們。
(2)聲明虛函數(shù)的目的在于,使派生類繼承函數(shù)的接口和缺省實(shí)現(xiàn)。當(dāng)一個(gè)成員函數(shù)為非虛函數(shù)時(shí),它在派生類中的行為就應(yīng)該一致。實(shí)際上,非虛成員函數(shù)表明了一種“特殊性上的不變性”,因?yàn)樗硎镜氖遣粫淖兊男袨椤还芤粋€(gè)派生類有多么特殊。
(3)聲明非虛函數(shù)的目的在于,使派生類繼承函數(shù)的接口和強(qiáng)制性得以實(shí)現(xiàn)。10.3晚綁定機(jī)制10.3.1函數(shù)調(diào)用綁定把函數(shù)體與函數(shù)調(diào)用相聯(lián)系稱為綁定(binding),在面向?qū)ο蟪绦蛟O(shè)計(jì)中經(jīng)常會用到兩個(gè)術(shù)語:早綁定(earlybinding)和晚綁定(latebinding)。對于C++而言,它們分別是指發(fā)生在編譯時(shí)的動作和發(fā)生在運(yùn)行時(shí)的動作。早綁定就是綁定在程序運(yùn)行之前(由編譯器和連接器)完成;晚綁定則是綁定在程序運(yùn)行時(shí)完成。晚綁定又稱動態(tài)綁定(dynamicbinding)或運(yùn)行時(shí)綁定(runtimebinding)。在程序10.1中出現(xiàn)的問題就是由于早綁定引起的,因?yàn)榫幾g器在只有Vehicle地址時(shí)并不知道要調(diào)用的正確函數(shù)。解決問題的方法就是使用晚綁定,在C++中,晚綁定是使用虛函數(shù)和派生類來實(shí)現(xiàn)的(如前文中程序10.2所示)。當(dāng)使用晚綁定時(shí),在程序執(zhí)行時(shí),哪個(gè)函數(shù)將被調(diào)用是懸而未決的。只有當(dāng)調(diào)用到那個(gè)函數(shù)時(shí),才會發(fā)生綁定行為。實(shí)際上,函數(shù)調(diào)用綁定正是C++中晚綁定的實(shí)現(xiàn)方法。10.3.2虛表和虛指針晚綁定究竟是如何發(fā)生的呢?當(dāng)聲明類中的某個(gè)函數(shù)為虛函數(shù)以后,編譯器在編譯的時(shí)候,并不將此函數(shù)綁定,而是為這個(gè)類生成一個(gè)虛函數(shù)表(virtualtable,vtbl)。在vtbl中,編譯器放置特定類的虛函數(shù)的地址。在每個(gè)帶有虛函數(shù)的類中,編譯器秘密地放置一個(gè)指針,稱為虛指針(virtualpointer,vptr),此指針指向這個(gè)對象的vtbl。當(dāng)通過基類指針做虛函數(shù)調(diào)用(也就是多態(tài)調(diào)用)時(shí),編譯器靜態(tài)地插入能取得這個(gè)vptr并在vtbl中查找函數(shù)地址的代碼,這樣就能調(diào)用正確的函數(shù)并引起晚綁定的發(fā)生。為每個(gè)帶虛函數(shù)的類設(shè)置vtbl、初始化vptr以及為虛函數(shù)調(diào)用插入代碼,這些全部是由編譯器自動完成的。利用虛函數(shù),即使在編譯器還不知道這個(gè)對象的特定類型的情況下,也能調(diào)用正確的函數(shù)。下面這段代碼就是為了驗(yàn)證虛指針的存在。
【程序10.6】
#include<iostream>
usingnamespacestd;
classNoVirtual
{ inta;
public: voidx()const{} inty()const{return1;}
};
classOneVirtual
{ inta;
public: virtualvoidx()const{} inty()const{return1;}
};
classTwoVirtual
{ inta;
public: virtualvoidx()const{}
virtualinty()const{return1;}
};
intmain()
{ cout<<"int:"<<sizeof(int)<<endl; cout<<"void*:"<<sizeof(void*)<<endl; cout<<"NoVirtual:"<<sizeof(NoVirtual)<<endl; cout<<"OneVirtual:"<<sizeof(OneVirtual)<<endl; cout<<"TwoVirtual:"<<sizeof(TwoVirtual)<<endl; return0;
}運(yùn)行結(jié)果如下:分析運(yùn)行結(jié)果,不帶虛函數(shù)的類,其對象的長度是單個(gè)int的長度。而帶有一個(gè)虛函數(shù)的類,其長度是一個(gè)int加上一個(gè)void*?指針的長度。帶有兩個(gè)虛函數(shù)的類,其長度與帶有一個(gè)虛函數(shù)的類相同。這反映出:類中如果有一個(gè)或多個(gè)虛函數(shù),編譯器都只在這個(gè)結(jié)構(gòu)中插入一個(gè)指針,即vptr。這是因?yàn)関ptr指向一個(gè)存放函數(shù)地址的表(vtbl),類中所有的虛函數(shù)地址都存在這個(gè)表中,所以只需要一個(gè)表,因此也只有一個(gè)指針。每個(gè)含有虛函數(shù)的類都有一張?zhí)摵瘮?shù)表(vtbl),表中每一項(xiàng)是指向一個(gè)虛函數(shù)的地址的指針,實(shí)際上是一個(gè)函數(shù)指針的數(shù)組。虛函數(shù)表既有繼承性又有多態(tài)性。每個(gè)派生類的vtbl繼承了它各個(gè)基類的vtbl,如果基類vtbl中包含某一項(xiàng),則其派生類的vtbl中也將包含同樣的一項(xiàng),但是兩項(xiàng)的值可能不同。如果派生類重寫了該項(xiàng)對應(yīng)的虛函數(shù),則派生類vtbl的該項(xiàng)將指向重寫后的虛函數(shù);否則,將沿用基類的值。在類對象的內(nèi)存布局中,首先是該類的vtbl指針(即vptr),然后才是對象數(shù)據(jù)。在通過對象指針調(diào)用一個(gè)虛函數(shù)時(shí),編譯器生成的代碼將先獲取對象類的vtbl指針,然后調(diào)用vtbl中對應(yīng)的項(xiàng)。對于通過對象指針調(diào)用的情況,在編譯期間無法確定指針指向的是基類對象還是派生類對象,或者是哪個(gè)派生類的對象。但是在運(yùn)行期間執(zhí)行到調(diào)用語句時(shí),這一點(diǎn)已經(jīng)確定,編譯后的調(diào)用代碼能夠根據(jù)具體對象獲取正確的vtbl,調(diào)用正確的虛函數(shù),從而實(shí)現(xiàn)多態(tài)性。問題的實(shí)質(zhì)是,對于發(fā)出虛函數(shù)調(diào)用的這個(gè)對象指針,在編譯期間缺乏更多的信息,而在運(yùn)行期間具備足夠的信息,但那時(shí)已不再進(jìn)行綁定了,怎么在二者之間作一個(gè)過渡呢?把綁定所需的信息用一種通用的數(shù)據(jù)結(jié)構(gòu)記錄下來,該數(shù)據(jù)結(jié)構(gòu)可以同對象指針相聯(lián)系,在編譯時(shí)只需要使用這個(gè)數(shù)據(jù)結(jié)構(gòu)進(jìn)行抽象的綁定,而在運(yùn)行期間將會得到真正的綁定。這個(gè)數(shù)據(jù)結(jié)構(gòu)就是vtbl??梢钥吹?,實(shí)現(xiàn)用戶所需的抽象和多態(tài)需要進(jìn)行晚綁定,而編譯器又是通過抽象和多態(tài)來實(shí)現(xiàn)晚綁定的。下面通過一個(gè)例子來說明虛函數(shù)表在內(nèi)存中位置。
【程序10.7】
#include<iostream>
usingnamespacestd;
classBase
{
public: virtualvoidf(){cout<<"Base::f"<<endl;} virtualvoidg(){cout<<"Base::g"<<endl;} virtualvoidh(){cout<<"Base::h"<<endl;}
};
typedefvoid(*Fun)(void);
Baseb;
FunpFun=NULL;
intmain(void)
{ cout<<"虛函數(shù)表地址:"<<(int*)(&b)<<endl; cout<<“虛函數(shù)表-第一個(gè)函數(shù)地址:”<<
(int*)*(int*)(&b)<<endl; //調(diào)用第一個(gè)虛函數(shù)
pFun=(Fun)*((int*)*(int*)(&b)); pFun(); return0;
}運(yùn)行結(jié)果如下:在這個(gè)示例中,通過強(qiáng)行把&b轉(zhuǎn)成int*,取得虛函數(shù)表的地址,然后,再次取址就可以得到第一個(gè)虛函數(shù)的地址了,也就是Base::f(),這在上面的程序中得到了驗(yàn)證(把int*?強(qiáng)制轉(zhuǎn)成了函數(shù)指針)。獲取Base::g()地址的代碼如下:
【程序10.8】
#include<iostream>
usingnamespacestd;
classBase
{
public: virtualvoidf(){cout<<"Base::f"<<endl;} virtualvoidg(){cout<<"Base::g"<<endl;} virtualvoidh(){cout<<"Base::h"<<endl;}
};
typedefvoid(*Fun)(void);
Baseb;
FunpFun=NULL;
intmain(void)
{ cout<<"虛函數(shù)表地址:"<<(int*)(&b)<<endl; cout<<"虛函數(shù)表-第二個(gè)函數(shù)地址:"<<(int*)*(int*)(&b)+1<<endl; //調(diào)用第二個(gè)虛函數(shù)
pFun=(Fun)*((int*)*(int*)(&b)+1); pFun(); return0;
}
運(yùn)行結(jié)果如下:同理,獲取Base::h()的地址代碼如下:
【程序10.9】
#include<iostream>
usingnamespacestd;
classBase
{
public: virtualvoidf(){cout<<"Base::f"<<endl;} virtualvoidg(){cout<<"Base::g"<<endl;} virtualvoidh(){cout<<"Base::h"<<endl;}
};
typedefvoid(*Fun)(void);
Baseb;
FunpFun=NULL;
intmain(void)
{ cout<<"虛函數(shù)表地址:"<<(int*)(&b)<<endl; cout<<"虛函數(shù)表-第三個(gè)函數(shù)地址:"<<(int*)*(int*)(&b)+2<<endl; //調(diào)用第三個(gè)虛函數(shù)
pFun=(Fun)*((int*)*(int*)(&b)+2); pFun(); return0;
}運(yùn)行結(jié)果如下:在vtbl中,第一個(gè)虛函數(shù)地址指針位置是0046C01C,第二個(gè)虛函數(shù)地址指針位置是0046C020,第三個(gè)虛函數(shù)地址指針位置是0046C024,驗(yàn)證了前文中虛函數(shù)表是一個(gè)函數(shù)指針數(shù)組的結(jié)論。本章小結(jié)多態(tài)在C++中是通過虛函數(shù)實(shí)現(xiàn)的,它意味著具有“不同的形式”。多態(tài)是不能獨(dú)立工作的特征,必須協(xié)同工作,它是類關(guān)系大家庭中的一部分。如果沒有晚綁定就沒有多態(tài)。為了在程序中有效地使用多態(tài)等面向?qū)ο蟮募夹g(shù),不僅要
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 2025年度中國東航大學(xué)生招飛筆試參考題庫附帶答案詳解(3卷)
- 2025北京京能建設(shè)集團(tuán)有限公司公開招聘所屬公司副總經(jīng)理2人筆試參考題庫附帶答案詳解(3卷)
- 榆林市2024陜西榆林靖邊縣招聘駐礦安監(jiān)員(9人)筆試歷年參考題庫典型考點(diǎn)附帶答案詳解(3卷合一)
- 建鄴區(qū)2024江蘇南京市建鄴區(qū)學(xué)前教育事業(yè)單位招聘編外工作人員2人(一)筆試歷年參考題庫典型考點(diǎn)附帶答案詳解(3卷合一)
- 2026年吉林單招輕工紡織類職業(yè)適應(yīng)性測試模擬卷含答案
- 2026年深圳單招在職考生碎片化備考題庫含答案分章節(jié)便攜刷題
- 2026年云南單招退役士兵免考配套技能測試題含答案政策適配版
- 2026年青島單招工業(yè)機(jī)器人專業(yè)中職生技能經(jīng)典題含編程基礎(chǔ)
- 2026年內(nèi)蒙古單招考前提分卷含答案文化和技能綜合預(yù)測
- 2026年廣東單招護(hù)理專業(yè)技能操作規(guī)范經(jīng)典題詳解
- 北京市東城區(qū)2024-2025學(xué)年五年級上冊期末測試數(shù)學(xué)試卷(含答案)
- 眼科手術(shù)患者的心理護(hù)理與情緒管理
- 項(xiàng)目分包制合同范本
- 2025天津大學(xué)管理崗位集中招聘15人考試筆試備考題庫及答案解析
- 企業(yè)數(shù)據(jù)安全管理制度
- 2025年公務(wù)員多省聯(lián)考《申論》題(陜西A卷)及參考答案
- 摘菜勞動課件
- 2025義齒行業(yè)市場分析報(bào)告
- DB34∕T 4796-2024 藥品臨床綜合評價(jià)質(zhì)量控制規(guī)范
- 2025年公共管理與公共政策專業(yè)考試試卷及答案
- 學(xué)堂在線 雨課堂 學(xué)堂云 批判性思維-方法和實(shí)踐 章節(jié)測試答案
評論
0/150
提交評論