指標和記憶體
2017-04-26 為什麼要用指標
指標解決兩類軟體問題。第一,指標允許代碼的不同部分簡單地共用資訊。前後複製資訊可以達到同樣的效果,但是指標能夠更好地解決問題。第二,指標支援複雜關聯資料結構比如列表和二叉樹。 指標是什麼
指標儲存某一個值得引用而非值本身。 指標解引用
解引用操作在指標引用之後,目的在於擷取指標資料的值。當解引用操作正確使用時很簡單,即擷取指標資料的值。唯一限制在於指標必須有指標資料來解引用。在指標編碼中幾乎所有的bug涉及破壞這一限定。在解引用生效之前,指標必須分配指標資料。(即指標必須有所指向) null 指標
常量NULL是特殊指標,即什麼也不指。NULL為設定沒有指標資料的指標提供了便利。解引用null 指標屬於執行階段錯誤。NULL和整型常數0等價,因此可以當作邏輯非使用。正式C++不再使用NULL符號常量——直接使用整型常量。Java中使用符號null。 指標分配(賦值)
兩個指標之間相互賦值會使它們指向同一個指標資料。對於某些潛在的複雜情況,指標間相互賦值會帶來很大便利,指標間的相互賦值不會改變指標資料的值,僅改變指標指向的指標資料。賦值操作同樣對NULL值有效。用NULL指標賦值操作將把NULL值從一個指標傳遞給另外一個指標。 畫圖
記憶體配置圖示是考慮指標代碼的關鍵。當你在看代碼的時候,想一想它在運行時是如何利用記憶體的,很快畫一張圖表達你的想法。 共用
指向同一個指標資料的兩個指標稱為“共用”。在任何電腦語言中,兩個或多個實體共用同一個儲存結構是指標的主要優勢。指標操作僅僅是技巧,共用才是真正目的。共用可以用來在程式不同部分提供高效通訊。、 淺拷貝和深拷貝
特別的,共用可以支援兩個函數之間的通訊。一個函數傳遞指向興趣值的指標給另外一個函數。二者均可獲得興趣值,但興趣值本身並未被拷貝。這類通訊稱為“淺拷貝”,因為是一個(小的)指標被傳遞使得興趣值共用而非對興趣值的一個(大的)拷貝。接收方需要明白他們擁有淺拷貝,因此他們知道不要改變或刪除該資料,因為它是共用的。被完全複製和發送的可選值稱為“深拷貝”。某種程度上,深拷貝更簡單,但由於是全複製,深拷貝運行速度稍慢。 壞指標
當一個指標首次分配時,它並不對應(指向)指標資料。該指標時“未被初始化”的,或者簡單來說,是壞的。對一個壞指標進行解引用是非常嚴重的執行階段錯誤。如果你很幸運,該解引用操作會立馬崩潰(Java就這麼運行)。如果你很不幸,壞指標的解引用會侵佔記憶體中一塊隨機地區,稍微轉變程式操作,最終導致在之後的某個不確定時間段內崩潰。在支援解引用操作之前,所有指標必須指向指標資料。在指向指標資料之前,指標為壞指標,禁止使用。
壞指標非常普遍,事實上,每一個指標都是以壞值開始。正確的代碼用對指標資料的正確引用複寫壞值,此後指標正常工作。指標不會自動賦予合法值。
劃重點
很多語言為了簡便省略了這關鍵一步,編程要小心。如果代碼崩了,壞指標是第一個值得懷疑的。
指標在動態語言如Perl,LISP,Java等中有些不同。當分配指標時運行時系統把指標指向NULL,每次解引用都重新檢查合法性。因此代碼依然能夠顯示指標bug,但它們將會優雅地停止在警告區,而不是像C一樣隨意崩潰。也因此,在動態語言中,定位和修複指標bug更容易。運行時檢查也是為什麼這類語言總是會比編譯語言如C或C++慢一些的原因。 兩個層面
指標層面和指標資料層面,兩個層面均需要初始化和串連以工作。 分配指標 分配指標資料 將指標指向指標資料 指標類型文法
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等等。不幸的是,指標並不是簡單變數,在使用之前需要額外初始化。 第二部分——局部儲存 記憶體的分配和釋放
變數代表電腦記憶體的儲存空間。並不是程式中的每個變數都有固定分配的記憶體。術語講,當變數擁有一塊記憶體存放它的值時,稱為分配。當系統從變數回收記憶體空間時,稱為釋放,此時它不再有儲存值的空間。對於變數而言,從分配到什邡的這段時間稱為變數的生命週期。
記憶體最普遍的錯誤使用便是使用已釋放的變數。對於局部變數,現代語言自動防止該錯誤。對於指標,程式員必須確認分配處理正確。 局部儲存
局部變數是最普遍的變數。
變數被稱為是“局部”即表示它們的生命週期和函數綁在一起。隨函數生而生,死而死。
參數和局部變數的唯一區別在於,參數傳自調用者而局部變數開始於隨機初始化值。 局部變數的優點 方便——局部變數滿足快捷使用,函數通常需要臨時記憶體,僅需要在Function Compute範圍內有效。局部變數便捷地提供了這類臨時、獨立記憶體。 有效——相比於其他記憶體使用量技巧,局部變數非常有效。分配和釋放時效性高(快速),空間效率高,可回收。 局部複製——局部參數基本都是從調用方拷貝而來,這也通常稱為“值傳遞”。參數是從調用者賦值拷貝而來的局部變數。調用者不和指標那樣“共用”參數值,而是得到自己的副本。 局部變數的缺點
生命週期短——可以用堆解決;
限制性通訊——由於局部變數是調用參數的副本,它們不提供從被調用者到調用者的通訊,這便是“獨立性”優點的負面影響 “局部”的同義字
局部變數也被稱為“自動”變數,因為它們的分配和釋放作為函數機制的一部分。局部變數有時也被稱作“棧”變數,因為從底層來講,各類語言通常通過記憶體棧實現局部變數。
// TAB -- The Ampersand Bug function// Returns a pointer to an intint* 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.三次請求過後,記憶體可能是這樣:
每次分配請求在堆中為請求大小分配連續的地區,為程式返回指向該地區的指標,塊經常扮演指標資料的角色,程式總是通過指標堆堆塊進行操作。堆塊指標有時被稱作“基地址”指標,因為按照規定,它們指向塊的基部(最小的地址位元組)
上例中,這三個記憶體塊自堆的底部開始連續分配,每個塊都是按請求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-elementsassert(newString != NULL); // simplistic error check (a good habit)strcpy(newString, string); // copy the passed in string to the blockreturn(newString); // return a ptr to the block}