關於取地址運算符&以及指針的問題

這恐怕不是翻譯的問題。如果在你所看的那本書裏,出現了“取地址操作符即&,不能施加於表達式”這種說法的話,那麼,這種說法是錯誤的。不過,考慮到這本書的特殊的背景,這種“錯誤”在某種程度上是可以被原諒的 —— 畢竟,如果那本書能將一切問題都講細緻的話,它就遠遠不能止於那個篇幅了。

首先,在排除其他意義的情況下,作爲操作符的&,叫做“取地址操作符”(Address Operator)。然而,這種稱呼,其實有相當多的弊端,比如,會令讀者認爲,由其得到的指針就是地址。而實際情況是,指針與地址是兩個不同的範疇,無論如何都不能視同一致。所以,依照薛非大蝦的思路,並且遵循„Zu den Sachen selbst!“的精神,我們不妨這樣來描述操作符&:

它用來幫助我們獲取一個指針,這個指針僅指向它所作用的操作數。

理解這句話,有這樣幾個要點:

(1)&是操作符。既然是操作符,就有操作數,即操作符所施加的對象。

(2)或許,對於初學者,準確地理解“操作數”這個詞,是有些障礙的。因爲用漢字“數”來指稱這個對象,難免讓人望文生義地用初等數學裏的“數”或代數表達式來理解它。所以,我們有必要以更加確切的境況下的事實,揭示:這裏的“操作數”究竟指的是什麼?

(3)&的操作數,非但可以是表達式,而且必須是表達式。不過,這種表達式,並不是C語言裏的任意一個表達式,而是受制於某種規則的表達式。

(4)現在,我們引入了“表達式”這個範疇。這個概念,是問題的核心,也是許多問題膠着難解的所在。可以這麼說,一門編程語言在表達式這個範疇上的實際含義,體現了該門語言的特質。顯然,能解釋清楚這類事情,並非易事。我們還是以„Zu den Sachen selbst!“的思路繼續發掘。

(5)在C語言中,爲了平滑而自然地接受它在“表達式”這件事情上的準確含義,我們還得事先引入“對象”這個概念(跟“面向對象編程”裏的“對象”不同)。粗略地說,在C語言的視角下,一切存在於內存空間上的區域,皆是對象。這裏的內存空間,一般專指主存儲器上可以承載數據的空間分佈。詳細討論“主存儲器”,恐怕要花上幾本書的篇幅,現在我們只抓住幾個重點:

(i) 主存儲器上用來承載數據的空間分佈,以“地址”這種機制來把握。—— 我們理解“地址”,不應該僅僅將其理解爲一個數值範疇,而是應該用它的作用(爲什麼要用到它)來理解。從硬件視角看,連接在CPU與主存儲器之間的“血脈”分別有地址總線、數據總線與控制總線等。從運作邏輯接口來看,主存儲器在CPU這端,以用來放置前者上的地址的寄存器與用來放置前者上的數據的寄存器來呈現。

(ii)“地址”這種機制,在實際運用中,具有“連續”的特質(這種固有稟性,是關於指針的一些關鍵運算的基本前提)。而同是用來承載數據的寄存器(專指CPU的某個組成部分,不指其他的寄存器),則沒有這種特質。從這個角度上,我們可以將主存儲器與寄存器很清晰地分別開來。所以,用存儲類別register限定的被聲明的對象,不能成爲&的操作數。但是,請特別注意:前述規定的理由,並不是在於register跟寄存器有一一對應的聯繫!而是在於:由register限定的變量,有可能被編譯器分配到寄存器。register這個限定符的真確含義是:可以用它,來提示編譯器在爲變量分配存儲空間的時候,[color=blue]編譯器允許將變量分配到寄存器,從而棄絕可能發生的與該變量有關的“地址”與“指針”機制。[/color]

(6)回到“對象”(即存在於內存空間上的區域)這件事情。在C語言中,如何讀取與刷寫對象上的數據?C語言採取的策略,是左值表達式。不難理解,只有左值表達式可以出現在賦值等號(賦值操作符)的左邊,有如:[code]int a, b, c, d, e, *ptr;
/* 上述部分變量須初始化,此略 */

