首頁 > 軟體

C語言中動態記憶體管理圖文詳解

2022-06-17 18:02:31

1.動態記憶體開闢的原因

常見的記憶體開闢方式

int val = 20;//在棧空間上開闢四個位元組的空間

char arr[10] = {0};//在棧空間上開闢10個位元組的連續空間

上面開闢空間的方式有兩個特點:

1.空間開闢的大小是固定的;

2. 陣列在宣告的時候,必須指定陣列的長度,它所需要的空間在編譯時分配;

但是對於空間的需求,不只是上面的情況,有時候需要的空間大小在程式執行的時候才能得知,這時候陣列的編譯時開闢空間的方式就不能滿足了。

所以需要動態開闢記憶體

2.動態記憶體函數的介紹

動態記憶體的開闢都是在記憶體的堆區中進行開闢的

2.1malloc和free

C語言提供了一個動態開闢記憶體的函數:

void* malloc(size_t size);

malloc函數向記憶體申請一塊連續可用的空間,並返回指向這塊空間起始位置的指標。

1.如果開闢成功,則返回一個指向開闢好的空間的指標;

2.如果開闢失敗,則返回一個NULL指標,因此malloc的返回值一定要做檢查,不然可能會造成野指標的問題;

3.返回值的型別時void*,所以malloc函數並不知道開闢空間的型別,具體在使用的時候由使用者自己來決定返回值的型別;

4.如果引數size為0,此時malloc函數的行為是標準未定義的,取決於程式執行時使用的編譯器;

malloc的使用:

vint main()
{
	int* p = (int*)malloc(40);//向記憶體申請一塊40位元組的空間,並對放回的指標型別轉換
	if (p == NULL)//返回NULL指標時讓列印錯誤資訊並讓程式結束
	{
		perror("malloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	return 0;
}

要記得包含標頭檔案<stdlib.h>

這樣就對開闢的記憶體空間進行了一個使用了,但是還有點問題,因為向記憶體申請的空間沒有進行釋放。

所以這時就引入了另一個函數free

C語言提供了另一個函數free,專門用來做動態記憶體的釋放和回收的

void free(void* ptr);

free函數用來釋放動態開闢的記憶體

1.如果引數ptr指向的空間是不是動態開闢的,那麼free函數的行為是未定義的;

2.如果引數 ptr是NULL指標,則free函數什麼事也不做;

malloc和free函數都宣告在stdlib.h標頭檔案中

free的使用:

int main()
{
	int* p = (int*)malloc(40);//向記憶體申請一塊40位元組的空間,並對放回的指標型別轉換
	if (p == NULL)//返回NULL指標時讓列印錯誤資訊並讓程式結束
	{
		perror("malloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	free(p);//釋放p所指向的動態記憶體
	p == NULL;//將p的值置為NULL
	return 0;
}

free的引數一定是動態開闢記憶體空間的那個起始位置的地址,否則會報錯

在用free釋放完動態開闢的記憶體之後,要對之前指向動態開闢空間的那個指標置為NULL,因為那塊動態開闢的空間已經被作業系統回收了,沒有了存取的許可權,所以要讓p的值為NULL,避免野指標的問題。

如果對動態記憶體開闢的空間沒有釋放掉,會出現一個記憶體漏失的問題。

2.2calloc

C語言還提供了一個calloc函數,calloc也是用來進行動態記憶體的分配

void* calloc(size_t num, size_t size);

 1.calloc的功能是為num個位元組大小為size的元素開闢一個空間,並且把空間的每個位元組的資料都初始化為0,然後返回這塊連續空間的起始位置的地址;

2.與malloc函數的區別只在於,calloc在返回地址之前會把申請的空間的每個位元組的資料都初始化為全0;

calloc的使用:

int main()
{
	int* p = (int*)calloc(10, 4);
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	free(p);
	p = NULL;
	return 0;
}

記憶體情況:

 可以看到,動態開闢的40個位元組的空間都被初始化為全0

所以如果要對動態開闢的空間進行初始化,可以直接使用calloc函數來完成

2.3realloc

有時會發現申請的空間太大或者太小了,為了合理的使用記憶體,一定會對記憶體的大小做一個靈活的調整,那麼realloc函數就可以做到對動態開闢記憶體大小的調整

realloc函數的出現讓動態記憶體管理更加靈活

void* realloc (void* ptr, size_t size); 

1.ptr是要調整的記憶體空間;

2.size是調整之後的新大小;

3.返回值為調整之後的記憶體起始位置;

4.realloc函數在調整原記憶體空間大小的基礎上,還會將原來記憶體中的資料移動到新的空間;

realloc函數在調整記憶體空間時存在兩種情況:

情況1:要調整的空間之後有足夠的空間來存放調整之後的大小

情況2:要調整的空間之後沒有足夠大的空間

如果是情況1,那麼就在原有的記憶體之後追加新的空間,原來空間的資料不發生變化。

如果是情況2,原有空間之後沒有足夠多的空間,此時就會在堆空間上另找一個合適大小的連續空間來使用,這樣函數返回的是一個新的記憶體地址 

 並且realloc函數還會將原空間的資料移動到新的空間。

如果realloc函數在堆區中都找不到一塊合適的空間,則會返回NULL指標。        

realloc的使用:

int main()
{
	int* p = (int*)calloc(10, 4);
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	p = realloc(p, 1000);
	if (p == NULL)
	{
		perror("realloc");
		return 1;
	}
	free(p);
	p = NULL;
	return 0;
}

其次,realloc函數還能使原有空間變小:

使用:

int main()
{
	int* p = (int*)calloc(10, 4);
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	p = realloc(p, 20);
	if (p == NULL)
	{
		perror("realloc");
		return 1;
	}
	free(p);
	p = NULL;
	return 0;
}

記憶體情況:

3.常見的動態記憶體錯誤

3.1對NULL指標的解除參照操作

 這裡編譯器直接把對NULL指標的解除參照操作給取消掉了,如果在其他的編譯器下執行,可能會出現問題,所以一定要對動態開闢記憶體函數的返回值進行一個NULL指標的判斷。

3.2對動態開闢空間的越界存取

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i <= 10; i++)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;
	return 0;
}

其中*(p + 10) = 10;時對動態開闢的空間進行了一個越界存取了,編譯器直接報錯

3.3對非動態開闢記憶體使用free

對棧區上的空間使用free:

int main()
{
	int a = 0;
	free(&a);
	return 0;
}

此時編譯器也會給出一個錯誤 

3.4使用釋放一塊動態開闢記憶體的一部分

int main()
{
	int* p = (int*)malloc(40);
	p++;
	free(p);
	return 0;
}

此時p沒有指向動態開闢記憶體的起始位置

編譯器同樣給出了一個錯誤

3.5對同一塊動態記憶體多次釋放

int main()
{
	int* p = (int*)malloc(40);
	free(p);
	free(p);
	return 0;
}

 p已經釋放過了

3.6動態開闢記憶體忘記釋放(記憶體漏失)

在向記憶體申請了一塊空間之後沒有對其進行釋放會造成記憶體漏失的問題

會迅速吃滿你的記憶體

int main()
{
	while (1)
	{
		malloc(40);
	}
	return 0;
}

 如圖:

如果程式在沒有結束之前申請的記憶體都沒有進行釋放的話,就會出現記憶體漏失的問題。所以在申請好一塊記憶體之後要記得對其進行釋放。

總結:

忘記釋放不再使用的動態記憶體開闢的空間就會造成記憶體漏失的問題,而且動態開闢的空間要正確釋放。

4.練習

4.1練習1

void GetMemory(char* p)
{
	p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}

請問執行Test 函數會有什麼樣的結果?

先建立了一個字元指標變數賦值為NULL,然後呼叫函數GerMemory,呼叫函數時形參只是一份對實參的臨時拷貝,函數呼叫時,申請了一塊動態開闢記憶體,但是並沒有返回p,p在函數呼叫結束後銷燬了,所以此時str指向的還是NULL,strcpy使用時對NULL指標進行了解除參照,造成了非法存取,野指標的問題,也造成了動態記憶體錯誤

4.1練習2

char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}

請問執行Test 函數會有什麼樣的結果?

呼叫GetMemory函數時在棧區開闢了一塊陣列的空間,而在函數呼叫結束後陣列開闢的空間已經被回收了,str接收了p的值,而p所指向的空間已經被回收了,所以p所指向的值也會發生變化,所以此時printf(str);列印的會是未知的結果

4.3練習3

void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
}

