c++多態技術和虛函數表_第1頁
c++多態技術和虛函數表_第2頁
c++多態技術和虛函數表_第3頁
c++多態技術和虛函數表_第4頁
c++多態技術和虛函數表_第5頁
已閱讀5頁,還剩9頁未讀 繼續免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

C++多態技術導言

多態(polymorphism)一詞最初來源于希臘語polumorphos,含義是具有多種形式或形態的情形。在程序設計領域,一個廣泛認可的定義是“一種將不同的特殊行為和單個泛化記號相關聯的能力”。和純粹的面向對象程序設計語言不同,C++中的多態有著更廣泛的含義。除了常見的通過類繼承和虛函數機制生效于運行期的動態多態(dynamicpolymorphism)外,模板也允許將不同的特殊行為和單個泛化記號相關聯,由于這種關聯處理于編譯期而非運行期,因此被稱為靜態多態(staticpolymorphism)。

事實上,帶變量的宏和函數重載機制也允許將不同的特殊行為和單個泛化記號相關聯。然而,習慣上我們并不將它們展現出來的行為稱為多態(或靜態多態)。今天,當我們談及多態時,如果沒有明確所指,默認就是動態多態,而靜態多態則是指基于模板的多態。不過,在這篇以C++各種多態技術為主題的文章中,我們首先還是回顧一下C++社群爭論已久的另一種“多態”:函數多態(functionpolymorphism),以及更不常提的“宏多態(macropolymorphism)”。

函數多態

也就是我們常說的函數重載(functionoverloading)。基于不同的參數列表,同一個函數名字可以指向不同的函數定義:

//overload_poly.cpp

#include<iostream>

#include<string>

//定義兩個重載函數

intmy_add(inta,intb)

{

returna+b;

}

intmy_add(inta,std::stringb)

{

returna+atoi(b.c_str());

}

intmain()

{

inti=my_add(1,2);

//兩個整數相加

ints=my_add(1,"2");

//一個整數和一個字符串相加

std::cout<<"i="<<i<<"\n";

std::cout<<"s="<<s<<"\n";

}

根據參數列表的不同(類型、個數或兼而有之),my_add(1,2)和my_add(1,"2")被分別編譯為對my_add(int,int)和my_add(int,std::string)的調用。實現原理在于編譯器根據不同的參數列表對同名函數進行名字重整,而后這些同名函數就變成了彼此不同的函數。比方說,也許某個編譯器會將my_add()函數名字分別重整為my_add_int_int()和my_add_int_str()。

宏多態

帶變量的宏可以實現一種初級形式的靜態多態:

//macro_poly.cpp

#include<iostream>

#include<string>

//定義泛化記號:宏ADD

#defineADD(A,B)(A)+(B);

intmain()

{

inti1(1),i2(2);

std::strings1("Hello,"),s2("world!");

inti=ADD(i1,i2);

//兩個整數相加

std::strings=ADD(s1,s2);

//兩個字符串“相加”

std::cout<<"i="<<i<<"\n";

std::cout<<"s="<<s<<"\n";

}

當程序被編譯時,表達式ADD(i1,i2)和ADD(s1,s2)分別被替換為兩個整數相加和兩個字符串相加的具體表達式。整數相加體現為求和,而字符串相加則體現為連接。程序的輸出結果符合直覺:

1+2=3

Hello,+world!=Hello,world!

動態多態

這就是眾所周知的的多態。現代面向對象語言對這個概念的定義是一致的。其技術基礎在于繼承機制和虛函數。例如,我們可以定義一個抽象基類Vehicle和兩個派生于Vehicle的具體類Car和Airplane:

//dynamic_poly.h

#include<iostream>

//公共抽象基類Vehicle

classVehicle

{

public:

virtualvoidrun()const=0;

};

//派生于Vehicle的具體類Car

classCar:publicVehicle

{

public:

virtualvoidrun()const

{

std::cout<<"runacar\n";

}

};

//派生于Vehicle的具體類Airplane

classAirplane:publicVehicle

{

public:

virtualvoidrun()const

{

std::cout<<"runaairplane\n";

}

};