a = b;
ptr = &a;
ptr[c] = 123;
* ptr = 456 + d;
*(ptr + e + 1) = 789;[/code]在這上面代碼中的後面五行裏,[code]a、ptr、ptr[c]、*ptr、*(ptr+e+1)[/code]都是左值表達式。

我們需要注意:上面所說的“只有左值表達式可以出現在賦值等號的左邊”,並不意味着“左值表達式只可以出現在賦值等號的左邊”。比如,在上列第一行代碼中,b其實也是一個左值表達式,但它可以出現在賦值等號的右邊。通過這樣的比對,我們不難理解:在C語言中,採用左值表達式這種策略,來handle該表達式所引用(這專指一種更爲一般的機制,與C++等語言專屬術語“引用”不同)的對象,從而實現對對象上的數據進行讀取或刷寫的企圖。比如:在賦值表達式a=b中,b首先是一個左值表達式,C語言用它來讀取這個表達式即b所引用的內存區域上的數值,然後將這個數值,刷寫到左值表達式a所引用的內存區域上。此外,需要提到的是:由於a這個“東東”,程序令它向程序自己開放了某種在任意(事先無法預知)時刻或時機被刷寫進任意數據內容的權限(但須是合法的),所以,我們也以“變量”來描摹a。

與之形成對比的是,那些無法幫助C語言施行這種策略與企圖的表達式,一般稱作右值表達式,比如上列代碼中的 456 + d,它是一個右值表達式,因爲它無法幫助C語言handle任何它自己所引用的內存區域(這種對象根本不存在)。

(7)&的操作數,僅能是一個左值表達式。我們可以從“取地址操作符”這個稱呼上,理解這件事情:&操作符幫助我們獲取一個地址,即用以定位內存空間上的區域的一種特殊數據,那麼,當&無法完成這個任務的時候,比如其操作數是一個不具備被獲取地址的特性的某種東東(如register所限定而聲明出來的變量、一個右值表達式),那麼這樣的操作數就是非法的。

(8)對於&操作符幫助我們所獲得的數據(即返回值),許多人認爲是一個地址,但在真確而實際的語義中,我們不如單純而樸素地認爲:該返回值,應該是一個指向其操作數的指針,而不是一個所謂的“地址”。爲什麼這麼說呢?

我們之前提到了,C語言採用了利用左值表達式來handle該表達式所引用的對象,比如:表達式a的存在,是爲了引用某塊“屬於(對應於)”a的內存區域。C語言程序的宗旨,當然就是讀取或刷寫這塊內存區域上的數據。所以,C語言程序實現這個宗旨的層次細節,大抵是這麼一種關係:

表達式a    ---->    一塊對應於a 的內存區域(一個內存對象)  ---->  這塊內存區域上的數據(值)

C語言採用“語句(斷言)”這樣的策略,圍繞着左值表達式施行一種當時性的對內存區域上的數據的讀取或刷寫。這構成了C語言在命令式語言上的基本特徵。

猶如一個*簡單的*左值表達式可以引用一個內存對象,指針也可以引用一個內存對象。(而地址,只是用以在連續地鋪張開來的內存空間中,定位內存對象的一種機制。所以,這也可以說明:指針跟地址,並不應該視同一回事。)不過,指針相對於一個*簡單的*左值表達式來說,其引用機制是間接的。所以,爲了能利用指針實行一個間接的引用(企圖等效於直接的引用),我們就必須藉助於間接訪問操作符Indirection Operator),即讓初學者恐懼而憂煩的那個星號。

這種含有間接訪問操作符的表達式,稱作間接訪問表達式,有如:[code]*ptr[/code]一般我們可以認爲:間接訪問操作符(即星號)作用在一個指針上面,從而使得整個間接訪問表達式猶如一個*簡單的*左值表達式那樣,去handle某個內存對象。

這裏,我們可以再一次將指針與地址兩下撇開。在指針被用來實現引用機制的過程中,至少我們從尋常的代碼及其運行的直觀中,絲毫覺察不到任何的地址這種特殊數據的形態。換句話說,地址只是指針機制的某種幕後支撐,而這種支撐策略,並非是惟一的。比如,技術性地,我們也可以選擇Hash的key-value對來實現指針的引用機制。

