引言
對于任何使用 C 語言的人,如果問他們 C 語言的最大煩惱是什么,其中許多人可能會回答說是指標和記憶體泄漏,這些的確是消耗了開發人員大多數除錯時間的事項,
指標和記憶體泄漏對某些開發人員來說似乎令人畏懼,但是一旦您了解了指標及其關聯記憶體操作的基礎,它們就是您在 C 語言中擁有的最強大工具,
本文將與您分享開發人員在開始使用指標來編程前應該知道的秘密,本文內容包括:
1. 導致記憶體破壞的指標操作型別
2. 在使用動態記憶體分配時必須考慮的檢查點
3. 導致記憶體泄漏的場景
如果您預先知道什么地方可能出錯,那么您就能夠小心避免陷阱,并消除大多數與指標和記憶體相關的問題,

啥是記憶體泄漏
記憶體泄露的解釋如下:
在計算機科學中,記憶體泄漏指由于疏忽或錯誤造成程式未能釋放已經不再使用的記憶體,記憶體泄漏并非指記憶體在物理上的消失,而是應用程式分配某段記憶體后,由于設計錯誤,導致在釋放該段記憶體之前就失去了對該段記憶體的控制,從而造成了記憶體的浪費,
在C++中出現記憶體泄露的主要原因就是程式猿在申請了記憶體后(malloc(), new),沒有及時釋放沒用的記憶體空間,甚至消滅了指標導致該區域記憶體空間根本無法釋放,
知道了出現記憶體泄露的原因就能知道如何應對記憶體泄露,即:不用了的記憶體空間記得釋放,不釋放留著過年哇!
? 記憶體泄漏可能會導致嚴重的后果:
● 程式運行后,隨著時間占用了更多的記憶體,最后無記憶體可用而崩潰;
● 程式消耗了大量的記憶體,導致其他程式無法正常使用;
● 程式消耗了大量記憶體,導致消費者選用了別人的程式而不是你的;
● 經常做出記憶體泄露bug的程式猿被公司開出而貧困潦倒,
? 如何知道自己的程式存在記憶體泄露?
根據記憶體泄露的原因及其惡劣的后果,我們可以通過其主要表現來發現程式是否存在記憶體泄漏:程式長時間運行后記憶體占用率一直不斷的緩慢的上升,而實際上在你的邏輯中并沒有這么多的記憶體需求,
? 如何定位到泄露點呢?
根據原理,我們可以先review自己的代碼,利用"查找"功能,查詢new與delete,看看記憶體的申請與釋放是不是成對釋放的,這使你迅速發現一些邏輯較為簡單的記憶體泄露情況,
如果依舊發生記憶體泄露,可以通過記錄申請與釋放的物件數目是否一致來判斷,在類中追加一個靜態變數 static int count;在建構式中執行count++;在解構式中執行count--;,通過在程式結束前將所有類析構,之后輸出靜態變數,看count的值是否為0,如果為0,則問題并非出現在該處,如果不為0,則是該型別物件沒有完全釋放,
檢查類中申請的空間是否完全釋放,尤其是存在繼承父類的情況,看看子類中是否呼叫了父類的解構式,有可能會因為子類析構時沒有是否父類中申請的記憶體空間,
對于函式中申請的臨時空間,認真檢查,是否存在提前跳出函式的地方沒有釋放記憶體,

什么地方可能出錯?
有幾種問題場景可能會出現,從而可能在完成生成后導致問題,在處理指標時,您可以使用本文中的資訊來避免許多問題,
未初始化的記憶體
在本例中,p 已被分配了 10 個位元組,這 10 個位元組可能包含垃圾資料,如圖所示,

char *p = malloc ( 10 );
垃圾資料
如果在對這個 p 賦值前,某個代碼段嘗試訪問它,則可能會獲得垃圾值,您的程式可能具有不可預測的行為,p 可能具有您的程式從未曾預料到的值,
良好的習慣是始終結合使用 memset 和 malloc分配記憶體,或者使用 calloc,
char *p = malloc (10);
memset(p,’\0’,10);
現在,即使同一個代碼段嘗試在對 p 賦值前訪問它,該代碼段也能正確處理 Null 值(在理想情況下應具有的值),然后將具有正確的行為,
記憶體覆寫
由于 p 已被分配了 10 個位元組,如果某個代碼片段嘗試向 p 寫入一個 11 位元組的值,則該操作將在不告訴您的情況下自動從其他某個位置“吃掉”一個位元組,讓我們假設指標 q 表示該記憶體,