客戶程序可以通過指向基類Vehicle的指針(或引用)來操縱具體對象。通過指向基類對象的指針(或引用)來調用一個虛函數,會導致對被指向的具體對象之相應成員的調用:

//dynamic_poly_1.cpp

#include<iostream>

#include<vector>

#include"dynamic_poly.h"

//通過指針run任何vehicle

voidrun_vehicle(constVehicle*vehicle)

{

vehicle->run();

//根據vehicle的具體類型調用對應的run()

}

intmain()

{

Carcar;

Airplaneairplane;

run_vehicle(&car);

//調用Car::run()

run_vehicle(&airplane);

//調用Airplane::run()

}

此例中,關鍵的多態接口元素為虛函數run()。由于run_vehicle()的參數為指向基類Vehicle的指針,因而無法在編譯期決定使用哪一個版本的run()。在運行期,為了分派函數調用,虛函數被調用的那個對象的完整動態類型將被訪問。這樣一來,對一個Car對象調用run_vehicle(),實際上將調用Car::run(),而對于Airplane對象而言將調用Airplane::run()。

或許動態多態最吸引人之處在于處理異質對象集合的能力:

//dynamic_poly_2.cpp

#include<iostream>

#include<vector>

#include"dynamic_poly.h"

//run異質vehicles集合爸爸爸爸

voidrun_vehicles(conststd::vector<Vehicle*>&vehicles)

{

for(unsignedinti=0;i<vehicles.size();++i)

{

vehicles[i]->run();

//根據具體vehicle的類型調用對應的run()

}

}

intmain()

{

Carcar;

Airplaneairplane;

std::vector<Vehicle*>v;

//異質vehicles集合

v.push_back(&car);

v.push_back(&airplane);

run_vehicles(v);

//run不同類型的vehicles

}

在run_vehicles()中,vehicles[i]->run()依據正被迭代的元素的類型而調用不同的成員函數。這從一個側面體現了面向對象編程風格的優雅。

靜態多態

如果說動態多態是通過虛函數來表達共同接口的話,那么靜態多態則是通過“彼此單獨定義但支持共同操作的具體類”來表達共同性,換句話說,必須存在必需的同名成員函數。

我們可以采用靜態多態機制重寫上一節的例子。這一次,我們不再定義vehicles類層次結構,相反,我們編寫彼此無關的具體類Car和Airplane(它們都有一個run()成員函數):

//static_poly.h

#include<iostream>

//具體類Car

classCar

{

public:

voidrun()const

{

std::cout<<"runacar\n";

}

};

//具體類Airplane

classAirplane

{

public:

voidrun()const

{

std::cout<<"runaairplane\n";

}

};

run_vehicle()應用程序被改寫如下:

//static_poly_1.cpp

#include<iostream>

#include<vector>

#include"static_poly.h"

//通過引用而run任何vehicle

template<typenameVehicle>

voidrun_vehicle(constVehicle&vehicle)

{

vehicle.run();

//根據vehicle的具體類型調用對應的run()

}

intmain()

{

Carcar;

Airplaneairplane;

run_vehicle(car);

//調用Car::run()

run_vehicle(airplane);

//調用Airplane::run()

}

現在Vehicle用作模板參數而非公共基類對象(事實上,這里的Vehicle只是一個符合直覺的記號而已,此外別無它意)。經過編譯器處理后,我們最終會得到run_vehicle<Car>()和run_vehicle<Airplane>()兩個不同的函數。這和動態多態不同,動態多態憑借虛函數分派機制在運行期只有一個run_vehicle()函數。

我們無法再透明地處理異質對象集合了,因為所有類型都必須在編譯期予以決定。不過,為不同的vehicles引入不同的集合只是舉手之勞。由于無需再將集合元素局限于指針或引用,我們現在可以從執行性能和類型安全兩方面獲得好處:

//static_poly_2.cpp

#include<iostream>

#include<vector>

#include"static_poly.h"

//run同質vehicles集合

template<typenameVehicle>

