前一段時間,我在 cnblogs 別人的部落格中,談到:
java 中的引用/指標,與 c++/C# 中的引用/指標不是一個概念.
Java 引用,相當於 c++ 指標(fun3)。Java 引用可以賦值 null, 而 c++ 引用 (見 fun2) 不能賦值 null,c++ 指標可以賦值 null(fun3).
Java 中,無 c++ 引用(fun2)對應的文法。
結果引起不必要的質疑,特此,寫部落格,對c++/java/c# 幾種程式設計語言的指標、引用,進行比較,期望引起更多的人,對此有所關注。
從文法上看,三種開發語言中,C++ 的指標、引用,最為複雜,因此,下面的舉例,都從 C++ 代碼開始,然後與 java/c# 的文法進行比較。
1) C++ 簡單類型變數,有直接變數定義、指標定義、引用定義。
int aa = 10;//c++ int &bb = aa;//c++ int *cc = &aa;//c++
上述三行代碼,最後三個變數指向同一個資料。相比較而言,java/c# 都只有變數定義,無引用定義、指標定義。補充:感謝 xiaotie 、飛浪 的提醒:C#中是有指標的,在unsafe狀態下,可以定義和使用指標。特更正。
2) C++ 函數調用參數,簡單類型變數,有直接變數定義、指標定義、引用定義,後兩個,在函數內部改變資料,退出函數,能看到改變後的資料。
void simple_by_val(int a, const int b){ a=15; //b=13; //error C2166: l-value specifies const object //a=NULL; //good //b=NULL; //error C2166: l-value specifies const object}void simple_by_ref(int &a, const int &b){ a=25; //b=23; //error C2166: l-value specifies const object //a=NULL; //good //b=NULL; //error C2166: l-value specifies const object}void simple_by_pointer(int *a, const int *b){ *a = 35; //*b = 33; //error C2166: l-value specifies const object a = NULL; //ok b = NULL; //ok}
java 沒有這麼多名堂,只有直接變數定義。C# 略為複雜一點,有引用,有 out 參數。
static void M(int a, ref int b, out int c) { c = 13; }
相比較而言,C# 的函數參數( ref int b), 類似於C++的函數參數( int &a),都是調用函數前要賦初值,在函數內部改變資料,退出函數,能看到改變後的資料。
而 C# 的 (out int c),在 C++/Java 中,無對應的文法。這個可以調用函數前,不賦初值。在 C# 之前,也很少見到這種文法,只在一些資料庫的預存程序、函數定義中,見過類似文法。估計是從資料庫編程文法中抄襲過來的文法。
特別註明:C# 的引用( ref int b),只是用在函數參數變數定義上,不能用在函數內部的局部變數中。C++ 中的引用( int &a),可以用在函數內部的局部變數中。
3) C++ 的類物件變數定義文法,較為複雜,可以定義在stack 上(不用 new),可以定義在 heap(用 new)。
CMyClass obj; //stack CMyClass *p2 = new CMyClass(); //heap
java/C# 中,沒有這麼複雜,可以認為是上述兩種“綜合+簡化”了。
4) 在 java/C# 中,如下用法是錯誤的,會報null 指標異常;但是在 C++ 裡是合法的。
CMyClass obj; obj.run();
在 C++ 中,
CMyClass obj;
以上一行代碼已經調用了建構函式,完成了變數初始化。而在 java/C# 中,這一行代碼相當於:
CMyClass obj = null;
5) C++ 中,stack 變數出了作用範圍,記憶體自動回收;heap 變數,需要手工 delete。
java/C# 中,變數是空閑時自動回收的(理論上的),不是變數出了作用範圍,就記憶體回收。
{ CMyClass obj; //stack obj.test();}//此處, stack 變數自動被 delete ,記憶體自動回收{ CMyClass *p2 = new CMyClass(); //heap p2->test();} //此處,超出變數 p2 的作用範圍,下面不能再用 p2 變數了,但是,記憶體並未釋放,有記憶體泄露。
6) 以下代碼在 C++ 中是正確的,在 java/C# 是錯誤的。在 java/C# 文法中,沒有定義變數加 * 的,也不能用 -> 來調用類的函數或類的成員變數,也不能用 delete。
CMyClass *p1 = null;CMyClass *p2 = new CMyClass();p2->ab();delete p2;p2 = null;
7) 以下代碼,在java/C# 文法中,是正確的,在 C++ 是錯誤的。C++ 中,這種賦值要用指標 (CMyClass *p1 = null;)。
CMyClass p1 = null;CMyClass p2 = new CMyClass();
8) 以下代碼,在 C++ 代碼中,會調用“拷貝建構函式”、"等號重載函數"。這兩個函數,在 C++ 中,預設會由編譯器自動產生。
//C++
CMyClass obj; //調用建構函式 CMyClass obj2 = obj; //調用拷貝建構函式 obj2 = obj; //調用 = 操作符重載函數
以上代碼,大致相當於 java/C# 中的 複製"clone"。但更隱蔽(初學者不知道調用了 C++ 建構函式、拷貝建構函式、= 操作符重載函數)、更複雜。java/C# 無操作符重載函數。
//C# CMyClass obj = new CMyClass(); CMyClass obj2 = (CMyClass)obj.Clone();
而在 C# 中,Clone 函數並不會自動產生。在 Java 中,可以調用 super.clone() ---- Java 基類 Object 預設有一個 clone 函數。
在 C++ 中,預設會由編譯器自動產生“拷貝建構函式”、"等號重載函數",這一點,很多時候會造成問題,要特別注意。
在 C++ 中,函數傳回值不要用 CMyClass ,這會造成不必要地調用“拷貝建構函式”、"等號重載函數";也不要返回引用 CMyClass&, 對函數內局部變數的引用,退出函數後無法繼續使用。而要返回指標 CMyClass *(最好用智能指標封裝後的指標變數)。這一點很多初學者不明白。
但是 C++ 的 std:string 除外。std:string 的“拷貝建構函式”、"等號重載函數"經過最佳化,拷貝後的變數,與拷貝之前的變數,內部使用相同的 char[] 數組,只有當一個 string 變數改變時,才會把 char[] 數組複製成兩份。std:string 的“拷貝建構函式” 沒有效能上損失,又比 string 指標減少了記憶體泄露,因此,對 std:string ,使用時盡量用 物件變數、對象引用、對象拷貝構造,避免使用 std:string 指標。
另,java/C# 的 String 變數不可改變(有其它類,比如 java StringBuilder類是可變的),C++ 的 string 變數可以改變。這個細微差異,很多人不明白。
9) C++ 引用文法,有一些是 Java/C# 程式員不知道的文法:
//C++CMyClass &a1; //錯誤,C++ 引用變數定義的時候就要初始化;//Java/C# 物件變數,沒有要求變數定義的時候,就要初始化CMyClass &a1 = NULL; //錯誤,C++ 引用變數不能賦值 nullCMyClass &a1 = new CMyClass(); //錯誤,C++ 引用變數不能賦值給一個 new 對象,這種情況,要用 C++ 指標。//以下C++ 代碼是正確的:CMyClass a;CMyClass &a1 = a;CMyClass *b =new CMyClass();CMyClass &b1 = *b; //這種寫法不常用。
10) Sun 自稱 java 中消滅了 C++ 中萬惡的指標,自己的物件變數,都是引用。做個比較:
C++ 引用不能賦值 null, 不能賦值 new XXX();C++ 指標可以賦值 null, 可以賦值 new XXX()。
C++ 引用對象通常在 stack 中,而C++ 指標 new 出來的對象則在 heap 中。
java/C# 中的物件變數,可以賦值 null, 可以賦值 new XXX()。java/C# 中的物件變數在 heap 中。
因此,java/C# 中的物件變數,更像是 C++ 中的指標,而不是 C++ 中的引用。
11) C++ 中,指標變數是一個 long 型整數,可以亂指的:
CMyClass *obj = (CMyClass *) 99; //compile/run good, should not use
如果我知道一個記憶體位址,就可以定義一個C++指標變數,指向這個記憶體位址。C++ 的“引用”沒有這個功能。C#/Java 的物件變數更沒有這個功能。
“指標亂指” 是 C++ 指標功能強大、靈活的體現(PC 上最早出現播放視頻的時候,大概是 intel 486 CPU 時代,C++軟體通常都直接寫顯存,據說這樣速度更快),也是最容易出問題的地方。估計是因為這個原因,所以C#/Java 都去掉了這個功能。所謂“萬惡的C++指標”,多半,也是指的是“指標亂指”。
12) C++ 有野指標,即已經刪除對象,但指標還是指向刪除對象,還可以繼續操作,但運行結果不保證正確。
CMyClass *p = new CMyClass();...//給 p 指向的記憶體賦值delete p;//這時 p 仍然指向之前的記憶體位址,該記憶體位址資料,一般情況下、短時間內,並沒有被清空或者覆蓋,仍然可以讀/寫。這就是“野指標”。p->run(); //運行結果可能正確,可能不正確,沒有保證。//此時指標 p 對應的記憶體,可能被下一個 new XXX() 代碼,用了這個記憶體,因此,理論上講,delete 之後的指標,不應再用來操作對象。p= NULL; //將指標指向“空”,可以避免“野指標”問題。p->run(); //這裡會報執行階段錯誤。也就是null 指標異常。null 指標異常在 java/c# 中都有。
C++ 中,delete 與將變數賦值 null , 理應放在一起,可以認為是一個“資料庫事務”一樣的,要麼都成功、要麼都失敗。其實,delete 關鍵字,是由 C++ 標準定義的,標準中,完全可以要求: delete 所在行的代碼,執行之後,把指標變數變成 null(C++ 標準的規範,很多都是規定編譯器做什麼,因此可以加這個規定)。這樣可以避免野指標問題。可惜,C++ 標準,在這方面沒有考慮周全。
另,有人抱怨,面試做題,看不是是 C++ 還是 Java、C# , 期望通過看本文,可以協助一二。
---------------------------------------
歡迎大家下載試用折桂單點登入系統, http://zheguisoft.com