題記:寫這篇博客要主是加深自己對錯誤內存的認識和總結實現算法時的一些驗經和訓教,如果有錯誤請指出,萬分感謝。
? ? ? ? ?對C/C++程序員來講,內存管理是個不小的挑戰,絕對值得慎之又慎,否則讓由上萬行代碼構成的模塊跑起來后才出現內存崩潰,是很讓人痛苦的。因為崩潰的位置在時間和空間上,通常是在距真正的錯誤源一段距離以后才表現出來。頭幾天線上模塊因堆內存寫越界1個字節引起各種詭異崩潰,定位問題過程當中的折騰仍歷歷在目,今天讀到《深刻理解計算機系統》第9章-虛擬存儲器,發明書中總結了C程序中常見的內存操縱有關的10種典型編程錯誤,總結的比擬全面。故作為筆記,記載于此。
????1. 間接引用無效指針
? ? ? ? 進程虛擬地址空間的某些地址范圍可能沒有映射到任何有意義的數據,如果我們試圖間接引用一個指向這些地址的指針,則操縱系統會以Segment Fault終止進程。而且,虛擬存儲器的某些區域是只讀的(如.text或.rodata),試圖寫這些區域會以掩護異常中止當前進程。
? ? ? ? 如從stdin讀取一個int變量時,scanf("%d", &val)是準確用法,若誤寫為scanf("%d", val)時,val的值會被解釋為一個地址,并試圖向該地址寫數據。在最好的情況下,進程立即異常中止。在最壞的情況下,val的值恰好對應于虛擬存儲器的某個正當的具有讀/寫權限的內存區域,于是該內存單元會被改寫,而這通常會在相當長的一段時間后形成災難性的、令人困惑的后果。
????2. 讀未初始化的存儲器
? ? ? ? C語言的malloc并不負責初始化申請到的內存區域,因此,常見的錯誤是假設堆存儲器被初始化為0,例如:
int * foo(int **A, int *x, int n){int i, j;int * y = (int *)Malloc(n * sizeof(int));for(i = 0; i < n; i++) {for(j = 0; j < n; j++){y[i] += A[i][j] * x[j];}}return y;}
? ? ? ? ? ?上述代碼中,錯誤地假設了y被初始化為0。準確的實現方式是顯式將y[i]置為0或者應用calloc。
????3. 棧緩沖區溢出
? ? ? ? ?例如:
char buf[5];sprintf(buf, "%s", "hello world");
? ? ? ? ? ?
????上面的代碼致使棧緩沖區溢出,安全的做法是:1)根據需求定義適合的buffer;2)采取snprintf(buf, sizeof(buf), "%s", "hello world")來實時截斷。
????4. 誤以為指針與其指向的對象是雷同巨細的
? ? ? ? 例如:
int **makeArray(int n, int m){int i;int **A = (int **)Malloc(n*sizeof(int)); // 這里錯誤地以為int *與int兩種變量類型具有雷同的sizefor(i = 0; i < n; i++) {A[i] = (int *)Malloc(m * sizeof(int));}return A;}
????? ? ? ? 上述代碼目的是創立一個由n個指針構成的數組,每一個指針均指向一個包含m個int的數組,但誤將sizeof(int *)寫成sizeof(int)。這段代碼只有在int和int *的size雷同的機器上運行良好。如果在像Core i7這樣的機器上運行這段代碼,由于指針變量的size大于sizeof(int),則會引發代碼中的for循環寫越界。因為這些字中的一個很多是已分配塊的邊界標記腳部,所以我們可能不會立即發明這個錯誤,直到進程運行很久釋放這個內存塊時,此時,分配器中的合并代碼會戲劇性地失敗,而沒有任何明顯的原因。這是"在遠處起作用"(action at distance)的一個隱秘示例,這類"在遠處起作用"是與存儲器有關的編程錯誤的典型情況。
????5. 形成錯位錯誤
? ? ? ? ?錯位(Off-by-one)錯誤是另一種常見的覆蓋錯誤來源:
全部世界,因為有了陽光,城市有了生機;細小心靈,因為有了陽光,內心有了舒暢。明媚的金黃色,樹叢間小影成像在葉片上泛有的點點破碎似的金燦,海面上直射反映留有的隨波浪層層翻滾的碎片,為這大自然創造了美景,惹人醉的溫馨之感,濃濃暖意中夾雜著的明朗與柔情,讓雨過天晴后久違陽光的心靈重新得到了滋潤!
int ** makeArray(int n, int m){int i;int **A = (int **)Malloc(n * sizeof(int *));for(i = 0; i <= n; i++) {A[i] = (int *)Malloc(m * sizeof(int));}return A;}
? ? ? ? ?
????很明顯,for循環次數分歧預期,致使寫越界。榮幸的話,進程會立即崩潰;不幸的話,運行很長時間才拋出各種詭異問題。
????6. 引用指針,而不是它所指向的對象
? ? ? ? 如果不注意C操縱符的優先級和結合性,就會錯誤地操縱指針,而不是指針所指向的對象。
? ? ? ? 比如上面的函數,其目的是刪除一個有*size項的二叉堆里的第一項,然后對剩下的*size-1項重建堆:
int * binheapDelete(int **binheap, int *size){int *packet = binheap[0];binheap[0] = binheap[*size - 1];*size--; // 此處應該為(*size)--heapify(binheap, *size, 0);return (packet);}
????? ? ? ?上述代碼中,由于--和*優先級雷同,從右向左結合,所以*size--其實增加的是指針自己的值,而非其指向的整數的值。因此,服膺:當你對優先級和結合性有疑問時,就應該應用括號。
????7. 誤解指針運算
? ? ? ? 在C/C++中,指針的算術操縱是以它們指向的對象的巨細為單位來進行的。例如上面函數的功能是掃描一個int的數組,并返回一個指針,指向val的初次出現:
int * search(int *p, int val){while(*p && *p != val) {p += sizeof(int); // 此處應該為p++,否則p += 4會致使大部分元素被跳過}}
????8. 引用不存在的變量
? ? ? ? ? ?
????C/C++新手不理解棧的規矩時,可能會引用不再正當的當地變量,例如:
int * stackref(){int val;return &val;}
????? ? ? ? 函數返回的指針(假設為p)指向棧中的局部變量,但該變量在函數返回后隨著stackref棧幀的銷毀已經不再有效。也即:盡管函數返回的指針p仍然指向一個正當的存儲器地址,但它已經不再指向一個正當的變量了。當程序后續調用其它函數時,存儲器將重用剛才銷毀棧幀處的存儲器區域。再后來,如果程序分配某個值給*p,那么它可能實際上正在修改另一個函數棧幀中的數據,從而潛在地帶來災難性的、令人困惑的后果。
????9. 引用閑暇堆塊中的數據
? ? ? ? 典型的錯誤為:引用已經被釋放了的堆塊中的數據,例如:
int * heapref(int n, int m){int i;int *x, *y;x = (int *)Malloc(n * sizeof(int));/* 各種操縱 */free(x);y = (int *)Malloc(m * sizeof(int));for(i = 0; i < m; i++) {y[i] = x[i]++; // 此處的x之前已經被釋放了!}}
????10. 內存泄露
? ? ? ?內存泄露是遲緩、隱性的殺手,當程序員忘記釋放已分配塊時會產生這類問題,例如:
void leak(int n){int *x = (int *)Malloc(n * sizeof(int));return;}
????? ? ? ?如果leak在程序全部生命周期內只調用數次,則問題還不是很嚴峻(但還是會浪費存儲器空間),因為隨著進程結束,操縱系統會回收這些內存空間。但如果leak()被經常調用,那就會產生嚴峻的內存泄露,最壞的情況下,會占用全部虛擬地址空間。對于像守護進程和服務器這樣的程序來講,內存泄露是嚴峻的bug,必須加以看重。
????【參考資料】
《深刻理解計算機系統》第9章 — 虛擬存儲器
????============== EOF ==================
????
文章結束給大家分享下程序員的一些笑話語錄: 一個合格的程序員是不會寫出 諸如 “摧毀地球” 這樣的程序的,他們會寫一個函數叫 “摧毀行星”而把地球當一個參數傳進去。
--------------------------------- 原創文章 By
錯誤和內存
---------------------------------