來看一段簡單的範例程式:
C++:
/* main.cpp */
#include "Point.h"

int main() {
  Point p1(5, 5);
  Point p2 = p1;
  cout << "p1 is : " << p1 << endl;
  cout << "p2 is : " << p2 << endl;
  
  p1.setX(500);
  p1.setY(500);
  cout << "p1 is : " << p1 << endl;
  cout << "p2 is : " << p2 << endl;
  return 0;
}

Java:
/* main.java */

public class Main {
  public static void main(String args[]) {
    Point p1 = new Point(5,5);
    Point p2 = p1;
    System.out.println("p1 is : " + p1);
    System.out.println("p2 is : " + p2);
    
    p1.setX(500);
    p1.setY(500);
    System.out.println("p1 is : " + p1);
    System.out.println("p2 is : " + p2);
  }
}


猜猜它們的結果會不會一樣!?

先補上其他部分的程式碼:
C++:
/* Point.h*/ #include <iostream> using namespace::std; class Point { private: int posX; int posY; public: Point(); Point(int x, int y); void setX(int x); void setY(int y); int getX() const; int getY() const; }; ostream& operator<<(ostream& os, const Point& p);

/* Point.cpp */ #include "Point.h" Point::Point(int x, int y) : posX(x), posY(y) { } void Point::setX(int x) { posX = x; } void Point::setY(int y) { posY = y; } int Point::getX() const { return posX; } int Point::getY() const { return posY; } ostream& operator<<(ostream& os, const Point& p) { return (os << "(" << p.getX() << "," << p.getY() << ")"); }
Java:
/* Point.java */ public class Point { public Point(int x, int y) { posX = x; posY = y; } public void setX(int x) { posX = x; } public void setY(int y) { posY = y; } public String toString() { return "("+ posX + "," + posY + ")"; } private int posX; private int posY; }




如果你不熟析Java,那麼它的結果可能會讓你有點吃驚:
C++:
C:\Point>bcc32 Point.cpp main.cpp C:\Point>Point.exe p1 is : (5,5) p2 is : (5,5) p1 is : (500,500) p2 is : (5,5)
Java:
C:\Point>javac Point.java C:\Point>java Point p1 is : (5,5) p2 is : (5,5) p1 is : (500,500) p2 is : (500,500)

在Java中,p2的內容也一併被修改了!

這是因為在Java中,所有的物件,即所有原生資料型態(primitive data type)的變數,都是參考!


參考是什麼!?


何謂參考(reference)?wiki中對參考的解釋,我覺得寫的不錯:「a reference is an object containing information about how to locate and access the particular data item, as opposed to containing the data itself.
參考的概念很像指標,它們同樣只保存關於「如何存取某筆資料」的資訊(像是指標中所指向的記憶體位址);但並不保存「該筆資料」本身。

因此,上述的範例中,兩種語言在寫法上雖然非常相似,但是所描述的行為卻是不同的。
我下方的圖簡單說明:

C++:
Java:
Point p1(5,5);
p1
XXXX : 0000
5
XXXX : 0004
5
XXXX : 0008
XXXX : 000C
Point p1 = new Point(5,5);
p1
XXXX : 0000
XXXX : 0100
XXXX : 0004
XXXX : 0008
XXXX : 000C
. . .
Instance of
XXXX : 0100
5
Point Object
XXXX : 0104
5
XXXX : 0108
XXXX : 010C

Point p2 = p1;
p1
XXXX : 0000
5
XXXX : 0004
5
p2
XXXX : 0008
5
XXXX : 000C
5
Point p2 = p1;
p1
XXXX : 0000
XXXX : 0100
p2
XXXX : 0004
XXXX : 0100
XXXX : 0008
XXXX : 000C
. . .
Instance of
XXXX : 0100
5
Point Object
XXXX : 0104
5
XXXX : 0108
XXXX : 010C


在C++中,「p2 = p1」的意義是將p1物件的內容複製一份給p2;
然而在Java中,p2與p1都是參考,因此「p2 = p1」的意義是將p1所指向的物件的參考複製一份給p2。
這樣的作法有點類似C/C++中的指標。
若將上述的C++範例改寫為使用指標的話:
Point* p1 = new Point(5,5);
Point* p2 = p1;
...
p1->setX(500);
...
那其行為與結果就會與上述的Java範例中類似了。


C++與Java中的參考


每種程式語言對於參考的支援也都不盡相同。
在Java中所有的物件都是一個參考,這是Java的特性之一。Java的參考不支援算術運算,也不能對其取址,不過可以改變該參考所指之處。
像上述的Java範例中,你可以將p2改為指向p3所指向的物件實體:
Java:
Point p3 = new Point(7, 7);
p2 = p3;
p1
XXXX : 0000
XXXX : 0100
p2
XXXX : 0004
XXXX : 0108
p3
XXXX : 0008
XXXX : 0108
XXXX : 000C
. . .
Instance of
XXXX : 0100
5
Point Object
XXXX : 0104
5
Instance of
XXXX : 0108
7
Point Object
XXXX : 010C
7

如此一來,p2與p3所指向的就是同一個物件實體了。