請問執行Test 函數會有什麼樣的結果?

 Getmemory函數時傳址呼叫,將申請的動態開闢記憶體空間的起始位置地址給了str,所以能夠正常存取開闢的記憶體。不過沒有進行free會造成記憶體漏失的問題。

4.4練習4

void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

請問執行Test 函數會有什麼樣的結果?

此時已經str所指向的動態記憶體空間已經釋放掉了,會造成非法存取。

5.C/C++程式的記憶體開闢

C/C++程式記憶體分配的幾個區域:

1.棧區:在執行函數時,函數內部變數的儲存單元都可以在棧上建立,函數結束時這些儲存單元自動被釋放。棧記憶體分配運算內建於處理器的指令集中,效率很高,但是分配的記憶體容量有限。棧區主要存放執行函數而存放的區域性變數、函數引數、返回資料、返回地址等。

2.堆區:一般有程式設計師分配,若程式設計師不釋放,程式結束時可能由OS回收,分配方式類似於連結串列。

3.資料段(靜態區):存放全域性變數,靜態資料,程式結束後由系統釋放。

4.程式碼段:存放函數體的二進位制程式碼

實際上普通的區域性變數是在棧區分配空間的,棧區的特點是在上面建立的變數出了作用域就銷燬,但是被static修飾的變數存放在資料段,資料段的特點是,在上面建立的變數,直到程式結束才銷燬,所以static修飾的變數生命週期變長了。

總結

到此這篇關於C語言中動態記憶體管理的文章就介紹到這了,更多相關C語言動態記憶體管理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com