不可否認(rèn), C++ 在過去十年乃至現(xiàn)在一直都是 windows 平臺上的主流開發(fā)語言,而來勢兇猛的 .NET 勢必開辟一個嶄新的局面,從目前的種種跡象來看, .NET 是大勢所趨,而 C# 作為 .NET 平臺上的第一開發(fā)語言自然備受關(guān)注,于是有很多程序員紛紛轉(zhuǎn)向 C# ,這其中當(dāng)然不乏 C++ 程序員。情況往往是這樣,從一種語言過渡到另一種語言,哪怕是比較相似的語言,程序員也經(jīng)常無意識地陷入原開發(fā)語言的思維定勢,這樣的結(jié)果通常只有一個,那就是導(dǎo)致連程序員自己也始終想不通的錯誤。本文由某 C++ 程序員提出的“難道 C# 中沒有拷貝構(gòu)造函數(shù)?”這一問題引出 C++ 與 C# 某些語言特性的對比。
一.發(fā)生了什么?
如果你是正在轉(zhuǎn)向 C# 的 C++ 程序員,你一定對 C# 中的類沒有拷貝構(gòu)造函數(shù)和很少發(fā)生賦值運算符的調(diào)用感到不可理解,而且你看到的很多語句并不是像你想象的那樣執(zhí)行,比如
// 假設(shè) Student 是一個類 ( C# )
Student s1 = new Student() ;
Student s2 ;
s2 = s1 ; // 此語句將發(fā)生什么?
因為你是一個熟練的 C++ 程序員,所以你在潛意識中就已經(jīng)斷定語句 Student s2 ; 將會在棧中生成一個 Student 對象(即實例),而語句 s2 = s1 ; 將會調(diào)用賦值運算符。即上述語句執(zhí)行完后,會產(chǎn)生如下內(nèi)存布局
<shapetype id="_x0000_t75" stroked="f" filled="f" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t" o:spt="75" coordsize="21600,21600"></shapetype><stroke joinstyle="miter"></stroke><formulas></formulas><f eqn="if lineDrawn pixelLineWidth 0"></f><f eqn="sum @0 1 0"></f><f eqn="sum 0 0 @1"></f><f eqn="prod @2 1 2"></f><f eqn="prod @3 21600 pixelWidth"></f><f eqn="prod @3 21600 pixelHeight"></f><f eqn="sum @0 0 1"></f><f eqn="prod @6 1 2"></f><f eqn="prod @7 21600 pixelWidth"></f><f eqn="sum @8 21600 0"></f><f eqn="prod @7 21600 pixelHeight"></f><f eqn="sum @10 21600 0"></f><lock aspectratio="t" v:ext="edit"></lock><shape id="_x0000_i1025" style="WIDTH: 248.25pt; HEIGHT: 115.5pt" type="#_x0000_t75"></shape><imagedata o:title="stack&heap03" src="file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_image001.jpg"></imagedata>
錯了,全錯了!!!
在 C# 中卻不是這樣。我先解釋上述語句在 C# 中是怎樣執(zhí)行的。
Student s1 = new Student() ;
上面的代碼將會在堆中生成一個對象,并且讓引用 s1 指向這個對象,而引用 s1 本身位于棧中,占用四個字節(jié)(在 32 位處理器上,即一個指針的長度)。
<shape id="_x0000_i1026" style="WIDTH: 220.5pt; HEIGHT: 102pt" type="#_x0000_t75" o:ole=""></shape><imagedata o:title="" src="file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_image003.wmz"></imagedata>
Student s2 ;
該聲明將會在棧中生成一個長度位 4 字節(jié)的引用變量 s2 ,并且缺省為 null ,即該引用不指向任何實例。
s2 = s1 ;
該賦值語句并沒有調(diào)用賦值運算符,而是僅僅使 s2 指向 s1 所指向的對象。所以上述語句執(zhí)行完后,內(nèi)存布局大致如下圖
<shape id="_x0000_i1027" style="WIDTH: 239.25pt; HEIGHT: 111pt" type="#_x0000_t75"></shape><imagedata o:title="stack & heap 02" src="file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_image005.jpg"></imagedata>
想要明白為什么,先要知道 C# 與 C++ 引用的區(qū)別。
二. C# 與 C++ 的引用區(qū)別
由上述簡單的例子就會看到“引用”在 C# 和 C++ 中的表現(xiàn)是多么的不一樣,其主要區(qū)別可以表述為一句話:
C++ 中的引用是緊綁定的,而 C# 中的引用是松綁定的。
C++ 中的引用使用“ & ”符號聲明,而且聲明時必須被初始化為一個有效的對象,而且引用一經(jīng)初始化后,就不能再次賦值(即不能再令其指向其它對象),因此在 C++ 中編譯器認(rèn)為所有的引用都是有效的,不必進行類型檢查等,這是 C++ 中引用沒有指針靈活卻比指針高效的原因。可以這么說,在 C++ 中,因為引用與對象是緊綁定的,我們可以認(rèn)為 引用就是對象本身。 正如棧對象的名字就是棧對象本身一樣。也可以這么想, C++ 中的引用只是某個對象的一個別名,這個名字僅僅因為這個對象的存在而存在。
請看如下 C++ 代碼
// c++
Student s1 ;
Student& s2 = s1; //s2 是棧對象 s1 的別名
... ...
Student s3 ;
s2 = s3; // 非法!!! s2 不能再成為對象 s3 的別名
s3 = s2 ; // 將調(diào)用賦值運算符
正是由于 C++ 中的緊綁定特性,所以上面最后一條語句會調(diào)用賦值運算符,使對象 s2 和 s3 處于一樣的狀態(tài)。
再看看 C# 。
在 C# 中只有兩種類型的數(shù)據(jù):值類型和引用類型。值類型通常是基本數(shù)據(jù)類型,如 int , double ,還有 struct 等;而所有的自定義的類,還有數(shù)組、代表、接口等都是引用類型。由于這樣的約定,所以你就不必對 C# 中沒有“ & ”引用符而感到奇怪了。
所有的值類型對象永遠(yuǎn)只在棧中分配,即使你對值類型使用了 new ;
//C#
int age = new int(24) ; // 仍然在棧中為 age 分配空間 ,與語句 int age = 24 ; 等價。
同樣所有引用類型對象永遠(yuǎn)只在堆中創(chuàng)建。要生成引用類型的實例一定要用 new ,而 new 返回的引用通常保存在棧中。僅僅聲明引用類型對象,就相當(dāng)于聲明了一個空指針(即 C# 中的引用可以為空,這在 C++ 中是不允許的),只是在棧中分配了 4 個字節(jié)給這個引用,在該引用沒有賦值之前(即沒有指向有效的堆內(nèi)存),不能使用該引用,因為該引用為空。
//C#
Student s5 ; // 僅僅聲明一個引用,并沒有創(chuàng)建任何對象
s5 = new Student() ; // 在堆中創(chuàng)建一個對象,并讓 s5 指向該對象
其實, C# 中的引用更像 C++ 中的指針,也就是說
C# 中的引用是具有指針語義的引用。
所以, C# 中的引用賦值就像 C++ 中的指針賦值一樣,僅僅是讓其指向另外的對象。也就是說 C# 中使用的是最淺層次的拷貝。引用相互賦值時,僅僅是引用的值(表示邏輯內(nèi)存地址數(shù)據(jù))發(fā)生了改變,而對引用指向的對象的狀態(tài)沒有絲毫的影響――如果說有影響,那就是僅僅改變了 GC 對該對象的引用計數(shù)。
正是由于 C# 中的引用具有指針的語義,才方便了 GC 對對象的引用計數(shù)。當(dāng)某個對象的引用計數(shù)變?yōu)? 0 時, GC 就會釋放這個對象,這就是 C# 中自動內(nèi)存管理的基本原理。
三.引用傳遞和值傳遞
在 C++ 中按值傳遞對象時,會調(diào)用拷貝構(gòu)造函數(shù)生成對象的副本,那么對應(yīng)的 C# 中也是這樣的嗎?
無論是在 C++ 中還是在 C# 中,當(dāng)變量或?qū)ο笞鳛楹瘮?shù)參數(shù)進行傳遞時都有兩種方式:按值傳遞和按引用傳遞。
所謂按值傳遞是指在函數(shù)體內(nèi)部使用的是對象的副本,在 C++ 中這個副本是調(diào)用對象的拷貝構(gòu)造函數(shù)完成的,而函數(shù)對副本的修改不會影響原來的對象。如
//C++
void Fun1(Student ss)
{
... ... // 對 ss 進行處理和修改――實際處理的是傳入對象的副本
}
... ...
Student s7 ;
Fun1(s7) ;// 此函數(shù)調(diào)用結(jié)束后,對象 s7 的狀態(tài)并沒有改變
... ...
所謂按引用傳遞是指傳給函數(shù)的實際上是對象的地址,這樣函數(shù)對對象的修改就會反應(yīng)在對象中,使對象的狀態(tài)發(fā)生變化。如
//C++
void Fun2(Student& ss)
{
... ... // 對 ss 進行處理和修改
}
... ...
Student s8 ;
Fun2(s8) ;// 此函數(shù)調(diào)用結(jié)束后,對象 s8 的狀態(tài)發(fā)生改變
... ...
在 C++ 中,可以通過指針和“ & ”引用符實現(xiàn)引用傳遞。上面的例子用到了“ & ”引用符號,其實換成指針也可以達到同樣的效果。如果我們再進一步去想,可以發(fā)現(xiàn),當(dāng)用指針進行引用傳遞時,也發(fā)生了復(fù)制,只不過復(fù)制的是指針的值(即對象的地址),而不是復(fù)制指針指向的對象。這可以通過如下例子得到證明。
//C++
void Fun3(Student* ss)
{
... ... // 對 ss 指向的對象進行處理和修改
ss = NULL ;
}
... ...
Student* s9 ;
Fun3(s9) ;// 此函數(shù)調(diào)用結(jié)束后, s9 指向的對象的狀態(tài)發(fā)生了改變
... ...
但是在 Fun3 ( s9 )調(diào)用完后, s9 并不是 NULL ,這說明 Fun3 中使用的是指針 s9 的副本。如果再進一步,我們可以猜測用“ & ”符實現(xiàn)引用傳遞時也發(fā)生了同樣的故事。事實上也是這樣, C++ 中的引用只是一個受限卻更加安全的指針而已。
那么按引用傳遞和按值傳遞各有什么好處了?
按引用傳遞不需要發(fā)生拷貝行為,因此速度快,特別是大對象時,這種優(yōu)勢很明顯。按值傳遞時對傳入對象的修改實際是對對象副本的修改,不會影響原對象的狀態(tài)。
你也許會想到如果采用 const 引用傳遞那么就可以得到雙倍的好處,可以這么說,但是不要走極端。
一般而言,將不允許改變的大對象作為 const 引用傳遞給函數(shù)是很合適的,但如果是簡單類型或自定義的小對象直接用值傳遞就可以了。
如果外界一定要看到函數(shù)對對象的修改,那么只有一條路 ―― 按引用傳遞。
在 C# 中情況卻發(fā)生了變化, C# 中的引用類型的對象都是按引用傳遞且只能按引用傳遞。而值類型對象(或者稱為變量),通常情況下是按值傳遞的。如果要按引用傳遞值類型對象,那么就要使用關(guān)鍵字 ref 或 out 。 ref 和 out 的唯一區(qū)別是 ref 用修飾參數(shù)時要求傳入的變量被初始化過。
由于類是引用類型,而所有的引用類型的對象的傳遞都是引用傳遞,所以在此過程中根本不會發(fā)生拷貝函數(shù)的調(diào)用。照這樣看來,根本就沒有必要有拷貝構(gòu)造函數(shù)了。
我想現(xiàn)在你已經(jīng)知道了 C# 中為什么不需要拷貝構(gòu)造函數(shù)和很少調(diào)用賦值運算符了。你也許會問既然是很少調(diào)用賦值運算符,那一定還有調(diào)用賦值運算符的情況存在,那么這種情況是怎樣的?那是因為類的相仿體――結(jié)構(gòu) struct 。
四. struct
C++ 中的 struct 和 class 幾乎沒有任何差別,唯一的差別在于 struct 的成員默認(rèn)是公有的,而 class 的成員默認(rèn)是私有的。然而情況在 C# 中發(fā)生了本質(zhì)的變化,因為 C# 中的 struct 是值類型的,而 class 是引用類型的。從下面的分析可以看出 C# 的創(chuàng)造者在這點設(shè)計上真是獨具匠心。那么好處在哪里?
C# 中所有值類型都在棧中創(chuàng)建,在棧中創(chuàng)建對象較之在堆中創(chuàng)建對象的優(yōu)勢在于:效率更高。因為在堆中分配對象之前要采用一定的算法尋找合適的內(nèi)存塊,而這可能是很費時間的,而創(chuàng)建值類型對象直接壓棧就可以了;還有棧對象在函數(shù)返回時會自動釋放,而堆對象要由 GC 來處理。如果我們設(shè)計的是一個不太大的類,而且其實例很少在函數(shù)間傳遞(因為函數(shù)間按非引用傳遞值類型對象會發(fā)生復(fù)制),那么我們可以考慮將其實現(xiàn)為 struct 來代替 class 。
既然 struct 是值類型,當(dāng)兩個同類型的 struct 相互賦值時,自然就會調(diào)用 struct 的賦值運算符。
另外,經(jīng)過我的驗證,在 C# 中確實沒有提供拷貝構(gòu)造函數(shù),但是你可以通過重載構(gòu)造函數(shù)來變相地得到拷貝構(gòu)造函數(shù),這個技術(shù)的實現(xiàn)是很簡單的,此處就不多說了。
講到這里,已經(jīng)差不多了,所以你不必在為像“為什么 C# 中沒有拷貝構(gòu)造函數(shù)?”、“為什么 C# 中很少看到賦值運算符的調(diào)用?”這樣的問題而疑惑了 :)
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
