常見的C語言內存錯誤及對策

1、指針沒有指向一塊合法的內存

定義了指針變量,可是沒有爲指針分配內存,即指針沒有指向一塊合法的內存。淺顯的例子就不舉了,這裏舉幾個比較隱蔽的例子。

一、結構體成員指針未初始化
struct student
{
   char *name;
   int score;
}stu,*pstu;
intmain()
{
   strcpy(stu.name,"Jimy");
   stu.score = 99;
   return 0;
}
不少初學者犯了這個錯誤還不知道是怎麼回事。這裏定義告終構體變量stu,可是他沒想到這個結構體內部char *name 這成員在定義結構體變量stu 時,只是給name 這個指針變量自己分配了4 個字節。name 指針並無指向一個合法的地址,這時候其內部存的只是一些亂碼。因此在調用strcpy 函數時,會將字符串"Jimy"往亂碼所指的內存上拷貝,而這塊內存name 指針根本就無權訪問,致使出錯。解決的辦法是爲name 指針malloc 一塊空間。

一樣,也有人犯以下錯誤:
intmain()
{
   pstu = (struct student*)malloc(sizeof(struct student));
   strcpy(pstu->name,"Jimy");
   pstu->score = 99;
   free(pstu);
   return 0;
}
爲指針變量pstu 分配了內存,可是一樣沒有給name 指針分配內存。錯誤與上面第一種狀況同樣,解決的辦法也同樣。這裏用了一個malloc 給人一種錯覺,覺得也給name 指針分配了內存。

二、沒有爲結構體指針分配足夠的內存
intmain()
{
   pstu = (struct student*)malloc(sizeof(struct student*));
   strcpy(pstu->name,"Jimy");
   pstu->score = 99;
   free(pstu);
   return 0;
}
爲pstu 分配內存的時候,分配的內存大小不合適。這裏把sizeof(struct student)誤寫爲sizeof(struct student*)。固然name 指針一樣沒有被分配內存。解決辦法同上。

三、函數的入口校驗
無論何時,咱們使用指針以前必定要確保指針是有效的。

通常在函數入口處使用assert(NULL != p)對參數進行校驗。在非參數的地方使用if(NULL != p)來校驗。但這都有一個要求,即p 在定義的同時被初始化爲NULL 了。好比上面的例子,即便用if(NULL != p)校驗也起不了做用,由於name 指針並無被初始化爲NULL,其內部是一個非NULL 的亂碼。

assert 是一個宏,而不是函數,包含在assert.h 頭文件中。若是其後面括號裏的值爲假,則程序終止運行,並提示出錯;若是後面括號裏的值爲真,則繼續運行後面的代碼。這個宏只在Debug 版本上起做用,而在Release 版本被編譯器徹底優化掉,這樣就不會影響代碼的性能。

有人也許會問,既然在Release 版本被編譯器徹底優化掉,那Release 版本是否是就徹底沒有這個參數入口校驗了呢?這樣的話那不就跟不使用它效果同樣嗎?

是的,使用assert 宏的地方在Release 版本里面確實沒有了這些校驗。可是咱們要知道,assert 宏只是幫助咱們調試代碼用的,它的一切做用就是讓咱們儘量的在調試函數的時候把錯誤排除掉,而不是等到Release 以後。它自己並無除錯功能。再有一點就是,參數出現錯誤並不是本函數有問題,而是調用者傳過來的實參有問題。assert 宏能夠幫助咱們定位錯誤,而不是排除錯誤。
windows

2、爲指針分配的內存過小

爲指針分配了內存,可是內存大小不夠,致使出現越界錯誤。
   char *p1 = 「abcdefg」;
   char *p2 = (char *)malloc(sizeof(char)*strlen(p1));
   strcpy(p2,p1);
p1 是字符串常量,其長度爲7 個字符,但其所佔內存大小爲8 個byte。初學者每每忘了字符串常量的結束標誌「\0」。這樣的話將致使p1 字符串中最後一個空字符「\0」沒有被拷貝到p2 中。解決的辦法是加上這個字符串結束標誌符:
   char *p2 = (char *)malloc(sizeof(char)*strlen(p1)+1*sizeof(char));
這裏須要注意的是,只有字符串常量纔有結束標誌符。好比下面這種寫法就沒有結束標誌符了:
   char a[7] = {‘a’,’b’,’c’,’d’,’e’,’f’,’g’};
另外,不要由於char 類型大小爲1 個byte 就省略sizof(char)這種寫法。這樣只會使你的代碼可移植性降低。
數組

3、內存分配成功,但並未初始化

犯這個錯誤每每是因爲沒有初始化的概念或者是覺得內存分配好以後其值天然爲0。未初始化指針變量也許看起來不那麼嚴重,可是它確確實實是個很是嚴重的問題,並且每每出現這種錯誤很難找到緣由。

