天天看點

C++與Java 多态差別

1. C++中,如果父類中的函數前邊标有virtual,才顯現出多态。Java中,不管寫不寫virtual都是多态的,子類的同名函數會override父類的

如果父類func是virtual的,則

Super *p =new Sub();

p->func(); // 調用子類的func

如果不是virtual的,p->func将調用父類原來的函數。

Java中,不管寫不寫virtual都是多态的,子類的同名函數會override父類的。與C++很不同的是,初始化的過程也不相同。在還未初始化子類的時候,子類的同名函數就已經覆寫了父類的了。例如:

public class Super { 
    public Super() { 
        System.out.println("super constructor..."); 
        m(); 
    } 
    protected void m() {
        System.out.println("test"); 
    } 
}
 
public class Sub extends Super{ 
    private final Date date; 
    public Sub(){
            System.out.println("sub constructor...");         
        date=new Date();} 
    public void m() 
    { 
        System.out.println(date); 
    } 
    public static void main(String[] args) 
    {  
        Super test1=new Sub();
        test1.m();  //執行的子類的m 
    } 
}
           

new Sub的時候首先調用Super,Super構造函數調用的m就已經是被Sub覆寫的m,是以會print出null(因為日期沒有初始化)。是以在java中,不要在父類構造函數中調用外部可改變的方法,有可能會輸出可改變方法中還沒初始化的東西。

但是,同樣的初始化在C++中,初始化一個子類的時候,父類調用的m,是父類自己的m,不會調用子類的。

2.函數調用方式的差別

首先本文不讨論面向對象程式設計的基本概念,如封裝、繼承和資料抽象等,這方面的資料現在應該多如牛毛,隻是稍微提一下多态性的概念。根據Bjarne Stoustrup的說法,多态性其實就是方法調用的機制,也就是說當在編譯時無法确定一個對象的實際類型時,應當能夠在運作時基于對象的實際類型來決定 調用的具體方法(動态綁定)。

我們先來看一下在C++中的函數調用方式:

Ø 普通函數調用:具體調用哪個方法在編譯時間就可以決定(通過查找編譯器的符号表),同時在使用标準過程調用機制基礎上增加一個表示對象身份的指針(this指針)。

Ø 虛函數調用:函數調用依賴于對象的實際類型,一般地說,對象的實際類型隻能在運作時間才能确定。虛函數一般要有兩個步驟來支援,首先每一個類産生出一堆指 向虛函數的指針,放在表格中,這個表格就叫虛函數表(virtual table);然後每一個類對象(class object)會添加一個指向相關虛函數表(virtual table)的指針,通常這個指針叫做vptr。

在java中又是如何的呢?恩,差別還是滿大的。在java虛拟機中,類執行個體的引用就是指向一個句柄(handle)的指針,而該句柄(handle)其實是一對指針:其中一個指針指向一張表,該表格包含了對象的方法清單以及一個指向類對象(表示對象類型)的指針;另一個指針指向一塊記憶體位址,該記憶體是從java堆中為對象的資料而配置設定出來的。

唔,你要說了,好象差不多嘛,不是都要維護一張函數表嗎?别急,讓我們先看一下例子,這樣你就能更好的了解它們之間的差別到底有多大了。

下面是C++和java的例子,不看後面的答案,你能夠正确說出它們的執行結果嗎?

例1:C++

#include<iostream>
using namespace std;

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的執行結果是:

in Base::init()

in Derived::init()

例2的執行結果是:

in Derived::init()

in Derived::init()

看了結果後,你是馬上頓悟呢抑或是處于疑惑中呢?ok,我們來分析一下兩個例子的執行過程。

首先看一下例1(C++的例子):

1. Base* pb; 隻是聲明,不做什麼。

2. pb = new Derived();

1) 調用new操作符,配置設定記憶體。

2) 調用基類(本例中是Base)的構造函數

3) 在基類的構造函數中調用init(),執行程式首先判斷出目前對象的實際類型是Base(Derived還沒構造出來,當然不會是Derived),是以這裡調用的是Base::init()。

4) 調用派生類(本例中是Derived)的構造函數,在這裡同樣要調用init(),執行程式判斷出目前對象的實際類型是Derived,調用Derived::init()。

3. delete pb; 無關緊要。

例2(java的例子)的執行過程:

1. Base base = new Derived();

1) 配置設定記憶體。

2) 調用基類(本例中是Base)的構造函數

3) 在基類的構造函數中調用init(),執行程式首先判斷出目前對象的實際類型是Derived(對,Derived已經構造出來,它的函數表當然也已經确定了)是以這裡調用的是Derived::init()。

