C語言動態內存管理分析總結

什麼是動態內存分配

我們都知道在C語言中,定義變量的時候,系統就會為這個變量分配內存空間,而且這個空間是在棧上開辟的,這種方式就會有兩個特點。

  • 開辟的空間大小是固定的
  • 數組在申明的時候,必須要指定數組的長度,以方便為其分配內存大小

但是這種方法並不能滿足我們在開發中的需要,因為有時候我們需要開辟的空間大小,是在程序巡行的過程中才能知道要開辟多大的。

這時候,數組的這種開辟方式就不能滿足瞭,因此也就有瞭動態內存分配的需要。

而動態內存分配,會根據需求而分配空間,大小是可以變化的,並且是在堆區上分配空間,而堆區的內存空間大小一半都會比棧區大。

動態內存函數的介紹

需要需要動態內存管理,需要瞭解C語言中一下的幾個函數。

統一說明:一下的函數都包含在<stdlib.h>中。

free

void free (void* ptr);

為瞭後面可以進行代碼演示,這裡先介紹free這個函數。

我們在使用相關函數動態開辟瞭內存空間之後,函數會返回這片空間的的首地址給使用者,當使用者用完空間之後,需要手動對這片空間進行釋放,把內存空間還給操作系統。

如果參數 ptr 指向的空間不是動態開辟的,那free函數的行為是未定義的。
如果參數 ptr 是NULL指針,則函數什麼事都不做。
釋放的空間是ptr所指向的那塊內存空間

上面的第一種情況,有可能編譯器會報錯。

如果我們僅僅隻是開辟空間使用,而不釋放的話,會占用內存空間資源,造成內存泄漏,影響性能。

因此,我們需要養成用完就釋放的好習慣。

malloc

void* malloc (size_t size);

這個函數向內存申請一塊連續可用的空間,並返回指向這塊空間的指針。

連續可用的特點,就像數組一樣。

如果開辟成功,則返回一個指向開辟好空間的指針。
如果開辟失敗,則返回一個NULL指針。
返回值的類型是 void* ,因此使用者需要根據自己的需求來轉化使用。
如果參數size大小為0,malloc 的行為是標志沒有規定的,取決於編譯器。

size的大小是按照字節為單位的。

#include <stdio.h>

int main()
{
	//代碼1
	int num = 0;
	scanf("%d", &num);
	int arr[num] = { 0 };

	//代碼2
	int* ptr = NULL;
	ptr = (int*)malloc(num * sizeof(int));
	if (NULL != ptr)//判斷ptr指針是否為空
	{
		int i = 0;
		for (i = 0; i < num; i++)
		{
			*(ptr + i) = 0;
		}
	}

	free(ptr);//釋放ptr所指向的動態內存
	ptr = NULL;//是否有必要?
	return 0;
}

上面的代碼就是malloc函數的基本使用過程,在代碼1中,這樣的使用方法,一般來說編譯器是不支持的,而第二種方法,也就是動態內存開辟的方法,編譯器就支持。

在最後,記得要把ptr所指向的空間給釋放掉。

這個時候,ptr種存儲的仍然是那塊空間的地址,這就稱為瞭野指針,所以我們還要把ptr置空。

calloc

與malloc類相似的,C語言還提供瞭另外一個動態開辟內存的函數,那就是calloc。

void* calloc (size_t num, size_t size);

需要註意的是,這個函數的參數有所不同,第一個參數num是用來確定你開辟的連續空間是用來存放多少個元素的,而第二個參數size表示的是一個元素占用多少的字節。

函數的功能是為 num 個大小為 size 的元素開辟一塊空間,並且把空間的每個字節初始化為0。
與函數 malloc 的區別隻在於 calloc 會在返回地址之前把申請的空間的每個字節初始化為全0。

所以,如果我們需要在動態開辟內存的時候進行初始化,我們可以使用calloc這個函數。

我們可以通過調試的監視窗口來查看這個函數在開辟空間的時候是否幫我們初始化。

#include <stdlib.h>


int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (NULL != p)
	{
		//使用空間
	}
	free(p);
	p = NULL;
	return 0;
}

realloc

有時候我們會發現,哪怕是使用上面的動態內存管理的函數,也會有不方便的時候。我們可能會因為空間申請小瞭不夠用而去擴容,但是如果我們使用上面兩個函數去開辟更大的空間的時候,之前的數據拷貝過來新開辟的更大的空間又很麻煩。

這個時候,我們就可以使用realloc瞭。

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

ptr 是要調整的內存地址。
size 調整之後新大小,和malloc一樣,是以字節為單位的。
返回值為調整之後的內存起始位置。
這個函數調整原內存空間大小的基礎上,還會將原來內存中的數據移動到新的空間。

在realloc函數使用的時候,會出現兩種情況:

原有的空間之後還有足夠的空間

如果原來的內存空間之後還有足夠的空間來滿足新的大小,這時候就會直接對原來的空間進行擴容,原來空間的數據不會發生變化。

原有的空間之後沒有足夠的空間

如果原來的空間之後沒有足夠的空間滿足需要,就會在內存區域中開辟一片能夠滿足新的大小需要的連續空間,並且把原來空間的數據拷貝的新的空間中,釋放原來的空間,返回新的地址。

#include <stdio.h>

