指針和內存

指針和內存

2017-04-26


爲什麼要用指針

指針解決兩類軟件問題。第一,指針允許代碼的不同部分簡單地共享信息。前後複製信息可以達到同樣的效果,但是指針能夠更好地解決問題。第二,指針支持複雜關聯數據結構比如列表和二叉樹。

指針是什麼

指針存儲某一個值得引用而非值本身。

指針解引用

解引用操作在指針引用之後,目的在於獲取指針數據的值。當解引用操作正確使用時很簡單,即獲取指針數據的值。唯一限制在於指針必須有指針數據來解引用。在指針編碼中幾乎所有的bug涉及破壞這一限定。在解引用生效之前,指針必須分配指針數據。(即指針必須有所指向)

空指針

常量NULL是特殊指針,即什麼也不指。NULL爲設置沒有指針數據的指針提供了便利。解引用空指針屬於運行時錯誤。NULL和整型常數0等價,因此可以當作邏輯非使用。正式C++不再使用NULL符號常量——直接使用整型常量。Java中使用符號null。

指針分配(賦值)

兩個指針之間相互賦值會使它們指向同一個指針數據。對於某些潛在的複雜情況,指針間相互賦值會帶來很大便利,指針間的相互賦值不會改變指針數據的值,僅改變指針指向的指針數據。賦值操作同樣對NULL值有效。用NULL指針賦值操作將把NULL值從一個指針傳遞給另外一個指針。

畫圖

內存圖示是考慮指針代碼的關鍵。當你在看代碼的時候,想一想它在運行時是如何利用內存的,很快畫一張圖表達你的想法。

共享

指向同一個指針數據的兩個指針稱爲“共享”。在任何計算機語言中,兩個或多個實體共享同一個存儲結構是指針的主要優勢。指針操作僅僅是技巧,共享纔是真正目的。共享可以用來在程序不同部分提供高效通信。、

淺拷貝和深拷貝

特別的,共享可以支持兩個函數之間的通信。一個函數傳遞指向興趣值的指針給另外一個函數。二者均可獲得興趣值,但興趣值本身並未被拷貝。這類通信稱爲“淺拷貝”,因爲是一個(小的)指針被傳遞使得興趣值共享而非對興趣值的一個(大的)拷貝。接收方需要明白他們擁有淺拷貝,因此他們知道不要改變或刪除該數據,因爲它是共享的。被完全複製和發送的可選值稱爲“深拷貝”。某種程度上,深拷貝更簡單,但由於是全複製,深拷貝運行速度稍慢。

壞指針

當一個指針首次分配時,它並不對應(指向)指針數據。該指針時“未被初始化”的,或者簡單來說,是壞的。對一個壞指針進行解引用是非常嚴重的運行時錯誤。如果你很幸運,該解引用操作會立馬崩潰(Java就這麼運行)。如果你很不幸,壞指針的解引用會侵佔內存中一塊隨機區域,稍微轉變程序操作,最終導致在之後的某個不確定時間段內崩潰。在支持解引用操作之前,所有指針必須指向指針數據。在指向指針數據之前,指針爲壞指針,禁止使用。

壞指針非常普遍,事實上,每一個指針都是以壞值開始。正確的代碼用對指針數據的正確引用複寫壞值,此後指針正常工作。指針不會自動賦予合法值。

劃重點

很多語言爲了簡便省略了這關鍵一步,編程要小心。如果代碼崩了,壞指針是第一個值得懷疑的。

指針在動態語言如Perl,LISP,Java等中有些不同。當分配指針時運行時系統把指針指向NULL,每次解引用都重新檢查合法性。因此代碼依然能夠顯示指針bug,但它們將會優雅地停止在警告區,而不是像C一樣隨意崩潰。也因此,在動態語言中,定位和修復指針bug更容易。運行時檢查也是爲什麼這類語言總是會比編譯語言如C或C++慢一些的原因。

兩個層面

指針層面和指針數據層面,兩個層面均需要初始化和連接以工作。

  1. 分配指針
  2. 分配指針數據
  3. 將指針指向指針數據

指針類型語法

int* 類型: pointer to int
float* 類型: pointer to float
struct fraction* 類型: pointer to struct fraction
struct fraction** 類型: pointer to struct fraction *

指針變量

變量聲明給新變量類型和存儲空間來存儲值。聲明不會將指針指向指針數據,即聲明的是壞指針。

&操作符(取地址操作符)

有很多種方法獲取指針數據的引用,最簡單的就是&取地址操作符。

使用&有可能編譯通過但在運行時出錯。

*操作符(解引用)

指針必須對應指針數據,否則是一個運行時錯誤。

分配指針不會自動賦值,需要手動將一個具體值的引用賦值給指針,這是經常忘記的一個獨立操作。

指針在機器中是怎樣執行的