曾經有一個學生在寫一個windows 程序時,想調用字庫的某個字體。而調用這個字庫須要填充一個結構體。他很天然的定義了一個結構體變量,而後把他想要的字庫代碼賦值給了相關的變量。可是,問題就來了,無論怎麼調試,他所須要的這種字體效果老是不出來。我在檢查了他的代碼以後,沒有發現什麼問題,因而單步調試。在觀察這個結構體變量的內存時,發現有幾個成員的值爲亂碼。就是其中某一個亂碼惹得禍!由於系統會按照這個結構體中的某些特定成員的值去字庫中尋找匹配的字體,當這些值與字庫中某種字體的某些項匹配時,就調用這種字體。可是很不幸,正是由於這幾個亂碼,致使沒有找到相匹配的字體!由於系統並沒有法區分什麼數據是亂碼,什麼數據是有效的數據。只要有數據,系統就理所固然的認爲它是有效的。

也許這種嚴重的問題並很少見,可是也毫不能掉以輕心。因此在定義一個變量時,第一件事就是初始化。你能夠把它初始化爲一個有效的值,好比:
   int i = 10;
   char *p = (char *)malloc(sizeof(char));
可是每每這個時候咱們還不肯定這個變量的初值,這樣的話能夠初始化爲0 或NULL。
   int i = 0;
   char *p = NULL;
若是定義的是數組的話,能夠這樣初始化:
   int a[10] = {0};
或者用memset 函數來初始化爲0:
   memset(a,0,sizeof(a));
memset 函數有三個參數,第一個是要被設置的內存起始地址;第二個參數是要被設置的值;第三個參數是要被設置的內存大小,單位爲byte。這裏並不想過多的討論memset 函數的用法,若是想了解更多,請參考相關資料。

至於指針變量若是未被初始化,會致使if 語句或assert 宏校驗失敗。這一點,上面已有分析。
安全

4、內存越界

內存分配成功,且已經初始化,可是操做越過了內存的邊界。這種錯誤常常是因爲操做數組或指針時出現「多1」或「少1」。好比:
int a[10] = {0};
for (i=0; i<=10; i++)
{
   a[i] = i;
}
因此,for 循環的循環變量必定要使用半開半閉的區間,並且若是不是特殊狀況,循環變量儘可能從0 開始。
函數

5、內存泄漏

內存泄漏幾乎是很難避免的,無論是老手仍是新手,都存在這個問題。甚至包括windows,Linux 這類軟件,都或多或少有內存泄漏。也許對於通常的應用軟件來講,這個問題彷佛不是那麼突出,重啓一下也不會形成太大損失。可是若是你開發的是嵌入式系統軟件呢?好比汽車制動系統,心臟起搏器等對安全要求很是高的系統。你總不能讓心臟起搏器重啓吧,人家閻王老爺是很是好客的。

會產生泄漏的內存就是堆上的內存(這裏不討論資源或句柄等泄漏狀況),也就是說由malloc 系列函數或new 操做符分配的內存。若是用完以後沒有及時free 或delete,這塊內存就沒法釋放,直到整個程序終止。

一、告老還鄉求良田
怎麼去理解這個內存分配和釋放過程呢?先看下面這段對話:
萬歲爺:愛卿,你爲朕立下了汗馬功勞,想要何賞賜啊?
某功臣:萬歲,黃金白銀,臣視之如糞土。臣年歲已老,欲告老還鄉。臣乞良田千畝以蔭後世,別無他求。
萬歲爺:愛卿,你勞苦功高,卻僅要如此小賞,朕今天就如你所願。戶部劉侍郎,查看湖廣一帶是否還有千畝上等良田不曾封賞。
劉侍郎:長沙尚有五萬餘畝上等良田不曾封賞。
萬歲爺:在長沙撥良田千畝封賞愛卿。愛卿,良田千畝,你欲何用啊?
某功臣:謝萬歲。長沙一帶,適合種水稻,臣想用來種水稻。種水稻須要把田分爲一畝一塊,方便耕種。
。。。。

二、如何使用malloc 函數
不要莫名其妙,其實上面這段小小的對話,就是malloc 的使用過程。malloc 是一個函數,專門用來從堆上分配內存。使用malloc 函數須要幾個要求:
內存分配給誰?這裏是把良田分配給某功臣。
分配多大內存?這裏是分配一千畝。
是否還有足夠內存分配?這裏是還有足夠良田分配。
內存的將用來存儲什麼格式的數據,即內存用來作什麼?
這裏是用來種水稻,須要把田分紅一畝一塊。分配好的內存在哪裏?這裏是在長沙。

若是這五點都肯定,那內存就能分配。下面先看malloc 函數的原型:
   (void *)malloc(int size)
malloc 函數的返回值是一個void 類型的指針,參數爲int 類型數據,即申請分配的內存大小,單位是byte。內存分配成功以後,malloc 函數返回這塊內存的首地址。你須要一個指針來接收這個地址。可是因爲函數的返回值是void *類型的,因此必須強制轉換成你所接收的類型。也就是說,這塊內存將要用來存儲什麼類型的數據。好比:
   char *p = (char *)malloc(100);
