1. 程式人生 > >C語言再學習 -- 再論記憶體管理

C語言再學習 -- 再論記憶體管理

但現在看來,缺少示例。從新再寫一篇文章,著重介紹常見記憶體錯誤、跨函式使用儲存區。開始吧,再論記憶體管理!!

發生記憶體錯誤是件非常麻煩的事情。編譯器不能自動發現這些錯誤,通常是在程式執行時才能捕捉到。而這些錯誤大多沒有明顯的症狀時隱時現增加了改錯的難度。

一、常見的記憶體錯誤及其對策

1、記憶體分配未成功,卻使用了它。

程式設計新手常犯這種錯誤,因為他們沒有意識到記憶體分配會不成功。常用解決辦法是,在使用記憶體之前檢查指標是否為NULL。如果指標 p 是函式的引數,那麼在函式的入口處用 assert (p != NULL) 進行檢查。如果是用 malloc 或 new 來申請記憶體,應該用 if (p == NULL) 或者 if (p != NULL) 進行防錯處理。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main (void)
{
	char *p = (char*)malloc (20 * sizeof (char));
	if (p == NULL) //必須檢查是否分配成功
	{
		perror ("error"),exit (1);
	}

	strcpy (p, "hello world");
	puts (p);

	free (p);
	p = NULL;

	return 0;
}
輸出結果:
hello world

2、記憶體分配雖然成功,但是尚未初始化就引用它

犯這種錯誤主要有兩個原因:一是沒有初始化的觀念,二是誤以為記憶體的預設初值全為零,導致引用初值錯誤(例如陣列)。

記憶體的預設初值究竟是什麼並沒有統一的標準,儘管有些時候 為零值,我們寧可信其無不可信其有。所以無論用何種方式建立陣列,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。

#include <stdio.h>
#include <string.h>

int main(void)
{
    int buffer[5] = {1, 2, 3, 4, 5}, i;
    memset(buffer, 0, sizeof(buffer));//將buffer元素全置為0
    for (i = 0; i < 5; ++i){
        printf ("%d ", buffer[i]);
    }
	printf ("\n");
    return 0;
}
輸出結果:
0 0 0 0 0 

3、忘記了釋放記憶體,造成記憶體洩漏

含有這種錯誤的函式每被呼叫一次就丟失一塊記憶體。剛開始時系統的記憶體充足,你看不到錯誤。終有一次程式突然死掉,系統出現提示:記憶體耗盡

這裡還需要了解如何檢查記憶體洩漏,

動態記憶體的申請與釋放必須配對,程式中 malooc 與 free 的使用次數一定要相同,否則肯定有錯誤 (new/delete同理)。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main (void)
{
	while (1) 
	{
		char *p = (char*)malloc (10000000 * sizeof (char));
		if (p == NULL) //必須檢查是否分配成功
		{
			perror ("error"),exit (1);
		}

		strcpy (p, "hello world");
		puts (p);
	}
//	free (p);
//	p = NULL;

	return 0;
}
輸出結果:
hello world
hello world
。。。
hello world
hello world
error: Cannot allocate memory

4、釋放了記憶體卻繼續使用它

有三種情況:

(1)程式中的物件呼叫關係過於複雜,實在難以搞清楚某個物件究竟是否已經釋放了記憶體,此時應該重新設計資料結構,從根本上解決物件管理的混亂局面。

(2)函式的 return 語句寫錯了,注意不要返回指向棧記憶體“棧記憶體”的“指標”或者“引用”,因為該記憶體在函式體結束時自動銷燬。

(3)使用 free 或 delete 釋放了記憶體後,沒有將指標設定為 NULL。導致產生“野指標”

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main (void)
{
	char *p = (char*)malloc (20 * sizeof (char));
	if (p == NULL) //必須檢查是否分配成功
	{
		perror ("error"),exit (1);
	}

	free (p);
	strcpy (p, "hello world");
	puts (p);

	free (p); //釋放了兩次
	p = NULL;

	return 0;
}
輸出結果:
hello world
*** glibc detected *** ./a.out: double free or corruption (fasttop): 0x0828b008 ***
段錯誤 (核心已轉儲)

總結:

函式用法:

type *p;
p = (type*)malloc(n * sizeof(type));
if(NULL == p)
/*請使用if來判斷,這是有必要的*/
{
    perror("error...");
    exit(1);
}
.../*其它程式碼*/
free(p);
p = NULL;/*請加上這句*/

函式使用需要注意的地方:

1、malloc 函式返回的是 void * 型別,必須通過 (type *) 來將強制型別轉換

2、malloc 函式的實參為 sizeof(type),用於指明一個整型資料需要的大小。

3、申請記憶體空間後,必須檢查是否分配成功

4、當不需要再使用申請的記憶體時,記得釋放,而且只能釋放一次。如果把指標作為引數呼叫free函式釋放,則函式結束後指標成為野指標(如果一個指標既沒有捆綁過也沒有記錄空地址則稱為野指標),所以釋放後應該把指向這塊記憶體的指標指向NULL,防止程式後面不小心使用了它。

5、要求malloc和free符合一夫一妻制,如果申請後不釋放就是記憶體洩漏,如果無故釋放那就是什麼也沒做。釋放只能一次,如果釋放兩次及兩次以上會出現錯誤(釋放空指標例外,釋放空指標其實也等於啥也沒做,所以釋放空指標釋放多少次都沒有問題)。

二、跨函式使用儲存區

之前培訓的時候有講到,但是看書總結的時候,大多分開來講了。現在來詳細講解下:

跨函式使用的三種情況:

(1)被呼叫函式可以使用呼叫函式的儲存區,指標作形參可以讓被呼叫函式使用其他函式的儲存區

#include <stdio.h>
char* fa(char* p_str)
{
	char* p=p_str;
	p="hello world";
	return p;
}

int main()
{
	char* str=NULL;
	printf("%s\n",fa(str));
	return 0;
}

(2)動態分配記憶體也可以實現跨函式使用儲存區,只要動態分配函式記憶體沒有被釋放就可以被任何函式使用。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void foo (char *p)
{
	strcpy (p, "hello");
	puts (p);
}

int main (void)
{
	char *p = (char*)malloc (20 * sizeof (char));
	strcpy (p, "hello world");
	if (p == NULL)
	{
		return ;
	}
	strcpy (p, "hello world");
	puts (p);
	foo (p);
	free (p);
	p = NULL;
	return 0;
}
輸出結果:
hello world
hello

(3)static 靜態區域性變數的儲存區可以被任意使用

#include <stdio.h>
int i = 20;  //全域性變數
void func (void)
{
	static int i = 10;  //靜態/區域性變數
	i++;
	printf("%d\n",i);
}
int main (void) 
{
	func ();               //11  優先使用區域性變數
	printf ("%d\n",i);    //20 
	func ();              //12   靜態區域性變數跨函式使用儲存區    
	return 0;             
}
輸出結果:
11
20
12

三、討論malloc(0)返回值

下面的程式碼片段的輸出是什麼,為什麼?
char *ptr;
if ((ptr = (char *)malloc(0)) == NULL) 
puts("Got a null pointer");
else
puts("Got a valid pointer"); 

討論  malloc(0)

#include <stdio.h>
#include <stdlib.h>

int main (void)
{
	char *ptr;
	if ((ptr = (char *)malloc(0)) == NULL) 
		puts("Got a null pointer");
	else
		puts("Got a valid pointer"); 
	return 0;
}
輸出結果:
Got a valid pointer
man malloc 檢視:
The  malloc()  function allocates size bytes and returns a pointer to the allocated memory.  The memory is not initialized.  If size is 0, then malloc() returns either NULL, or a unique pointer value that can later be successfully passed to free().
翻譯就是,就是傳個0的話,返回值要麼是NULL,要麼是一個可以被free呼叫的唯一的指標。

用一個測試示例來說明:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <malloc.h>

int alloc_memory(char *p , int size)
{
    printf("\nbefore malloc %p\n",p);
    p = (char *)malloc(size);
    if(!p)
    {
        printf("malloc error  \n");
        return -1;
    }

    //len of malloc(0)
    printf("len of malloc(%d)  is  %d  ,the ture is %d\n",size,strlen(p),malloc_usable_size(p));

    //the first member 
    printf("the first member of malloc(%d) is %p:%d \n",size,p,*p);

    //set the first member
    *p = 10;
    printf("set the first member of malloc(%d) is %p:%d \n",size,p,*p);

    //memcpy
    memset(p,'\0',12);
    memcpy(p,"01234567890123456789",12);
    printf("after memcpy , the content is %s   len is %d  , the ture is %d \n",p,strlen(p),malloc_usable_size(p));

    free(p);
    p = NULL;

    printf("\n");
}


int main(int argc ,char **argv)
{
    int size = -1;

    char *p = NULL;

    //malloc(0)
    size = 0;
    alloc_memory(p,size);
    
    //malloc(5)
    size = 5;
    alloc_memory(p,size);

    //malloc(20)
    size = 20;
    alloc_memory(p,size);
    return 0;
}
輸出結果:
before malloc (nil)
len of malloc(0)  is  0  ,the ture is 12
the first member of malloc(0) is 0x932f008:0 
set the first member of malloc(0) is 0x932f008:10 
after memcpy , the content is 012345678901   len is 15  , the ture is 12 

before malloc (nil)
len of malloc(5)  is  0  ,the ture is 12
the first member of malloc(5) is 0x932f008:0 
set the first member of malloc(5) is 0x932f008:10 
after memcpy , the content is 012345678901   len is 15  , the ture is 12 

before malloc (nil)
len of malloc(20)  is  0  ,the ture is 20
the first member of malloc(20) is 0x932f018:0 
set the first member of malloc(20) is 0x932f018:10 
after memcpy , the content is 012345678901   len is 12  , the ture is 20 
從測試結果來看,可以得出以下幾個結論:
1. malloc(0)在我的系統裡是可以正常返回一個非NULL值的。這個從申請前列印的before malloc (nil)和申請後的地址0x9e78008可以看出來,返回了一個正常的地址
2. malloc(0)申請的空間到底有多大不是用strlen或者sizeof來看的,而是通過malloc_usable_size這個函式來看的。---當然這個函式並不能完全正確的反映出申請記憶體的範圍。
3. malloc(0)申請的空間長度不是0,在我的系統裡它是12,也就是你使用malloc申請記憶體空間的話,正常情況下系統會返回給你一個至少12B的空間。這個可以從malloc(0)和malloc(5)的返回值都是12,而malloc(20)的返回值是20得到。---其實,如果你真的呼叫了這個程式的話,會發現,這個12確實是”至少12“的。
4. malloc(0)申請的空間是可以被使用的。這個可以從*p = 10;及memcpy(p,"01234567890123456789",12);可以得出。

總結:為了安全起見,malloc(0)的非NULL返回值,最好不要進行除了free()之外的任何操作