C++和java多態(tài)的區(qū)別
以前我有個(gè)錯(cuò)誤的觀點(diǎn):即使在C++和java中多態(tài)性的實(shí)現(xiàn)機(jī)制可能不同,但它們的表現(xiàn)形式應(yīng)該相同,也就是說如果代碼結(jié)構(gòu)相同,那么執(zhí)行結(jié)果也應(yīng)該相同??上屡c愿違,事情并不總是我想象中的那樣子,那么C++和java多態(tài)到底有何區(qū)別呢?
首先我們提一下多態(tài)性的概念。根據(jù)Bjarne Stoustrup的說法,多態(tài)性其實(shí)就是方法調(diào)用的機(jī)制,也就是說當(dāng)在編譯時(shí)無法確定一個(gè)對象的實(shí)際類型時(shí),應(yīng)當(dāng)能夠在運(yùn)行時(shí)基于對象的實(shí)際類型來決定調(diào)用的具體方法(動(dòng)態(tài)綁定)。
我們先來看一下在C++中的函數(shù)調(diào)用方式:
- 普通函數(shù)調(diào)用:具體調(diào)用哪個(gè)方法在編譯時(shí)間就可以決定(通過查找編譯器的符號表),同時(shí)在使用標(biāo)準(zhǔn)過程調(diào)用機(jī)制基礎(chǔ)上增加一個(gè)表示對象身份的指針(this指針)。
- 虛函數(shù)調(diào)用:函數(shù)調(diào)用依賴于對象的實(shí)際類型,一般地說,對象的實(shí)際類型只能在運(yùn)行時(shí)間才能確定。虛函數(shù)一般要有兩個(gè)步驟來支持,首先每一個(gè)類產(chǎn)生出一堆指向虛函數(shù)的指針,放在表格中,這個(gè)表格就叫虛函數(shù)表(virtual table);然后每一個(gè)類對象(class object)會添加一個(gè)指向相關(guān)虛函數(shù)表(virtual table)的指針,通常這個(gè)指針叫做vptr。
在java中又是如何的呢?恩,區(qū)別還是滿大的。在java虛擬機(jī)中,類實(shí)例的引用就是指向一個(gè)句柄(handle)的指針,而該句柄(handle)其實(shí)是一對指針:其中一個(gè)指針指向一張表,該表格包含了對象的方法列表以及一個(gè)指向類對象(表示對象類型)的指針;另一個(gè)指針指向一塊內(nèi)存地址,該內(nèi)存是從java堆中為對象的數(shù)據(jù)而分配出來的。
這時(shí),你可能會說,好象差不多嘛,不是都要維護(hù)一張函數(shù)表嗎?別急,讓我們先看一下例子,這樣你就能更好的理解它們之間的區(qū)別到底有多大了。
下面是C++和java的例子,不看后面的答案,你能夠正確說出它們的執(zhí)行結(jié)果嗎?
例1:C++
- class Base
- {
- public:
- Base()
- {
- init();
- }
- virtual ~Base() {}
- public:
- virtual void do_init()
- {
- init();
- }
- protected:
- virtual void init()
- {
- cout << "in Base::init()" << endl;
- }
- };
- class Derived : public Base
- {
- public:
- Derived()
- {
- init();
- }
- protected:
- void init()
- {
- cout << "in Derived::init()" << endl;
- }
- };
- int main(int argc, char* argv[])
- {
- Base* pb;
- pb = new Derived();
- delete pb;
- return 0;
- }
例2:java
- class Base
- {
- public Base()
- {
- init();
- }
- protected void init()
- {
- System.out.println("in Base::init()");
- }
- public void do_init()
- {
- init();
- }
- }
- class Derived extends Base
- {
- public Derived()
- {
- init();
- }
- protected void init()
- {
- System.out.println("in Derived::init()");
- }
- }
- public class Test
- {
- public static void main(String[] args)
- {
- Base base = new Derived();
- }
- }
例1的執(zhí)行結(jié)果是:
- in Base::init()
- in Derived::init()
例2的執(zhí)行結(jié)果是:
- in Derived::init()
- in Derived::init()
看了結(jié)果后,你是馬上頓悟呢抑或是處于疑惑中呢?ok,我們來分析一下兩個(gè)例子的執(zhí)行過程。
首先看一下例1(C++的例子):
1. Base* pb; 只是聲明,不做什么。
2. pb = new Derived();
1) 調(diào)用new操作符,分配內(nèi)存。
2) 調(diào)用基類(本例中是Base)的構(gòu)造函數(shù)
3) 在基類的構(gòu)造函數(shù)中調(diào)用init(),執(zhí)行程序首先判斷出當(dāng)前對象的實(shí)際類型是Base(Derived還沒構(gòu)造出來,當(dāng)然不會是Derived),所以這里調(diào)用的是Base::init()。
4) 調(diào)用派生類(本例中是Derived)的構(gòu)造函數(shù),在這里同樣要調(diào)用init(),執(zhí)行程序判斷出當(dāng)前對象的實(shí)際類型是Derived,調(diào)用Derived::init()。
3. delete pb; 無關(guān)緊要。
例2(java的例子)的執(zhí)行過程:
1. Base base = new Derived();
1) 分配內(nèi)存。
2) 調(diào)用基類(本例中是Base)的構(gòu)造函數(shù)
3) 在基類的構(gòu)造函數(shù)中調(diào)用init(),執(zhí)行程序首先判斷出當(dāng)前對象的實(shí)際類型是Derived(對,Derived已經(jīng)構(gòu)造出來,它的函數(shù)表當(dāng)然也已經(jīng)確定了)所以這里調(diào)用的是Derived::init()。
4) 調(diào)用派生類(本例中是Derived)的構(gòu)造函數(shù),在這里同樣要調(diào)用init(),執(zhí)行程序判斷出當(dāng)前對象的實(shí)際類型是Derived,調(diào)用Derived::init()。
明白了吧。java中的類對象在構(gòu)造前(調(diào)用構(gòu)造函數(shù)之前)就已經(jīng)存在了,其函數(shù)表和對象類型也已經(jīng)確定了,就是說還沒有出生就已經(jīng)存在了。而C++中只有在構(gòu)造完畢后(所有的構(gòu)造函數(shù)都被成功調(diào)用)才存在,其函數(shù)表和對象的實(shí)際類型才會確定。所以這兩個(gè)例子的執(zhí)行結(jié)果會不一樣。當(dāng)然,構(gòu)造完畢后,C++與java的表現(xiàn)就都一樣了,例如你調(diào)用Derived::do_init()的話,其執(zhí)行結(jié)果是:
- in Derived::init()
個(gè)人認(rèn)為,java中的多態(tài)實(shí)現(xiàn)機(jī)制沒有C++中的好。還是以例子說明吧:
例子3:C++
- class Base
- {
- public:
- Base()
- {
- init();
- }
- virtual ~Base() {}
- protected:
- int value;
- virtual void init()
- {
- value = 100;
- }
- };
- class Derived : public Base
- {
- public:
- Derived()
- {
- init();
- }
- protected:
- void init()
- {
- cout << "value = " << value << endl;
- // 做一些額外的初始化工作
- }
- };
- int main(int argc, char* argv[])
- {
- Base* pb;
- pb = new Derived();
- delete pb;
- return 0;
- }
例4:java
- class Base
- {
- public Base()
- {
- init();
- }
- protected int value;
- protected void init()
- {
- value = 100;
- }
- }
- class Derived extends Base
- {
- public Derived()
- {
- init();
- }
- protected void init()
- {
- System.out.println("value = " + value);
- // 做一些額外的初始化工作
- }
- }
- public class Test
- {
- public static void main(String[] args)
- {
- Base base = new Derived();
- }
- }
例3的執(zhí)行結(jié)果是:
- value = 10
例4的執(zhí)行結(jié)果是:
- value = 0
- value = 0
從以上結(jié)果可以看出,java例子中應(yīng)該被初始化的值(這里是value)沒有被初始化,派生類根本不能重用基類的初始化函數(shù)。試問,如果初始化要在構(gòu)造時(shí)完成,并且初始化邏輯比較復(fù)雜,派生類也需要額外的初始化,派生類是不是需要重新實(shí)現(xiàn)基類的初始化函數(shù)呢?這樣的面向?qū)ο蠓椒ê貌缓媚?歡迎大家討論。
【編輯推薦】