在堆上分配了100 個字節內存,返回這塊內存的首地址,把地址強制轉換成char *類型後賦給char *類型的指針變量p。同時告訴咱們這塊內存將用來存儲char 類型的數據。也就是說你只能經過指針變量p 來操做這塊內存。這塊內存自己並無名字,對它的訪問是匿名訪問。

上面就是使用malloc 函數成功分配一塊內存的過程。可是,每次你都能分配成功嗎?

不必定。上面的對話,皇帝讓戶部侍郎查詢是否還有足夠的良田未被分配出去。使用malloc函數一樣要注意這點:若是所申請的內存塊大於目前堆上剩餘內存塊(整塊),則內存分配會失敗,函數返回NULL。注意這裏說的「堆上剩餘內存塊」不是全部剩餘內存塊之和,由於malloc 函數申請的是連續的一塊內存。

既然malloc 函數申請內存有不成功的可能,那咱們在使用指向這塊內存的指針時,必須用if(NULL != p)語句來驗證內存確實分配成功了。

三、用malloc 函數申請0 字節內存
另外還有一個問題:用malloc 函數申請0 字節內存會返回NULL 指針嗎?

能夠測試一下,也能夠去查找關於malloc 函數的說明文檔。申請0 字節內存,函數並不返回NULL,而是返回一個正常的內存地址。可是你卻沒法使用這塊大小爲0 的內存。這好尺子上的某個刻度,刻度自己並無長度,只有某兩個刻度一塊兒才能量出長度。對於這一點必定要當心,由於這時候if(NULL != p)語句校驗將不起做用。

四、內存釋放
既然有分配,那就必須有釋放。否則的話,有限的內存總會用光,而沒有釋放的內存卻在空閒。與malloc 對應的就是free 函數了。free 函數只有一個參數,就是所要釋放的內存塊的首地址。好比上例:
   free(p);
free 函數看上去挺狠的,但它到底做了什麼呢?其實它就作了一件事:斬斷指針變量與這塊內存的關係。好比上面的例子,咱們能夠說malloc 函數分配的內存塊是屬於p 的,由於咱們對這塊內存的訪問都須要經過p 來進行。free 函數就是把這塊內存和p 之間的全部關係斬斷。今後p 和那塊內存之間再無瓜葛。至於指針變量p 自己保存的地址並無改變,可是它對這個地址處的那塊內存卻已經沒有全部權了。那塊被釋放的內存裏面保存的值也沒有改變,只是再也沒有辦法使用了。

這就是free 函數的功能。按照上面的分析,若是對p 連續兩次以上使用free 函數,確定會發生錯誤。由於第一使用free 函數時,p 所屬的內存已經被釋放,第二次使用時已經無內存可釋放了。關於這點,我上課時讓學生記住的是:必定要一夫一妻制,否則確定出錯。

malloc 兩次只free 一次會內存泄漏;malloc 一次free 兩次確定會出錯。也就是說,在程序中malloc 的使用次數必定要和free 相等,不然必有錯誤。這種錯誤主要發生在循環使用malloc 函數時,每每把malloc 和free 次數弄錯了。這裏留個練習:
寫兩個函數,一個生成鏈表,一個釋放鏈表。兩個函數的參數都只使用一個表頭指針。

五、內存釋放以後
既然使用free 函數以後指針變量p 自己保存的地址並無改變,那咱們就須要從新把p的值變爲NULL:
   p = NULL;
這個NULL 就是咱們前面所說的「栓野狗的鏈子」。若是你不栓起來早晚會出問題的。好比:
在free(p)以後,你用if(NULL != p)這樣的校驗語句還能起做用嗎?例如:
   char *p = (char *)malloc(100);
   strcpy(p, 「hello」);
   free(p); /* p 所指的內存被釋放,可是p 所指的地址仍然不變*/
   …
   if (NULL != p)
   {
      /* 沒有起到防錯做用*/
      strcpy(p, 「world」); /* 出錯*/
   }
釋放完塊內存以後,沒有把指針置NULL,這個指針就成爲了「野指針」,也有書叫「懸垂指針」。這是很危險的,並且也是常常出錯的地方。因此必定要記住一條:free 完以後,必定要給指針置NULL。

同時留一個問題:對NULL 指針連續free 屢次會出錯嗎?爲何?若是讓你來設計free函數,你會怎麼處理這個問題?
性能

6、內存已經被釋放了,可是繼續經過指針來使用

這裏通常有三種狀況:
第一種:就是上面所說的,free(p)以後,繼續經過p 指針來訪問內存。解決的辦法就是給p 置NULL。
第二種:函數返回棧內存。這是初學者最容易犯的錯誤。好比在函數內部定義了一個數組,卻用return 語句返回指向該數組的指針。解決的辦法就是弄明白棧上變量的生命週期。
第三種:內存使用太複雜,弄不清到底哪塊內存被釋放,哪塊沒有被釋放。解決的辦法是從新設計程序,改善對象之間的調用關係。


上面詳細討論了常見的六種錯誤及解決對策,但願讀者仔細研讀,儘可能使本身對每種錯誤發生的緣由及預防手段爛熟於胸。必定要多練,多調試代碼,同時多總結經驗。測試

相關文章
相關標籤/搜索