voidrun_vehicles(conststd::vector<Vehicle>&vehicles)

{

for(unsignedinti=0;i<vehicles.size();++i)

{

vehicles[i].run();

//根據vehicle的具體類型調用相應的run()

}

}

intmain()

{

Carcar1,car2;

Airplaneairplane1,airplane2;

std::vector<Car>vc;

//同質cars集合

vc.push_back(car1);

vc.push_back(car2);

//vc.push_back(airplane1);

//錯誤:類型不匹配

run_vehicles(vc);

//runcars

std::vector<Airplane>vs;

//同質airplanes集合

vs.push_back(airplane1);

vs.push_back(airplane2);

//vs.push_back(car1);

//錯誤:類型不匹配

run_vehicles(vs);

//runairplanes

}

兩種多態機制的結合使用

在一些高級C++應用中,我們可能需要結合使用動態多態和靜態多態兩種機制,以期達到對象操作的優雅、安全和高效。例如,我們既希望一致而優雅地處理vehicles的run問題,又希望“安全而高效”地完成給飛行器(飛機、飛艇等)進行“空中加油”這樣的高難度動作。為此,我們首先將上面的vehicles類層次結構改寫如下:

//dscombine_poly.h

#include<iostream>

#include<vector>

//公共抽象基類Vehicle

classVehicle

{

public:

virtualvoidrun()const=0;

};

//派生于Vehicle的具體類Car

classCar:publicVehicle

{

public:

virtualvoidrun()const

{

std::cout<<"runacar\n";

}

};

//派生于Vehicle的具體類Airplane

classAirplane:publicVehicle

{

public:

virtualvoidrun()const

{

std::cout<<"runaairplane\n";

}

voidadd_oil()const

{

std::cout<<"addoiltoairplane\n";

}

};

//派生于Vehicle的具體類Airship

classAirship:publicVehicle

{

public:

virtualvoidrun()const

{

std::cout<<"runaairship\n";

}

voidadd_oil()const

{

std::cout<<"addoiltoairship\n";

}

};

我們理想中的應用程序可以編寫如下:

//dscombine_poly.cpp

#include<iostream>

#include<vector>

#include"dscombine_poly.h"

//run異質vehicles集合

voidrun_vehicles(conststd::vector<Vehicle*>&vehicles)

{

for(unsignedinti=0;i<vehicles.size();++i)

{

vehicles[i]->run();

//根據具體的vehicle類型調用對應的run()

}

}

//為某種特定的aircrafts同質對象集合進行“空中加油”

template<typenameAircraft>

voidadd_oil_to_aircrafts_in_the_sky(conststd::vector<Aircraft>&aircrafts)

{

for(unsignedinti=0;i<aircrafts.size();++i)

{

aircrafts[i].add_oil();

}

}

intmain()

{

Carcar1,car2;

Airplaneairplane1,airplane2;

Airshipairship1,airship2;

std::vector<Vehicle*>v;

//異質vehicles集合

v.push_back(&car1);

v.push_back(&airplane1);

v.push_back(&airship1);

run_vehicles(v);

//run不同種類的vehicles

std::vector<Airplane>vp;

//同質airplanes集合

vp.push_back(airplane1);

vp.push_back(airplane2);

add_oil_to_aircrafts_in_the_sky(vp);

//為airplanes進行“空中加油”

std::vector<Airship>vs;

//同質airships集合

vs.push_back(airship1);

vs.push_back(airship2);

add_oil_to_aircrafts_in_the_sky(vs);

//為airships進行“空中加油”

}

我們保留了類層次結構,目的是為了能夠利用run_vehicles()一致而優雅地處理異質對象集合vehicles的run問題。同時,利用函數模板add_oil_to_aircrafts_in_the_sky<Aircraft>(),我們仍然可以處理特定種類的vehicles—aircrafts(包括airplanes和airships)的“空中加油”問題。其中,我們避開使用指針,從而在執行性能和類型安全兩方面達到了預期目標。

結語