原始 q 內容

覆寫后的 q 內容
結果,指標 q 將具有從未預料到的內容,即使您的模塊編碼得足夠好,也可能由于某個共存模塊執行某些記憶體操作而具有不正確的行為,下面的示例代碼片段也可以說明這種場景,
char *name = (char *) malloc(11);
// Assign some value to name
memcpy ( p,name,11); // Problem begins here
在本例中,memcpy 操作嘗試將 11 個位元組寫到 p,而后者僅被分配了 10 個位元組,
作為良好的實踐,每當向指標寫入值時,都要確保對可用位元組數和所寫入的位元組數進行交叉核對,一般情況下,memcpy 函式將是用于此目的的檢查點,
記憶體讀取越界
記憶體讀取越界 (overread) 是指所讀取的位元組數多于它們應有的位元組數,這個問題并不太嚴重,在此就不再詳述了,下面的代碼提供了一個示例,
char*ptr = (char*)malloc(10);charname[20] ;memcpy( name,ptr,20);// Problem begins here
在本例中,memcpy 操作嘗試從 ptr 讀取 20 個位元組,但是后者僅被分配了 10 個位元組,這還會導致不希望的輸出,
記憶體泄漏
記憶體泄漏可能真正令人討厭,下面的串列描述了一些導致記憶體泄漏的場景,
重新賦值
我將使用一個示例來說明重新賦值問題,
char *memoryArea = malloc(10);
char *newArea = malloc(10);
這向如下面的圖所示的記憶體位置賦值,
記憶體位置
memoryArea 和 newArea 分別被分配了 10 個位元組,它們各自的內容如圖 4 所示,如果某人執行如下所示的陳述句(指標重新賦值)……
memoryArea= newArea;
則它肯定會在該模塊開發的后續階段給您帶來麻煩,
在上面的代碼陳述句中,開發人員將 memoryArea 指標賦值給 newArea 指標,結果,memoryArea 以前所指向的記憶體位置變成了孤立的;
如下面的圖所示,它無法釋放,因為沒有指向該位置的參考,這會導致 10 個位元組的記憶體泄漏,
記憶體泄漏
在對指標賦值前,請確保記憶體位置不會變為孤立的,
首先釋放父塊
假設有一個指標 memoryArea,它指向一個 10 位元組的記憶體位置,該記憶體位置的第三個位元組又指向某個動態分配的 10 位元組的記憶體位置,如圖 6 所示,

動態分配的記憶體
free(memoryArea)
如果通過呼叫 free 來釋放了 memoryArea,則 newArea 指標也會因此而變得無效,newArea 以前所指向的記憶體位置無法釋放,因為已經沒有指向該位置的指標,
換句話說,newArea 所指向的記憶體位置變為了孤立的,從而導致了記憶體泄漏,
每當釋放結構化的元素,而該元素又包含指向動態分配的記憶體位置的指標時,應首先遍歷子記憶體位置(在此例中為 newArea),并從那里開始釋放,然后再遍歷回父節點,
這里的正確實作應該為:
free( memoryArea->newArea);
free(memoryArea);
回傳值的不正確處理
有時,某些函式會回傳對動態分配的記憶體的參考,跟蹤該記憶體位置并正確地處理它就成為了 calling 函式的職責,
char *func ( )
{
return malloc(20); // make sure to memset this location to ‘\0’…
}
void callingFunc ( )
{
func ( ); // Problem lies here
}
在上面的示例中,callingFunc() 函式中對 func() 函式的呼叫未處理該記憶體位置的回傳地址,結果,func() 函式所分配的 20 個位元組的塊就丟失了,并導致了記憶體泄漏,