int main()
{
	int* ptr = (int*)malloc(100);
	if (ptr != NULL)
	{
		//業務處理
	}
	else
	{
		exit(EXIT_FAILURE);
	}

	//擴展容量

	//代碼1
	ptr = (int*)realloc(ptr, 1000);
	
	//代碼2
	int* p = NULL;
	p = realloc(ptr, 1000);
	if (p != NULL)
	{
		ptr = p;
	}
	//業務處理
	free(ptr);
	return 0;
}

上述代碼種,代碼1的做法是不安全的,如果我們擴容失敗後,會返回空指針,這個時候,ptr接收瞭空指針,就會造成ptr原來的那塊空間的數據丟失,並且造成內存泄漏。

因此,一般我們都需要像代碼2那樣,先用一個臨時的指針變量接收返回值,並且在判定不為空指針後再復制給ptr。

動態內存管理中常見的錯誤

我們在使上面這些函數的時候,會經常出現一下的錯誤。

對NULL指針的解引用操作

void test()
{
	int* p = (int*)malloc(INT_MAX / 4);

	//沒有進行判空,就直接使用空間
	*p = 20;//如果p的值是NULL,就會有問題
	free(p);
}

對動態開辟空間的越界訪問

void test()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p)
	{
		exit(EXIT_FAILURE);
	}
	for (i = 0; i <= 10; i++)
	{
		*(p + i) = i;//當i是10的時候越界訪問
	}
	free(p);
}

對非動態開辟內存使用free釋放

void test()
{
int a = 10;
int *p = &a;
free(p);
}

使用free釋放一塊動態開辟內存的一部分

void test()
{
	int* p = (int*)malloc(100);
	p++;
	free(p);//p不再指向動態內存的起始位置
	//這樣做一般編譯器也會報錯的
}

對同一塊動態內存多次釋放

void test()
{
	int* p = (int*)malloc(100);
	free(p);
	free(p);//重復釋放,一般來說,編譯器會報錯
}

動態開辟內存忘記釋放(內存泄漏)

void test()
{
	int* p = (int*)malloc(100);
	if (NULL != p)
	{
		*p = 20;
	}
}
int main()
{
	test();
	while (1);
}

我們在使用上面的函數的時候,一定要小心,避免上面的這些錯誤。

一些經典的筆試題

題目1

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

上面的代碼有錯,在開辟空間後,沒有進行判空操作,並且在調用GetMemory函數的時候,傳入的形參是str的一份臨時拷貝,在函數內部p的改變不會改變main中的str,所以在GetMemory返回的時候,p所指向的空間會泄漏,並且在strcpy中,造成瞭對空指針的解引用操作。

題目2

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

這段代碼也是錯誤的,在GetMemory中,不是用動態內存管理的函數來開辟空間,而是使用數組的開辟方式,這樣的開辟方式會在棧區開辟空間,當GetMemory函數調用完成的時候,就會銷毀開辟的數組,這時候,外面的str接收瞭返回的數組的地址,就會變成一個野指針。

題目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);
}

上面的代碼在使用完成動態開辟的空間後沒有進行判空操作,並且沒有進行內存釋放,造成瞭內存泄漏。

題目4

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

上面的代碼提前釋放瞭空間,後面又使用指針對已經釋放的空間進行操作,這是非法的,釋放後,指向這塊空間的指針就是野指針,需要進行置空。

柔性數組

C99 中,結構中的最後一個元素允許是未知大小的數組,這就叫做『柔性數組』成員。

例如:

typedef struct st_type
{
	int i;
	int a[0];//柔性數組成員
}type_a;

如果有的編譯器報錯,可以改成下面這個樣子

typedef struct st_type
{
	int i;
	int a[];//柔性數組成員
}type_a;

柔性數組的特點

結構中的柔性數組成員前面必須至少一個其他成員。
sizeof 返回的這種結構大小不包括柔性數組的內存。
包含柔性數組成員的結構用malloc ()函數進行內存的動態分配,並且分配的內存應該大於結構的大小,以適應柔性數組的預期大小。

例如:

//code1
typedef struct st_type
{
	int i;
	int a[0];//柔性數組成員
}type_a;
printf("%d\n", sizeof(type_a));//輸出的是4
typedef struct st_type
{
	int i;
	int a[0];//柔性數組成員
}type_a;

//代碼1
int i = 0;
type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
//業務處理
p->i = 100;
for (i = 0; i < 100; i++)
{
	p->a[i] = i;
}
free(p);

這樣柔性數組成員a,相當於獲得瞭100個整型元素的連續空間。

柔性數組的優勢

上面的代碼也可以這樣設計:

//代碼2
typedef struct st_type
{
	int i; 
	int* p_a;
}type_a;

type_a* p = (type_a*)malloc(sizeof(type_a));
p->i = 100;

p->p_a = (int*)malloc(p->i * sizeof(int));

//業務處理
for (i = 0; i < 100; i++)
{
	p->p_a[i] = i;
}

//釋放空間
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;

上述 代碼1 和 代碼2 可以完成同樣的功能,但是 方法1 的實現有兩個好處:

  • 方便內存釋放
  • 這樣有利於訪問速度

到此這篇關於C語言動態內存管理分析總結的文章就介紹到這瞭,更多相關C語言 動態內存管理內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: