【我的《冒號課堂》學習筆記】值與引用(1)語法類型


值與引用

 

值(value)與引用(reference)因其天生的對立性,提供了一個二分法(dichotomy)的准則。

  把數據分成兩類:

    值——具有某種類型的數據

    引用——可用來獲取特定數據的值

  把變量分成兩類:

    值變量——表示值的變量

    引用變量——表示引用的變量

  把數據類型分成兩類:

    值類型——能直接被訪問的數據類型

    引用類型——借助引用才能被訪問的數據類型

  把對象分成兩類:  值類型對象  引用類型對象

(按此定義,C++是沒有引用類型的,但這里有兩處令人疑惑的地方。一、C++有一種被稱為引用(reference)的類型(一種被指針更安全的引用);二、指針(包括第一點的特定引用)具有引用功能,可認為屬於引用端的引用類型,在上文提到的引用類型都是指被引用端的)

 

以下言論主要限於C++、Java和C#三種語言。

先簡單說說內存分配,不同語言采取的內存分配策略不盡相同,相同的語言也可能有不用的實現,但一般都有三種基本機制,按靈活度遞增依次為:靜態分配(static allocation)、棧分配(stack allocation)和堆分配(heap allocation)。其中靜態分配發生於編譯期,在靜態內存區內為全局變量、靜態變量、常數變量(在一些語言實現中為常數預備了常數變量存儲區)等安排空間。棧分配與堆分配發生在運行期,但前者一般在編譯器就可確定待分配內存的空間大小和生命周期(例外,C提供了非標准的alloca函數,可讓程序員在棧上分配動態大小的內存,但仍由編譯器釋放),后者則可能推遲到運行期。棧內存區用於存放由new運算符、malloc函數等動態分配而得的空間(C++中,new分配的free store和由malloc分配的棧內存區有可能不一致;C#中new產生的值類型對象也可能在棧上)。

棧分配是基於簡單的堆棧結構,因此效率很高。另外通常每一個線程都有獨立的棧區。故而棧變量天然是線程安全(thread-safe)的。棧分配的主要缺點就是須提前分配內存,而且棧內存總容量有限,容易發生棧溢出(stack overflow)。至於棧內存的靜態有效期,優劣參半:無需擔心內存管理的同時無法突破作用域。

堆分配雖然比棧分配更強大靈活,但其復雜的算法影響了時間效率,內存碎片、元數據開銷和可能的內存泄漏等問題也影響了空間效率。此外程序員還要負擔更多的內存管理、線程安全等方面的責任。

 

一個變量是在棧還是堆與是值變量還是引用變量無關。比如Java中,局部變量總在棧里,而實例變量總在堆里,與變量類型無關。其次,‘被引用的對象總在堆中’的說法在Java和C#中成立,但在C++中則未必——C++也可在棧上的對象創建引用。由於C++只有引用類型的變量,卻沒有引用類型的對象,因此更保險的說法是:引用類型對象總在堆中。值對象不是總在棧中,這是一個著名的誤解,正確的說法是:值對象可能再棧中。假如它嵌在引用類型對象當中,那么將與后者一道分配在堆中。

值變量與引用變量的區別好比名詞與代詞。對值變量而言,它對應的是目標數據;對引用變量而言,它對應的不是目標數據本身,而是用來訪問目標數據的數據,可以說是一種元數據(關於數據的數據)。舉例來說,引用最原始的形式是指針,任何有效的非空指針對應的數據都是一個內存地址——目標數據的地址。

 

//Java
SomeType a=new SomeType();
//上面的代碼實際上是2步原子操作,如下
SomeType a;
a=new SomeType();

  

 SomeType是引用類型,a是引用變量。由於a是局部變量,所以分配在棧上的,但它所指代的對象本身則是分配在堆上。

 

Java和C++是兩種極端,Java中無法自定義值類型(Java中的基本類型就是值類型)而C++中無法自定義引用類型(C++中雖不能創建引用類型,卻可以創建引用)。C#則是兼收並蓄,可以自定義引用類型和值類型。假如上述代碼是C#代碼,並且SomeType不是引用類型的class而是值類型的struct的話,那么系統將不在堆中分配內存,而在棧中直接分配一個對象。

//C++如下

SomeType* a=new SomeType();//堆上

SomeType a=SomeType();//棧上 簡化SomeType a;

值變量與引用變量的區別不在於它們存放的地點——棧或堆,而在於它們存放的內容——數據或地址,在於它們獲取目標數據的方式——直接或間接,在於它們存放目標數據的方式——在線(變量的目標數據在空間上內嵌於包含該變量的對象或環境中)或離線。這個關系好比是現金和銀行卡。值重在價值,引用重在使用價值。

值與引用本來是相對的概念,引用也是一種特殊的值——包含被引用對象的地址信息的值。再者,Java與C#中的引用變量在形式上兼具雙重身份:以引用的身份被調動方法,以值的身份被賦值或作為參數傳遞;C++中的指針是一種引用,但在指針的指針即二重指針的面前,它搖身一變成了后者的值。

在調用函數時,需要參數傳遞,即有一個將實際參數映射到形式參數的過程。最常見有兩種機制,一種是按值傳遞,函數收到的是實際參數的值——按位拷貝(bitwise copy);另一種是按引用傳遞,函數收到的是實際參數的引用——內存地址。此外還有拷貝-恢復(copy-restore),按名調用(call-by-name),宏展開(macro expansion)等機制。

 

Java中只有按值傳遞,沒有按引用傳遞!

在引用變量作為參數按值傳遞后,方法體獲得了對象引用的拷貝,與原引用對應着同一個對象,故仍可以操作該對象。Java方法盡管不能改變引用原件,即不能為其重新賦值,但仍可通過引用復件來改變對象的內容。“Java按值傳遞對象引用”的說法就合情合理。

//C++

static void change(string& s){s=”new“;}

//C#

static void change(ref string s){s=”new“;}

C++中string為值類型 加上”&“實現值類型的引用傳遞,而C#中string是引用類型,使用關鍵字ref表示傳進去的是string引用的引用,而不是拷貝。雖然看上去很迷,但是由此可以得出“Java按值傳遞對象引用”的結論。

 

當一個對象給一個變量賦值或作為參數按值傳遞是,C++中復制的是該對象的值,而Java只復制的卻是該對象的引用。因此C++有專門的賦值運算符合復制構造函數,而Java則沒有。如果Java要達到復制對象值的目的,不能隱式地通過變量賦值或參數傳遞,只能顯式地重新構造對象或者通過克隆(clone)、序列化(serialization)等手段。這就是值語義與引用語義的區別。

 

 

 


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com