C++中也支援參考,寫法如下:
C++:
Point& p2 = p1;
p1, p2
XXXX : 0000
5
XXXX : 0004
5
XXXX : 0008
XXXX : 000C


上例中,p2宣告為一個參考,與p1指向同一塊記憶體。

注意這邊&是寫在資料型態後面,代表的是參考,是C++才有的寫法,C並不支援。
(在C中,有個很類似的寫法,就是寫在變數(variable)前面的 & ,代表針對該變數取址,通常作為指標用。
此兩處的&所代表的意義是不同的,請小心不要混淆。)


C++中的參考,必須在宣告時就指定其所指之處,且不得改變

在C++中,使用參考存取記憶體中的資料時,不需要像指標變數那般,使用不同的語法(syntax)來存取所指向的資料;
像是使用「*」運算子取值,或是使用「->」運算子存取成員(member)。
因此,一個宣告為參考的變數,在使用上就如一般的變數般:
C++:
p1.setX(6)
p1, p2
XXXX : 0000
6
XXXX : 0004
5
XXXX : 0008
XXXX : 000C
p2.setY(6); // 等同於 p1.setY(6);
p1, p2
XXXX : 0000
6
XXXX : 0004
6
XXXX : 0008
XXXX : 000C

因此,C++的參考,更像是一個別名(Alias)。

但注意!千萬別學Java企圖用「=」(assignment operator)讓C++中的參考指向別處,像下面這個例子(程式碼接續上例):

C++:
Point p3(7,7);
p1, p2
XXXX : 0000
6
XXXX : 0004
6
p3
XXXX : 0008
7
XXXX : 000C
7


p2 = p3;
p1, p2
XXXX : 0000
7
XXXX : 0004
7
p3
XXXX : 0008
7
XXXX : 000C
7


在上例中,「p2 = p3」等同於「p1 = p3」!
結果不僅p2還是與p1指向同一塊記憶體,且p1的內容也被修改了。
(更別異想天開寫出「&p2 = &p3」...即便是在C也不允許修改變數位址的,否則就天下大亂了...。)

前幾個例子其實看不太出在C++使用參考的好處,
事實上,C++的參考,比較常用的場合是在呼叫函式時,傳遞參數給函式的時候,稱為call by reference。在C++的標準函式庫中,很常見到這樣的作法,如srting類別的compare函式:
int string::compare( const string& str ) const;
這樣做的好處,與C的傳址呼叫(call by address)是一樣的,就是避免傳遞大型資料結構時,複製資料所需的時間。

而需要注意的事也一樣,就是在函式中若修改了該參數的內容,則原本作為參數的變數也會一併被修改。這也是為什麼compare的str參數前面還加上了const



回到最一開始的範例:
C++:
/* main.cpp */ #include "Point.h" int main() { Point p1(5, 5); Point p2 = p1; cout << "p1 is : " << p1 << endl; cout << "p2 is : " << p2 << endl; p1.setX(500); p1.setY(500); cout << "p1 is : " << p1 << endl; cout << "p2 is : " << p2 << endl; return 0; }
Java:
/* main.java */ public class Main { public static void main(String args[]) { Point p1 = new Point(5,5); Point p2 = p1; System.out.println("p1 is : " + p1); System.out.println("p2 is : " + p2); p1.setX(500); p1.setY(500); System.out.println("p1 is : " + p1); System.out.println("p2 is : " + p2); } }

同樣是類似的寫法,在C++中代表的是變數內容的複製,在Java中代表的卻是參考的複製,這是兩者產生的結果有所不同的原因。


物件的複製


那麼,在Java中,要如何做才能真正複製一個物件呢?
答案是使用Object類別的clone方法。

首先你必須先實作Cloneable這個介面:
public class Point implements Cloneable{
...
幸運的是Cloneable本身並沒有定義任何成員需要實作。
但你還需要覆寫(override)Object類別中的public Object clone()這個方法(method):
public Object clone() throws CloneNotSupportedException {
  return super.clone();
}

接著,只要把p2 = p1改成:
Point p2 = (Point) p1.clone();
通常就可以了...

為什麼說通常呢?因為物件複製(object copy)的問題還不僅如此,
上例中以super呼叫了父類別Object的clone方法來複製物件,但是Object類別的clone方法執行的是shallow copy,也就是說,如果當欲複製之物件的成員中,也有物件的時候,該成員只會將其參考複製而已!
(反之,則稱為deep copy。)

如果你希望該成員也要進行複製,那麼你也必須針對該成員所屬的類別作一樣的事:
實作Cloneable介面覆寫clone方法

同樣的問題連C++也會遇到,就是當欲複製之物件的成員中,有指標的時候。
C++中的同類別的物件在複製的時候,是透過copy constructorcopy assignment operator,兩者都是C++的special member functions,由編譯器(compiler)自動實作,而有需求的話programmer可自行override。

而預設上,無論是copy constructor還是copy assignment operator都是執行shallow copy。因此,若你希望執行deep copy,則必須將兩者覆寫。

arrow
arrow
    全站熱搜

    MylesLittleWolf 發表在 痞客邦 留言(2) 人氣()