長期以來,C++社群對于多態的內涵和外延一直爭論不休。在comp.object這樣的網絡論壇上,此類話題爭論至今仍隨處可見。曾經有人將動態多態(dynamicpolymorphism)稱為inclusionpolymorphism,而將靜態多態(staticpolymorphism)稱為parametricpolymorphism或parameterizedpolymorphism。

我注意到2003年斯坦福大學公開的一份C++andObject-OrientedProgramming教案中明確提到了函數多態概念:Functionoverloadingisalsoreferredtoasfunctionpolymorphismasitinvolvesonefunctionhavingmanyforms。文后的“參考文獻”單元給出了這個網頁鏈接。

可能你是第一次看到宏多態(macropolymorphism)這個術語。不必訝異—也許我就是造出這個術語的“第一人”。顯然,帶變量的宏(或類似于函數的宏或偽函數宏)的替換機制除了免除小型函數的調用開銷之外,也表現出了類似的多態性。在我們上面的例子中,字符串相加所表現出來的符合直覺的連接操作,事實上是由底部運算符重載機制(operatoroverloading)支持的。值得指出的是,C++社群中有人將運算符重載所表現出來的多態稱為adhocpolymorphism。

DavidVandevoorde和NicolaiM.Josuttis在他們的著作C++Templates:TheCompleteGuide一書中系統地闡述了靜態多態和動態多態技術。因為認為“和其他語言機制關系不大”,這本書沒有提及“宏多態”(以及“函數多態”)。(需要說明的是,筆者本人是這本書的繁體中文版譯者之一,本文正是基于這本書的第14章ThePolymorphicPowerofTemplates編寫而成)

動態多態只需要一個多態函數,生成的可執行代碼尺寸較小,靜態多態必須針對不同的類型產生不同的模板實體,尺寸會大一些,但生成的代碼會更快,因為無需通過指針進行間接操作。靜態多態比動態多態更加類型安全,因為全部綁定都被檢查于編譯期。正如前面例子所示,你不可將一個錯誤的類型的對象插入到從一個模板實例化而來的容器之中。此外,正如你已經看到的那樣,動態多態可以優雅地處理異質對象集合,而靜態多態可以用來實現安全、高效的同質對象集合操作。

靜態多態為C++帶來了泛型編程(genericprogramming)的概念。泛型編程可以認為是“組件功能基于框架整體而設計”的模板編程。STL就是泛型編程的一個典范。STL是一個框架,它提供了大量的算法、容器和迭代器,全部以模板技術實現。從理論上講,STL的功能當然可以使用動態多態來實現,不過這樣一來其性能必將大打折扣。

靜態多態還為C++社群帶來了泛型模式(genericpatterns)的概念。理論上,每一個需要通過虛函數和類繼承而支持的設計模式都可以利用基于模板的靜態多態技術(甚至可以結合使用動態多態和靜態多態兩種技術)而實現。正如你看到的那樣,AndreiAlexandrescu的天才作品ModernC++Design:GenericProgrammingandDesignPatternsApplied(Addison-Wesley)和Loki程序庫已經走在了我們的前面。

參考文獻

1.DavidVandevoorde,NicolaiM.Josuttis,C++Templates:TheCompleteGuide,AddisonWesley,2002.