至此,我們不難理解,間接訪問表達式,也是左值表達式。
因此,諸如[code]&*ptr[/code]和[code]&*&i[/code]這樣的表達式,都是完全合法的。

(9)&和*操作符的使用,尤其是配對使用,極容易給初學者帶來“C語言的指針是‘脫褲子放屁’”的印象。爲了破除這種誤解,必須將C語言的其他特質納入我們的視野。在這些特質中,最具有代表性的,就是C語言的函數策略。

我們觀察並思考如下代碼:[code]
void  foo(int x)  {
    x++;
}
[/code]我們的期望:以foo這個函數,爲被傳至其中的參數本身,施行自增操作。

但是,不論我們調用多少次函數foo(a),我們在調用完成之後,變量a不會有任何變化。這是爲什麼?原因出於C語言固有的函數策略。具體地說:當我們以變量a爲函數foo的參數的時候,函數foo將變量a的數據值刷寫到該函數治下的變量x,也就是說,函數foo內部的變量x只是擁有了作爲調用該函數的參數即變量a的數據值的私有副本。換句話說,函數foo內部的變量x,只是被初始化了一個值,至於這個值是從哪裏來的,站在變量x的角度,它自己是永遠無法知道的。這猶如:[code]x = a ;[/code]作爲左值的x,只是被賦值以另一個左值a所能handle的內存區域上的數據,除此之外,有關左值a自己的其他一切信息,都不會傳達給x。所以,在執行了上述語句之後,分別對左值x和a的任何更改性操作,都不會影響到對方。

除此之外,當函數完成任務之後,該函數治下的一切私有變量,都將消亡 —— 這是導致所謂的“內存泄露”現象的根本原因,後面我們會講到。

那麼,我們爲了利用一個函數,完成對被傳入該函數變量的本身的某種更改性操作,我們應當這麼做:[code]
void  bar(int *x)  {
    (*x)++;
}
[/code]我們調用函數bar的寫法,也應當有所改變,應當這樣:[code]bar(&a);[/code]此時,被傳入函數bar的參數,是一個指向變量a的指針。函數bar治下的變量x(這是一個指針變量)所擁有的,依然僅僅是前者的的一個私有副本。

函數bar在其內部,通過把間接訪問(星號)操作符作用在這個私有副本上,handle到了變量a所對應的那塊內存區域,從而,間接地在那塊區域上刷寫了新的數據。當然,變量a自已對此是一無所知的,只不過,以後當其他地方又“召喚”a的時候,a若自有記性,會心說:“唉?這個值跟原來的不一樣了嘛,一定是哪個臭小子,在背地裏拿了一個指向我的指針去間接地修改了這個值……”

(10)最後,我們考慮一種情形:[code]
void qux(void){
    int *ptr = malloc(sizeof(int)*15);        
}
[/code]函數qux內部調用了函數malloc,後者負責在當前內存中的空閒空間中分配出一塊由其參數所確定大小的存儲空間(那麼,這新分配出來的存儲空間將不再是空閒的,直到有函數free來釋放之),並將指向這塊被分配出來存儲空間的指針,賦值給變量ptr。

那麼,只要在變量ptr生存期所覆蓋的範圍內,程序都可以通過ptr來handle這塊存儲空間。問題在於,變量ptr單單屬於函數qux治下。一旦函數qux例程完結,變量ptr即消亡。此時,若程序裏沒有任何變量ptr的遺子,程序將永遠無法再handle那塊存儲空間,甚至無法釋放它。那塊存儲空間將一直處於被佔用(非空閒)狀態,且是完全是個孤島。這就造成了存儲空間的浪費,是爲“內存泄露”。

以上,僅供參考,呵呵 —— :)

P.S.: 這種技術概念性比較強的文章,還是請薛非大蝦審訂一下才是妥當。一旦通過了薛非大蝦的審覈,樓主再和盤接受就沒有什麼問題了,呵呵 ……

轉載自:http://bbs.csdn.net/topics/390334109?page=1

發佈了17 篇原創文章 · 獲贊 17 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章