《C++程序設(shè)計(jì)》課件第10章_第1頁
《C++程序設(shè)計(jì)》課件第10章_第2頁
《C++程序設(shè)計(jì)》課件第10章_第3頁
《C++程序設(shè)計(jì)》課件第10章_第4頁
《C++程序設(shè)計(jì)》課件第10章_第5頁
已閱讀5頁,還剩69頁未讀, 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(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)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

最新文檔

評論

0/150

提交評論