從Java到C++ — 對比Java與C++程式設計的不同
1. 資料型別和變數
C++ 中的變數型別與Java很相似。像Java一樣,C++ 有int 和 double 型別。但是這些數字型別的取值範圍是依賴於機器的。比如在16位系統上,例如執行DOS 或Windows 3.x的PC機上,int 是雙位元組(2-byte)的,取值範圍比Java的4-byte的int 要小很多。在這些機器上,如果 int 不夠用的話,你需要使用長整型long。
C++ 有 short 和 unsigned 型別來更有效的儲存數字。(我認為所謂有效是指更高的空間利用率。) 最好是儘量避免使用這些型別除非是空間利用的有效性對你的系統真的非常重要。
在C++中布林型用 bool 表示,而不像在Java中用boolean。
C++ 中字串型別用 string 表示。它與Java中的 String 型別非常相似,但是,還是要逐一以下幾點不同之處:
1. C++ 字串儲存ASCII 碼字元,而不是標準碼Unicode 字元
2. C++ 字串是可以被修改的,而Java字串的內容是不可修改的(immutable)。
3. 取子字串的操作在 C++ 中叫做 substr,這個命令s.substr(i, n) 從字串s中取得從位置 i 開始長度為n的子字串。
4. 在C++中,你只能夠將字串與其它字串物件相串聯(concatenate),而不能夠與任意的物件相串聯。
5. C++中可以直接使用關係操作符 ==、 !=、 <、 <=、 >、 >= 來進行字串比較,其中後面四個操作符是按字母順序進行比較的。 這比Java中使用函式equals和compareTo來比較要方便很多。
2. 變數和常量
在C++中,本地變數的定義看起來與Java中相同,例如:
int n = 5;
實際上這正是C++和Java的一個重要不同之處。C++編譯器不對本地變數進行初始化檢驗,所以在C++中很容易忘記初始化一個變數,這種情況下,變數的值該變數所佔記憶體區域中剛好當前存在隨機值。這顯然是很容易產生程式出錯的地方。
與Java一樣, C++中類可以有資料域和靜態變數。不同的是,C++中變數可以在函式甚至是類的外面定義,這些所謂的全域性變數可以在程式的任何函式中被訪問,因而不易被很好的管理。所C++中應該儘量避免使用全域性變數。
在C++中,常量可以在任何地方被定義(記得在Java中,常量必須是類的靜態資料static data)。 C++ 使用關鍵字 const 來定義常量,而Java中是 final。例如:
const int DAYS_PER_YEAR = 365;
3. 類
C++ 中對類的定義與Java有些不同,這裡是一個例子:一個C++ 版本的 Point 類:
class Point /* C++ */
{
public:
Point();
Point(double xval, double yval);
void move(double dx, double dy);
double getX() const;
double getY() const;
private:
double x;
double y;
};
這裡幾點重要的不同是:
1. C++的類定義中分為公共和私有部分,分別以關鍵字 public 和 private開始。而在Java中,每一個元素都必須標明 public 或 private。
2. C++中類的定義只包含函式的宣告,真正的實現另外單獨列出。
3. 訪問函式(accessor methods)標有關鍵字 const ,表明這個函式不會改變本物件的元素值。
4. 類定義的結尾處有分號
類中函式的實現跟在類的定義之後。因為函式是在類外面定義的,所以每一個函式的名字前面要加類名稱作為字首,並使用操作符雙冒號::來分割類的名稱和函式的名稱。不改變隱含引數值(即當前物件的值)的訪問函式用 const標明。如下所示是上面類定義中的函式的實現:
Point::Point() { x = 0; y = 0; }
void Point::move(double dx, double dy)
{
x = x + dx;
y = y + dy;
}
double Point::getX() const
{
return x;
}
4. 物件
Java 與 C++ 最主要的不同在於物件變數的使用。在 C++中,物件變數儲存的是真正的物件的值,而不是物件引用(reference)。注意在C++中構造一個物件的時候是不使用關鍵字new的,只需要在變數的名字後面直接賦予建構函式的引數就可以了,例如:
Point p(1, 2); /* 構造物件 p */
如果不跟引數賦值,則使用預設建構函式,例如:
Time now; /* 預設使用建構函式 Time::Time() */
這一點與Java很不同。在Java中,這個命令僅僅生成一個沒有初始化的reference,而在C++中,它生成一個實際的物件。
當一個物件被賦給另一個物件變數的時候,實際的值將被拷貝。而在Java中,拷貝一個物件變數只不過是建立了另外一個指向物件的reference。拷貝一個C++的物件就像在Java中呼叫clone這個函式一樣,而修改拷貝的值不會改變原物件的值。例如:
Point q = p; /* 拷貝p到q */
q.move(1, 1); /* 移動q而p不動,即q的值變了,而p的不變*/
多數情況下,C++中這種物件直接對值操作的特性使用起來很方便,但是也有些時候不盡如人意:
1. 當需要一個函式中修改一個物件的值,必須記住要使用按引用呼叫call by reference (參見下面函式部分)
2. 兩個物件變數不能指向同一個物件實體。如果你要在C++中實現這種效果,必須使用指標pointer(參見下面指標部分)
3. 一個物件變數只能儲存一種特定的型別的值,如果你想要使用一個變數來儲存不同子類的物件的值(多型ploymorphism),則需要使用指標。
4. 如果你想在C++中使用一個變數來或者指向null或者指向一個實際的物件,則需要使用指標
5. 函式
在Java中,每一個函式必須或者是物件函式(instance method),或者是靜態函式(static function)或稱類函式。C++同樣支援物件函式和靜態函式(類函式),但同時C++也允許定義不屬於任何類的函式,這些函式叫做全域性函式(global functions)。
特別的是,每一個C++ 程式都從一個叫做 main的全域性函式開始執行:
int main()
{ . . .
}
還有另外一個格式的main函式可以用來捕捉命令列引數,類似於Java的main函式,但是它要求關於C格式的陣列和字串的知識,這裡就不介紹了。
按照習慣,通常如果程式執行成功, main 函式返回0,否則返回非零整數。
同Java一樣,函式引數是通過值傳遞的(passed by value)。在Java中,函式無論如何都是可以修改物件的值的。然而在C++中,因為物件直接儲存的是實際的值,而不是指向值的reference,也就是說傳入函式的是一個實際值的拷貝,因此也就無法修改原來物件的值。
所以,C++ 有兩種引數傳遞機制,同Java一樣的按值呼叫(call by value) ,以及按地址呼叫(call by reference)。當一個引數是按reference傳遞時,函式可以修改其原始值。Call by reference 的引數前面有一個地址號 & 跟在引數型別的後面,例如:
void raiseSalary(Employee& e, double by)
{ . . .
}
下面是一個典型的利用call by reference的函式,在Java中是無法實現這樣的功能的。
void swap(int& a, int& b)
{ int temp = a;
a = b;
b = temp;
}
如果使用 swap(x, y)來呼叫這個函式,則reference引數 a 和 b 指向原實際引數x 和 y的位置,而不是它們的值的拷貝,因此這個函式可以實現實際交換這兩個引數的值。
在 C++中,每當需要實現修改原引數的值時你就可以使用按地址呼叫 call by reference 。
6. 向量Vector
C++ 的向量結構結合了Java中陣列和向量兩者的優點。一個C++ 的向量可以方便的被訪問,其容量又可以動態的增長。如果 T 是任意型別,則 vector<T> 是一個元素為 T 型別的動態陣列。下面的語句
vector<int> a;
產生一個初始為空的向量。而語句
vector<int> a(100);
生成一個初始有100個元素的向量。你可以使用push_back 函式來新增元素:
a.push_back(n);
呼叫 a.pop_back() 從a中取出最後一個元素(操作後這個元素被從a中刪掉), 使用函式size 可以得到當前a中的元素個數。
你還可以通過我們熟悉的 [] 操作符來訪問向量中元素,例如:
for (i = 0; i < a.size(); i++) {
sum = sum + a[i];
}
同Java中一樣,陣列索引必須為 0 和 a.size() - 1之間的值。但是與Java不同的是,C++中沒有runtime的索引號合法性檢驗。試圖訪問非法的索引位置可能造成非常嚴重的出錯。
就像所有其它 C++ 物件一樣,向量也是值。如果你將一個向量賦值給另外一個向量變數,所有的元素都會被拷貝過去。
vector<int> b = a; /* 所有的元素都被拷貝了 */
對比Java中的情況,在Java中,一個數組變數是一個指向陣列的reference。拷貝這個變數僅僅產生另外一個指向同一陣列的reference,而不會拷貝每一個元素的值。
正因如此,如果一個C++函式要實現修改向量的值,必須使用reference引數:
void sort(vector<int>& a)
{ . . .
}
7. 輸入和輸出
在C++中,標準的輸入輸出流用物件 cin 和 cout 表示。我們使用 << 操作符寫輸出,例如:
cout << "Hello, World!";
也可以連著輸出多項內容,例如:
cout << "The answer is " << x << "\n";
我們使用 >> 操作符來讀入一個數字或單詞,例如:
double x;
cout << "Please enter x: ";
cin >> x;
string fname;
cout << "Please enter your first name: ";
cin >> fname;
函式getline 可以讀入整行的輸入,例如:
string inputLine;
getline(cin, inputLine);
如果到達輸入的結尾,或者一個數字無法被正確的讀入,這個流物件會被設定為 failed 狀態,我們可以使用函式 fail 來檢驗這個狀態,例如:
int n;
cin >> n;
if (cin.fail()) cout << "Bad input";
一旦一個流的狀態被設為failed,我們是很難重置它的狀態的,所以如果你的程式需要處理錯誤輸入的情況,應該使用函式 getline 然後人工處理得到的輸入資料。
8. 指標pointer
我們已經知道在C++中,物件變數直接儲存的是物件的值。這是與Java不同的,在Java中物件變數儲存的是一個地址,該地址指向物件值實際儲存的地方。有時在C++中也需要實現這樣的佈置,這就用到了指標pointer。在 C++中,一個指向物件的變數叫做指標。如果T是一種資料型別,則 T* 是指向這種資料型別的指標。 這裡重點介紹C++與Java的不同,要詳細瞭解C++中指標的使用,請參考本站另一篇文章“”。
就像 Java中一樣,一個指標變數可以被初始化為空值 NULL,另外一個指標變數的值,或者一個呼叫new生成的新物件:
Employee* p = NULL;
Employee* q = new Employee("Hacker, Harry", 35000);
Employee* r = q;
實際上在C++中還有第四種可能,那就是指標可以被初始化為另外一個物件的地址,這需要使用地址操作符 & :
Employee boss("Morris, Melinda", 83000);
Employee* s = &boss;
這實際上並不是什麼好主意。保險的做法還是應該直接讓指標指向使用 new生成的新物件。
到目前為止,C++ 指標看起來非常像 Java 的物件變數。然而,這裡有一個很重要的語法的不同。我們必須使用星號操作符 * 來訪問指標指向的物件。如果 p 是一個指向Employee物件的指標,則 *p 才代表了這個物件:
Employee* p = . . .;
Employee boss = *p;
當我們需要執行物件的函式或訪問物件的一個數據域時,也需要使用 *p :
(*p).setSalary(91000);
*p外面的括號是必需的,因為 . 操作符比 * 操作符有更高的優先順序。C的設計者覺得這種寫法很難看,所以他們提供了另外一種替代的寫法,使用 -> 操作符來實現 * 和 . 操作符的組合功能。表示式
p->setSalary(91000);
可以呼叫物件*p的函式 setSalary 。你可以簡單的記住 . 操作符是在物件上使用的,-> 操作符是在指標上使用的。
如果你不初始化一個指標,或者如果一個指標為空值 NULL 或指向的物件不再存在,則在它上面使用 * 或 -> 操作符就會出錯。 不幸的是 C++ runtime 系統並不檢查這個出錯。如果你範了這個錯誤,你的程式可能會行為古怪或宕機。
而在Java中,這些錯誤是不會發生的。所有的reference都必須初始化,所有的物件只要仍有reference指向它就不會被從記憶體中清除,因此你也不會有一個指向已被刪除的物件的reference。Java的runtime 系統會檢查reference是否為空,並在遇到空指標時丟擲一個null pointer的例外(exception)。
C++ 和 Java還有一個顯著的不同,就是 Java 有垃圾回收功能,能夠自動回收被廢棄的物件。而在C++中,需要程式設計師自己管理記憶體分配回收。
C++中當物件變數超出範圍時可以自動被回收。但是使用new生成的物件必須用delete操作符手動刪除,例如:
Employee* p = new Employee("Hacker, Harry", 38000);
. . .
delete p; /* 不在需要這個物件 */
如果你忘記刪除一個物件,那麼你的程式有可能最終用光所有記憶體。這就是我們常說的記憶體洩漏 (memory leak)。更重要的是,如果你如果刪除了一個物件,然後又繼續使用它,你可能覆蓋不屬於你的資料。如果你剛巧覆蓋了用於處理記憶體回收的資料域,那麼記憶體分配機制就可能運轉失常而造成更嚴重的錯誤,而且很難診斷和修復。因此,在C++中最好儘量少用指標。
9. 繼承
C++和Java中繼承的基本語法是很相似的。在C++中,使用 : public 代替Java中的extends 來表示繼承關係 。 (C++ 也支援私有繼承的概念,但是不太有用。)
預設情況下,C++中的函式不是動態繫結的。如果你需要某個函式實現動態繫結,需要使用virtual宣告它為虛擬函式,例如:
class Manager : public Employee
{
public:
Manager(string name, double salary, string dept);
virtual void print() const;
private:
string department;
};
同Java一樣,建構函式中呼叫父類的建構函式有特殊的語法。 Java使用關鍵字 super。C++中必須在子類的建構函式體外呼叫父類的建構函式。下面是一個例子:
Manager::Manager(string name, double salary, string dept)
: Employee(name, salary) /* 呼叫父類的建構函式 */
{ department = dept;
}
Java 中在子類函式中呼叫父類的函式時也使用關鍵字 super 。而在C++中是使用父類的名稱加上操作符 ::表示,例如:
void Manager::print() const
{ Employee::print(); /* 呼叫父類的函式 */
cout << department << "\n";
}
一個 C++ 物件變數只能儲存特定型別的物件值。要想在C++中實現多型(polymorphism),必須使用指標。一個 T* 指標可以指向型別為 T 或 T 的任意子類的物件,例如:
Employee* e = new Manager("Morris, Melinda", 83000, "Finance");
你可以將父類和不同子類的物件混合收集到一個元素均為指標的向量中,然後呼叫動態繫結的函式,如下所示:
vector<Employee*> staff;
. . .
for (i = 0; i < staff.size(); i++)
staff[i]->print();