簡而言之,內存中的每一塊區域都有如1000或者20452這樣的數字地址。指針即地址。解引用操作即看一下指針中存儲的地址,然後到相應的地址中將指針數據提取出來。NULL值通常就是數字地址0.計算機從不給0地址賦值,因此該地址可被用來表示NULL。一個壞指針事實上就是包含了隨機地址的指針——就像是未被初始化的int變量以隨機int型變量開始一樣。該指針還未被分配具體指針數據的引用。這也是爲什麼用壞指針進行解引用操作如此不可預測。

詞條“引用”

“引用”和“指針”意思相近,區別在於,”引用“往往用作討論指針問題,不針對某一語言或實現。“指針”暗指C/C++作爲地址的指針實現。

爲什麼壞指針如此普遍

思維定勢。簡單變量不需要額外設置,聲明之後即可直接用,比如int,char,struct fraction等等。不幸的是,指針並不是簡單變量,在使用之前需要額外初始化。

第二部分——局部存儲

內存的分配和釋放

變量代表計算機內存的存儲空間。並不是程序中的每個變量都有固定分配的內存。術語講,當變量擁有一塊內存存放它的值時,稱爲分配。當系統從變量回收內存空間時,稱爲釋放,此時它不再有存儲值的空間。對於變量而言,從分配到什邡的這段時間稱爲變量的生命週期。

內存最普遍的錯誤使用便是使用已釋放的變量。對於局部變量,現代語言自動防止該錯誤。對於指針,程序員必須確認分配處理正確。

局部存儲

局部變量是最普遍的變量。

變量被稱爲是“局部”即表示它們的生命週期和函數綁在一起。隨函數生而生,死而死。

參數和局部變量的唯一區別在於,參數傳自調用者而局部變量開始於隨機初始化值。

局部變量的優點

  1. 方便——局部變量滿足快捷使用,函數通常需要臨時內存,僅需要在函數計算範圍內有效。局部變量便捷地提供了這類臨時、獨立內存。
  2. 有效——相比於其他內存使用技巧,局部變量非常有效。分配和釋放時效性高(快速),空間效率高,可回收。
  3. 局部複製——局部參數基本都是從調用方拷貝而來,這也通常稱爲“值傳遞”。參數是從調用者賦值拷貝而來的局部變量。調用者不和指針那樣“共享”參數值,而是得到自己的副本。

局部變量的缺點

生命週期短——可以用堆解決;

限制性通信——由於局部變量是調用參數的副本,它們不提供從被調用者到調用者的通信,這便是“獨立性”優點的負面影響

“局部”的同義詞

局部變量也被稱爲“自動”變量,因爲它們的分配和釋放作爲函數機制的一部分。局部變量有時也被稱作“棧”變量,因爲從底層來講,各類語言通常通過內存棧實現局部變量。

// TAB -- The Ampersand Bug function
// Returns a pointer to an int
int* TAB() {
int temp;
return(&temp); // return a pointer to the local int
}
void Victim() {
int* ptr;
ptr = TAB();
*ptr = 42; // Runtime error! The pointee was local to TAB
}

局部變量總結

要解決的問題

一個函數如何將數據反饋到它的調用方?

一個函數如何在少些生命週期限制情況下分配獨立空間

函數調用棧如何工作

引用參數

在最簡單的“值傳遞”或者“之參數”方法中,每個函數有獨立的局部內存,函數發生調用時,參數從調用者到被調用者拷貝。但是從被調用者到調用者如何通信?在被調用函數底部使用“return”拷貝結果將其傳回調用者,這種方法對一些簡單的情況適用,但是一些複雜的情況卻不行。有時來回複製值並不可行。“引用傳遞”參數解決了所有的問題。

“興趣值”指調用者和被調用者之間想要傳遞的值。引用參數傳遞興趣值的指針而非興趣值的副本。這種方法利用了指針的共享性,因此調用方和被調用方可以共享興趣值。

語法

C語言中,引用參數的語法即對指針操作

A person with one watch always knows what time it is. A person with two watches is never sure.

避免複製。

關於&Bug

使用&從調用者到被調用者傳遞指針變量到局部存儲沒問題,反過來,從被調用者到調用者,便會出現&bug,因爲函數一旦退出,所佔內存便會被釋放掉,指針隨之失效。

**的情況

要是興趣值已經是一個指針,比如int* 或者 struct fraction*?這樣會改變設置引用參數的規則嗎?並不會。引用參數仍然是一個指向興趣值的指針,幾遍興趣值本身便是指針。假設興趣值是int .這意味着興趣值本身便是int 值,是調用者和被調用者所共享的。所以引用參數應該爲int**.對引用參數的單個 解引用操作和之前一樣可以獲得興趣值。兩個( *)指針參數在鏈表中很普遍。

堆內存