2.ChrisNeumann,CS193d(Summer2003)C++andObject-OrientedProgramming,\n_blank/class/cs193d/,2003.虛函數表對C++了解的人都應該知道虛函數(VirtualFunction)是通過一張虛函數表(VirtualTable)來實現的。簡稱為V-Table。在這個表中,主是要一個類的虛函數的地址表,這張表解決了繼承、覆蓋的問題,保證其容真實反應實際的函數。這樣,在有虛函數的類的實例中這個表被分配在了這個實例的內存中,所以,當我們用父類的指針來操作一個子類的時候,這張虛函數表就顯得由為重要了,它就像一個地圖一樣,指明了實際所應該調用的函數。這里我們著重看一下這張虛函數表。在C++的標準規格說明書中說到,編譯器必需要保證虛函數表的指針存在于對象實例中最前面的位置(這是為了保證正確取到虛函數的偏移量)。這意味著我們通過對象實例的地址得到這張虛函數表,然后就可以遍歷其中函數指針,并調用相應的函數。聽我扯了那么多,我可以感覺出來你現在可能比以前更加暈頭轉向了。沒關系,下面就是實際的例子,相信聰明的你一看就明白了。假設我們有這樣的一個類:classBase{public:virtualvoidf(){cout<<"Base::f"<<endl;}virtualvoidg(){cout<<"Base::g"<<endl;}virtualvoidh(){cout<<"Base::h"<<endl;}};按照上面的說法,我們可以通過Base的實例來得到虛函數表。下面是實際例程:typedefvoid(*Fun)(void);Baseb;FunpFun=NULL;cout<<"虛函數表地址:"<<(int*)(&b)<<endl;cout<<"虛函數表—第一個函數地址:"<<(int*)*(int*)(&b)<<endl;//InvokethefirstvirtualfunctionpFun=(Fun)*((int*)*(int*)(&b));pFun();實際運行經果如下:(WindowsXP+VS2003,Linux2.6.22+GCC4.1.3)虛函數表地址:0012FED4虛函數表—第一個函數地址:0044F148Base::f通過這個示例,我們可以看到,我們可以通過強行把&b轉成int*,取得虛函數表的地址,然后,再次取址就可以得到第一個虛函數的地址了,也就是Base::f(),這在上面的程序中得到了驗證(把int*強制轉成了函數指針)。通過這個示例,我們就可以知道如果要調用Base::g()和Base::h(),其代碼如下:(Fun)*((int*)*(int*)(&b)+0);//Base::f()(Fun)*((int*)*(int*)(&b)+1);//Base::g()(Fun)*((int*)*(int*)(&b)+2);//Base::h()這個時候你應該懂了吧。什么?還是有點暈。也是,這樣的代碼看著太亂了。沒問題,讓我畫個圖解釋一下。如下所示:注意:在上面這個圖中,我在虛函數表的最后多加了一個結點,這是虛函數表的結束結點,就像字符串的結束符“\0”一樣,其標志了虛函數表的結束。這個結束標志的值在不同的編譯器下是不同的。在WinXP+VS2003下,這個值是NULL。而在Ubuntu7.10+Linux2.6.22+GCC4.1.3下,這個值是如果1,表示還有下一個虛函數表,如果值是0,表示是最后一個虛函數表。下面,我將分別說明“無覆蓋”和“有覆蓋”時的虛函數表的樣子。沒有覆蓋父類的虛函數是毫無意義的。我之所以要講述沒有覆蓋的情況,主要目的是為了給一個對比。在比較之下,我們可以更加清楚地知道其內部的具體實現。一般繼承(無虛函數覆蓋)下面,再讓我們來看看繼承時的虛函數表是什么樣的。假設有如下所示的一個繼承關系:請注意,在這個繼承關系中,子類沒有重載任何父類的函數。那么,在派生類的實例中,其虛函數表如下所示:對于實例:Derived;的虛函數表如下:我們可以看到下面幾點:1)虛函數按照其聲明順序放于表中。2)父類的虛函數在子類的虛函數前面。我相信聰明的你一定可以參考前面的那個程序,來編寫一段程序來驗證。一般繼承(有虛函數覆蓋)覆蓋父類的虛函數是很顯然的事情,不然,虛函數就變得毫無意義。下面,我們來看一下,如果子類中有虛函數重載了父類的虛函數,會是一個什么樣子?假設,我們有下面這樣的一個繼承關系。為了讓大家看到被繼承過后的效果,在這個類的設計中,我只覆蓋了父類的一個函數:f()。那么,對于派生類的實例,其虛函數表會是下面的一個樣子:我們從表中可以看到下面幾點,1)覆蓋的f()函數被放到了虛表中原來父類虛函數的位置。2)沒有被覆蓋的函數依舊。這樣,我們就可以看到對于下面這樣的程序,Base*b=newDerive();b->f();由b所指的內存中的虛函數表的f()的位置已經被Derive::f()函數地址所取代,于是在實際調用發生

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業或盈利用途。
  • 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論