轉自: http://blog.csdn.net/zhuweisky/article/details/415661
不可否認,C++在過去十年乃至現在一直都是windows平台上的主流開發語言,而來勢兇猛的.NET勢必開闢一個嶄新的局面,從目前的種種跡象來看,.NET是大勢所趨,而C#作為.NET平台上的第一開發語言自然備受關注,於是有很多程式員紛紛轉向C#,這其中當然不乏C++程式員。情況往往是這樣,從一種語言過渡到另一種語言,哪怕是比較相似的語言,程式員也經常無意識地陷入原開發語言的思維定勢,這樣的結果通常只有一個,那就是導致連程式員自己也始終想不通的錯誤。本文由某C++程式員提出的“難道C#中沒有拷貝建構函式?”這一問題引出C++與C# 某些語言特性的對比。
一.發生了什嗎?
如果你是正在轉向C# 的C++程式員,你一定對C#中的類沒有拷貝建構函式和很少發生賦值運算子的調用感到不可理解,而且你看到的很多語句並不是像你想象的那樣執行,比如
//假設Student是一個類 (C#)
Student s1 = new Student() ;
Student s2 ;
s2 = s1 ; //此語句將發生什嗎?
因為你是一個熟練的C++程式員,所以你在潛意識中就已經斷定語句Student s2 ;將會在棧中產生一個Student對象(即執行個體),而語句s2
= s1 ;將會調用賦值運算子。即上述語句執行完後,會產生如下記憶體布局
錯了,全錯了!!!
在C#中卻不是這樣。我先解釋上述語句在C#中是怎樣執行的。
Student s1 = new Student() ;
上面的代碼將會在堆中產生一個對象,並且讓引用s1指向這個對象,而引用s1本身位於棧中,佔用四個位元組(在32位處理器上,即一個指標的長度)。
Student s2 ;
該聲明將會在棧中產生一個長度位4位元組的引用變數s2,並且預設為null,即該引用不指向任何執行個體。
s2 = s1 ;
該指派陳述式並沒有調用賦值運算子,而是僅僅使s2指向s1所指向的對象。所以上述語句執行完後,記憶體布局大致如
想要明白為什麼,先要知道C#與C++引用的區別。
二.C# 與C++的引用區別
由上述簡單的例子就會看到“引用”在C# 和C++中的表現是多麼的不一樣,其主要區別可以表述為一句話:
C++中的引用是緊綁定的,而C# 中的引用是鬆綁定的。
C++中的引用使用“&”標記法宣告,而且聲明時必須被初始化為一個有效對象,而且引用一經初始化後,就不能再次賦值(即不能再令其指向其它對象),因此在C++中編譯器認為所有的引用都是有效,不必進行類型檢查等,這是C++中引用沒有指標靈活卻比指標高效的原因。可以這麼說,在C++中,因為引用與對象是緊綁定的,我們可以認為引用就是對象本身。正如棧對象的名字就是棧對象本身一樣。也可以這麼想,C++中的引用只是某個對象的一個別名,這個名字僅僅因為這個對象的存在而存在。
請看如下C++代碼
// c++
Student s1 ;
Student& s2 = s1; //s2是棧對象s1的別名
... ...
Student s3 ;
s2 = s3; //非法!!!s2不能再成為對象s3的別名
s3 = s2 ; //將調用賦值運算子
正是由於C++中的緊綁定特性,所以上面最後一條語句會調用賦值運算子,使對象s2和s3處於一樣的狀態。
再看看C#。
在C#中只有兩種類型的資料:實值型別和參考型別。實值型別通常是基礎資料型別 (Elementary Data Type),如int,double,還有struct等;而所有的自訂的類,還有數組、代表、介面等都是參考型別。由於這樣的約定,所以你就不必對C#中沒有“&”引用符而感到奇怪了。
所有的實值型別對象永遠只在棧中分配,即使你對實值型別使用了new ;
//C#
int age = new int(24) ; //仍然在棧中為age分配空間 ,與語句int age = 24 ;等價。
同樣所有參考型別對象永遠只在堆中建立。要產生參考型別的執行個體一定要用new,而new返回的引用通常儲存在棧中。僅僅聲明參考型別對象,就相當於聲明了一個null 指標(即C#中的引用可以為空白,這在C++中是不允許的),只是在棧中分配了4個位元組給這個引用,在該引用沒有賦值之前(即沒有指向有效堆記憶體),不能使用該引用,因為該引用為空白。
//C#
Student s5 ; //僅僅聲明一個引用,並沒有建立任何對象
s5 = new Student() ; //在堆中建立一個對象,並讓s5指向該對象
其實,C#中的引用更像C++中的指標,也就是說
C#中的引用是具有指標語義的引用。
所以,C#中的引用賦值就像C++中的指標賦值一樣,僅僅是讓其指向另外的對象。也就是說C#中使用的是最淺層次的拷貝。引用相互賦值時,僅僅是引用的值(表示邏輯記憶體位址資料)發生了改變,而對引用指向的對象的狀態沒有絲毫的影響――如果說有影響,那就是僅僅改變了GC對該對象的引用計數。
正是由於C#中的引用具有指標的語義,才方便了GC對對象的引用計數。當某個對象的引用計數變為0時,GC就會釋放這個對象,這就是C#中自動記憶體管理的基本原理。
三.引用傳遞和值傳遞
在C++中按值傳遞對象時,會調用拷貝建構函式產生對象的副本,那麼對應的C#中也是這樣的嗎?
無論是在C++中還是在C#中,當變數或對象作為函數參數進行傳遞時都有兩種方式:按值傳遞和按引用傳遞。
所謂按值傳遞是指在函數體內部使用的是對象的副本,在C++中這個副本是調用對象的拷貝建構函式完成的,而函數對副本的修改不會影響原來的對象。如
//C++
void Fun1(Student ss)
{
... ... //對ss進行處理和修改――實際處理的是傳入對象的副本
}
... ...
Student s7 ;
Fun1(s7) ;//此函數調用結束後,對象s7的狀態並沒有改變
... ...
所謂按引用傳遞是指傳給函數的實際上是對象的地址,這樣函數對對象的修改就會反應在對象中,使對象的狀態發生變化。如
//C++
void Fun2(Student& ss)
{
... ... //對ss進行處理和修改
}
... ...
Student s8 ;
Fun2(s8) ;//此函數調用結束後,對象s8的狀態發生改變
... ...
在C++中,可以通過指標和“&”引用符實現引用傳遞。上面的例子用到了“&”引用符號,其實換成指標也可以達到同樣的效果。如果我們再進一步去想,可以發現,當用指標進行引用傳遞時,也發生了複製,只不過複製的是指標的值(即對象的地址),而不是複製指標指向的對象。這可以通過如下例子得到證明。
//C++
void Fun3(Student* ss)
{
... ... //對ss指向的對象進行處理和修改
ss = NULL ;
}
... ...
Student* s9 ;
Fun3(s9) ;//此函數調用結束後, s9指向的對象的狀態發生了改變
... ...
但是在Fun3(s9)調用完後,s9並不是NULL ,這說明Fun3中使用的是指標s9的副本。如果再進一步,我們可以猜測用“&”符實現引用傳遞時也發生了同樣的故事。事實上也是這樣,C++中的引用只是一個受限卻更加安全的指標而已。
那麼按引用傳遞和按值傳遞各有什麼好處了?
按引用傳遞不需要發生拷貝行為,因此速度快,特別是大對象時,這種優勢很明顯。按值傳遞時對傳入對象的修改實際是對對象副本的修改,不會影響原對象的狀態。
你也許會想到如果採用const引用傳遞那麼就可以得到雙倍的好處,可以這麼說,但是不要走極端。
一般而言,將不允許改變的大對象作為const引用傳遞給函數是很合適的,但如果是簡單類型或自訂的小對象直接用值傳遞就可以了。
如果外界一定要看到函數對對象的修改,那麼只有一條路 ―― 按引用傳遞。
在C#中情況卻發生了變化,C#中的參考型別的對象都是按引用傳遞且只能按引用傳遞。而實值型別對象(或者稱為變數),通常情況下是按值傳遞的。如果要按引用傳遞實值型別對象,那麼就要使用關鍵字ref或out 。ref和out的唯一區別是ref用修飾參數時要求傳入的變數被初始化過。
由於類是參考型別,而所有的參考型別的對象的傳遞都是引用傳遞,所以在此過程中根本不會發生拷貝函數的調用。照這樣看來,根本就沒有必要有拷貝建構函式了。
我想現在你已經知道了C# 中為什麼不需要拷貝建構函式和很少調用賦值運算子了。你也許會問既然是很少調用賦值運算子,那一定還有調用賦值運算子的情況存在,那麼這種情況是怎樣的?那是因為類的相仿體――結構struct 。
四.struct
C++中的struct和class幾乎沒有任何差別,唯一的差別在於struct的成員預設是公有的,而class的成員預設是私人的。然而情況在C#中發生了本質的變化,因為C#中的struct是實值型別的,而class是參考型別的。從下面的分析可以看出C#的創造者在這點設計上真是獨具匠心。那麼好處在哪裡?
C#中所有實值型別都在棧中建立,在棧中建立對象較之在堆中建立對象的優勢在於:效率更高。因為在堆中指派至之前要採用一定的演算法尋找合適的記憶體塊,而這可能是很費時間的,而建立實值型別對象直接壓棧就可以了;還有棧對象在函數返回時會自動釋放,而堆對象要由GC來處理。如果我們設計的是一個不太大的類,而且其執行個體很少在函數間傳遞(因為函數間按非引用傳遞實值型別對象會發生複製),那麼我們可以考慮將其實現為struct來代替class。
既然struct是實值型別,當兩個同類型的struct相互賦值時,自然就會調用struct的賦值運算子。
另外,經過我的驗證,在C#中確實沒有提供拷貝建構函式,但是你可以通過重載建構函式來變相地得到拷貝建構函式,這個技術的實現是很簡單的,此處就不多說了。
講到這裡,已經差不多了,所以你不必在為像“為什麼C#中沒有拷貝建構函式?”、“為什麼C#中很少看到賦值運算子的調用?”這樣的問題而疑惑了 :)