堆內存也稱爲動態內存,是對局部棧內存的替代。局部內存太自動化,堆內存不同,程序員爲“塊”內存申請特定大小的內存分配,這個塊會一直存在,直到程序員明確申請釋放空間。沒有什麼是自動完成的。因此程序員堆內存有更大的支配權,但也有了更大的責任,因爲內存現在必須主動管理。

堆內存的優點在於:

生命週期——由於程序員現在能夠控制內存的分配和釋放,在內存中建立數據結構,並將數據結構返回給調用者。這在局部內存中是不可能實現的,因爲函數退出時內存被自動釋放。

大小——分配內存的大小可以用更多細節來控制。比如,字符串緩衝可以在運行時分配,可以恰好是要容納字符串的大小。而在局部內存中,代碼更傾向於儘可能大的緩衝大小以保證最好的效果。

堆內存的缺點在於:

工作量變大——堆分配需要在代碼中作出詳盡的安排,工作量變大;

bug變多——由於現在內存分配需要手動完成,有可能的誤操作會導致內存bug。局部內存有約束性,但至少永遠不會發生錯誤。

儘管如此,很多問題只能用堆內存解決。在有垃圾回收器的編程語言中,比如Perl,LISP,或者Java,上邊的缺點很大程度上被忽略。垃圾回收器接管了很多堆管理的責任,在運行時花費一些額外的時間來處理。

堆像什麼

堆是內存中可供程序使用的很大一塊內存區域。程序能夠在申請內存區域或內存塊。爲了能分配某大小的內存塊,程序通過調用堆分配函數作出明確請求。該分配函數在堆中預留出請求大小的內存塊並返回指向該內存塊的指針。假設一個程序爲了存儲三張獨立GIF圖像在堆中作出三次內存分配請求,每張圖1024 byte.三次請求過後,內存可能是這樣:

heap

每次分配請求在堆中爲請求大小分配連續的區域,爲程序返回指向該區域的指針,塊經常扮演指針數據的角色,程序總是通過指針堆堆塊進行操作。堆塊指針有時被稱作“基地址”指針,因爲按照規定,它們指向塊的基部(最小的地址字節)

上例中,這三個內存塊自堆的底部開始連續分配,每個塊都是按請求1024字節的大小。事實上,堆管理可以在堆中任意位置開始分配,只要塊沒有重疊,至少是連續的申請大小。特殊情況,一些堆區域已經分配給了程序,因此它們是“使用中”。堆管理滿足每一個來自分配要求自由內存池,更新私有數據結構用一級庫堆中的哪些區域正在使用中。

釋放

當程序結束使用內存塊時,需要給堆管理作出明確的釋放請求:程序已結束對塊的使用。堆管理更新它的私有數據結構來顯示被佔用的那片區域已經可以重新使用了。

釋放掉之後,指針繼續指向已被釋放的塊。程序不可獲取該處的指針數據。指針還在,但是不能用了。有時代碼會設置指針指向NULL,一旦內存釋放,明確已經不合法。

編程堆

在大多數編程語言中,編程堆都看起來非常相似,基本特點是:

堆是一片可供程序分配內存區域或者內存塊的內存區域;

有些“堆管理”庫代碼爲程序管理堆。程序員向堆管理作出請求,堆管理反過來管理堆的內部構件。在C中,堆由ANSI庫中的 malloc(), free()和 realloc()函數管理。

堆管理使用自己的私有數據結構跟蹤堆中的哪些塊能用了,哪些塊正在用,這些塊有多大。最初,所有的堆都是可用的。

堆可能是固定的大小(通常的構想),或者看起來是固定大小,事實上背後有虛擬內存在支撐。不管哪種情況,堆都可能變滿如果它的內存都被分配出去,此時它不能響應新的分配請求。分配函數以某種方式將此時的運行時環境傳遞給程序——通常情況下通過返回NULL指針或者拋出一個具體的運行時異常。

分配函數在堆中請求某一具體大小的塊。堆管理選擇一塊內存區域滿足該請求,在它自己的數據結構中標記該區域正在使用,返回指向該堆塊的指針。這個塊保證預留給調用函數單獨使用——堆不會將同一塊內存區域分配給其他的調用函數。該塊不會在堆內周圍移動——一旦分配,地址和大小便固定了。通常,一個塊被分配,它的內容是隨機的,新的所有者有責任使這塊內存有意義。有時,在內存分配函數上有變量設置該塊爲全0;

釋放函數是分配函數的相反面。程序作出單一釋放調用以返回一塊內存到堆空閒區以便重新利用。每一個塊應該只釋放一次。釋放函數的指針須和分配函數的一致,即爲分配函數返回的指針,而不是指向該區域的任何指針。釋放之後,程序必須將該指針作爲壞指針處理,不允許獲取指針數據。

C實現

C語言中,申請堆的庫函數是malloc()和free()。這些函數的原型在< stdlib.h >中。儘管不同語言語法不同,malloc()和free()在所有語言中的角色基本一致。

The C operator sizeof() is a convenient way to compute the size in bytes of a type —sizeof(int) for an int pointee,sizeof(struct fraction) for a struct fraction pointee.

傳遞給free()的指針必須恰好是先前由malloc分配的,而不是一個指向塊的某個地方的指針。用錯誤的指針調用free是常見的崩潰錯誤。對free()的調用不需要提供堆塊的大小——堆管理會在它的私有數據結構中記錄。如果程序正確釋放所有分配的內存,之後每個malloc()調用之後都會恰好對應一個free()調用。實際問題是,對於一個程序來講,釋放每個分配的塊不總是必要的,見下方“內存泄漏”。

heap字符串觀察結果

StringCopy()包含堆內存兩個重要的優點:

大小——StringCopy可以在運行時指定用來存儲字符串的塊的大小,在調用malloc()函數時。局部內存不能這樣做,因爲它的大小在編譯時已經被指定好了。

The call to sizeof(char) is not really necessary, since the size of char is 1 by definition.

生命週期——StringCopy()分配塊,但之後將所有權傳遞給調用者。如果不調用free(),塊將一直存在,計時函數退出。局部變量做不到。調用者需要仔細看好內存的釋放當字符串使用完成時。

內存泄漏

如果內存被堆分配卻沒有釋放,會發生什麼?一個分配了卻忘了釋放內存的程序可能有也可能沒有嚴重的問題。結果會是:一直在申請,直到沒有可用的內存空間。對於一個正在運行,計算的程序,之後馬上退出,內存泄漏通常不是所要關心的問題。這樣“一次性”的程序大多數情況下可以忽略掉所有的釋放依然能夠很好地運行。內存泄漏通常發生在一個不確定結束時間的程序上。這種情況下,內存泄漏會慢慢充滿堆直到分配申請不能得到滿足,程序停止工作或者直接崩潰。許多商用程序存在內存泄露的問題,因此當運行了很久之後,或者有很大的數據集,充滿堆進而崩潰。通常情況下針對滿堆的錯誤檢測和預防代碼沒有很好地測試,很多是由於跑幾次程序這種情況很少遇到——也就是爲什麼滿堆通常導致直接崩潰而不是友好錯誤信息。許多編譯器有“堆調試”功能,將調試代碼加進去以追蹤每一次分配和釋放。當分配沒有釋放,就是一次泄露,堆調試會幫你找到它們。

所有權

StringCopy()分配堆塊,但是它沒有釋放。這也是爲什麼調用者可以使用新字符串的原因。然而,這也意味着釋放操作需要程序員完成,StringCopy()不管。也因此在StringCopy()的說明中詳細說明調用者擁有塊的所有權。每一個內存塊有一個確切的所有者負責釋放。其他實體可以擁有指針,但是他們僅僅是共享。只有唯一的所有者。好的文檔總是記得討論一個函數期望應用到它的參數還是值的所有權規則。或者這麼說,文檔中頻繁的錯誤是忘了提及,一個參數或者返回值的歸屬法則是什麼。這是內存錯誤和泄露的一個原因。

所有權模型

所有權的兩個共同模式是:

調用者所有權——調用者擁有自己的內存。出於共享的目的,它可能給被調用者傳遞一個指針,但是調用者保存歸屬權。當被調用者運行時,被調用者可以獲取到東西,分配釋放自己的內存,但不應該破壞調用者的內存。

被調用者分配和返回——被調用者分配一些內存然後把它返回給調用者。這通常發生在被調用者計算結果需要新的內存存儲和表達。新的內存被傳遞給調用者,因此他們能看到結果,調用者必須接管內存的所有權。這是StringCopy()函數說明的模式。

堆內存總結

堆內存爲程序員提供了更大的控制權——內存塊可以申請任意大小,保留分配直到明確釋放。堆內存可以被傳遞迴調用者因爲函數退出後並未自動釋放,這可以用來建立鏈式結構比如鏈表和二叉樹。堆內存的缺點在於程序必須明確堆內存的分配和釋放。堆內存不按局部內存那樣自動處理。

/*
 Given a C string, return a heap allocated copy of the string.
 Allocate a block in the heap of the appropriate size,
 copies the string into the block, and returns a pointer to the block.
 The caller takes over ownership of the block and is responsible
 for freeing it.
*/
char* StringCopy(const char* string) {
char* newString;
int len;
len = strlen(string) + 1; // +1 to account for the '\0'
newString = malloc(sizeof(char)*len); // elem-size * number-of-elements
assert(newString != NULL); // simplistic error check (a good habit)
strcpy(newString, string); // copy the passed in string to the block
return(newString); // return a ptr to the block
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章