歸還您所獲得的
在開發組件時,可能存在大量的動態記憶體分配,您可能會忘了跟蹤所有指標(指向這些記憶體位置),并且某些記憶體段沒有釋放,還保持分配給該程式,
始終要跟蹤所有記憶體分配,并在任何適當的時候釋放它們,事實上,可以開發某種機制來跟蹤這些分配,比如在鏈表節點本身中保留一個計數器(但您還必須考慮該機制的額外開銷),
訪問空指標
訪問空指標是非常危險的,因為它可能使您的程式崩潰,始終要確保您不是 在訪問空指標,
沒有躲過的坑--指標(記憶體泄露)
C++被人罵娘最多的就是指標,
夜深人靜的時候,拿出幾個使用指標容易出現的坑兒,可能我的語言描述有些讓人費勁,盡量用代碼說話,
通過指向類的NULL指標呼叫類的成員函式
試圖用一個null指標呼叫類的成員函式,導致崩潰:
#include <iostream>
using namespace std;
class A{int value;public:void dumb() const {cout << "dumb()\n";}void set(int x) {cout << "set()\n"; value=https://www.cnblogs.com/huya-edu/archive/2020/10/26/x;}int get() const {cout <<"get()\n"; return value;}};
int main(){A *pA1 = new A;A *pA2 = NULL;
pA1->dumb();pA1->set(10);pA1->get();pA2->dumb();pA2->set(20);//崩潰pA2->get();
return 0;}
為什么會這樣?
通過非法指標呼叫函式,就相當于給函式傳遞了一個指向函式的非法指標!
但是為什么pA2->dumb()會成功呢?
因為導致崩潰的是訪問了成員變數!!
使用已經釋放的指標
struct X{int data;};
int foo(){
struct X *pX;
pX = (struct X *) malloc(sizeof (struct X));
pX->data = https://www.cnblogs.com/huya-edu/archive/2020/10/26/10;
free(pX);
...return pX->data;
}
使用未初始化的指標
如果你這樣寫,編譯器會提示你使用了未初始化的變數p,
void foo A()
{
int *p;*p = 100;
}
那么如果我釋放一個初始化的指標呢?
void foo B()
{
int *p;
free(p);
}
結果是一樣的!!
釋放已經釋放的指標
直接看看代碼:
void foo A()
{
char*p;
p = (char*)malloc(100);
cout<<"free(p)\n";
free(p);
cout<<"free(p)\n";
free(p);
}
這樣的問題也許不會立即使你的程式崩潰,那樣后果更加嚴重!!
沒有呼叫子類的解構式
之前講過,父類的解構式最好宣告為虛!!
ParentClass *pObj = new ChildClass;
...delete pObj;
上述代碼會造成崩潰,如果父類的解構式不宣告為虛,那么不會呼叫繼承類的解構式,造成記憶體泄露,
記憶體溢位
當我們拷貝字串的時候,我們常常會用到 memcpy函式,這里特別需要注意的就是字串結尾的null字符:
char *p = (char *)malloc(strlen(str));
strcpy(p, str);
為了躲過這個坑,只需要把 strlen(str) 改為 strlen(str)+1,

總結
本文討論了幾種在使用動態記憶體分配時應該避免的陷阱,要避免記憶體相關的問題,良好的習慣是:
1. 始終結合使用 memset 和 malloc分配記憶體,或始終使用 calloc,
2. 每當向指標寫入值時,都要確保對可用位元組數和所寫入的位元組數進行交叉核對,
3. 在對指標賦值前,要確保沒有記憶體位置會變為孤立的,
4. 每當釋放結構化的元素(而該元素又包含指向動態分配的記憶體位置的指標)時,都應首先遍歷子記憶體位置并從那里開始釋放,然后再遍歷回父節點,
5. 始終正確處理回傳動態分配的記憶體參考的函式回傳值,
6. 每個 malloc 都要有一個對應的 free,
7. 確保您不是在訪問空指標,

如果你想深度學習C語言以及高級編程,小編推薦一個編程俱樂部【點擊進入】!
涉及到:C語言、C++、windows編程、網路編程、QT界面開發、Linux編程、游戲編程、黑客等等......

程式員編程入門資料:

程式員?推薦學習書籍:

一個活躍、高逼格、高層次的程式員編程學習殿堂;編程入門只是順帶,思維的提高才有價值!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/192281.html
標籤:其他