4) 調用派生類(本例中是Derived)的構造函數,在這裡同樣要調用init(),執行程式判斷出目前對象的實際類型是Derived,調用Derived::init()。

明白了吧。java中的類對象在構造前(調用構造函數之前)就已經存在了,其函數表和對象類型也已經确定了,就是說還沒有出生就已經存在了。而 C++中隻有在構造完畢後(所有的構造函數都被成功調用)才存在,其函數表和對象的實際類型才會确定。是以這兩個例子的執行結果會不一樣。當然,構造完畢 後,C++與java的表現就都一樣了,例如你調用Derived::do_init()的話,其執行結果是:

in Derived::init()。

個人認為,java中的多态實作機制沒有C++中的好。還是以例子說明吧:

例子3:C++

#include<iostream>
using namespace std;

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的執行結果是:

value = 10

例4的執行結果是:

value = 0

value = 0

從以上結果可以看出,java例子中應該被初始化的值(這裡是value)沒有被初始化,派生類根本不能重用基類的初始化函數。試問,如果初始化要 在構造時完成,并且初始化邏輯比較複雜,派生類也需要額外的初始化,派生類是不是需要重新實作基類的初始化函數呢?

3. 另外一個關于java的例子:

http://blog.csdn.net/lzz313/archive/2009/06/16/4274936.aspx

class Parent
{
	int x=10;
	public Parent(){
		add(2);
	}
	void add(int y){
		x+=y;
	}
}

class Child extends Parent
{
	int x=9;
	void add(int y){
		x+=y;
	} 
}

public class test
{
	public static void main(String[] args){
		Parent p=new Child();
		System.out.println(p.x);
	} 
}
           

問輸出結果是什麼? 

     答案應該是10。 

     要了解結果為什麼是10,需要首先明白下面的知識: 

     (1)方法和變量在繼承時的隐藏與覆寫 

     隐藏:若B隐藏了A的變量或方法,那麼B不能通路A被隐藏的變量或方法,但将B轉換成A後可以通路A被隐藏的變量或者方法。 

     覆寫:若B覆寫了A的變量或者方法,那麼不僅B不能通路A被覆寫的變量或者方法,将B轉換成A後同樣不能通路A被覆寫的變量或者方法。 

     (2)Java中變量與方法在繼承中的隐藏與覆寫規則: 

          一、父類的執行個體變量和類變量能被子類的同名變量隐藏。 

          二、父類的靜态方法被子類的同名靜态方法隐藏,父類的執行個體方法被子類的同名執行個體方法覆寫。 

          三、不能用子類的靜态方法隐藏父類的執行個體方法,也不能用子類的執行個體方法覆寫父類的靜态方法,否則編譯器會異常。 

          四、用final關鍵字修飾的最終方法不能被覆寫。 

          五、變量隻能被隐藏不會被覆寫,子類的執行個體變量可以隐藏父類的類變量,子類的類變量也可以隐藏父類的執行個體變量。 

     在上面的試題中,子類Child的執行個體方法add(int y)覆寫了父類Parent的執行個體方法add(int y),而子類的執行個體變量x則是隐藏了父類的執行個體變量x。 

     Child對象的初始化過程是: 

     首先為父類的執行個體變量x配置設定記憶體空間,因為在定義變量x時為它賦了值(int x=10),是以會同時将這個值賦給x。 

     其次調用父類的無參構造函數,Parent的構造函數中做的唯一的事情就是調用了add(2); 

     第三、由于子類的add(int y)方法覆寫了父類的方法,是以add(2)實際調用的是子類的方法,在子類的add方法中做了如下操作x+=j;在這裡由于子類的執行個體變量x隐藏了父類 的執行個體變量x,是以這條語句是針對子類本身的,但是這時還沒有為子類的實力變量x配置設定空間,它的預設值是0,加2之後是2。 

     第四、父類初始化完畢後接着初始化子類,為子類的x配置設定記憶體空間并将它指派為9,之前的add(2)操作白瞎了。 

     再次注意Parent p=new Child();這條語句,它是用父類的引用指向子類的對象,而前面已經說過變量隻會被隐藏不會被覆寫,是以這時的p.x值應該是父類的10,而不是子類的9; 

     如果将輸出語句換成下面的語句結果就是9了: 

     System.out..println(((Child)p).x); //首先将p轉換成Child類型

注:如果對java類初始化順序不清楚,請參看:http://my.oschina.net/leoson/blog/103251