1. 程式人生 > 其它 >16.【C語言進階】動態記憶體管理

16.【C語言進階】動態記憶體管理

為什麼存在動態記憶體分配

棧區上的記憶體開闢

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

這樣直接在函式體中開闢記憶體的方式有它的特點

  1. 開闢記憶體的大小是固定的。
  2. 陣列在申請空間是,需要指定陣列大小。
  3. 出了函式作用域變數就失效了。

但是對於空間的需求,不僅僅是上述的情況。有時候我們需要的空間大小在程式執行的時候才能知道,那陣列的編譯時開闢空間的方式就不能滿足了。這時候就只能試試動態存開闢了。

動態記憶體函式

malloc free

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

void* malloc (size_t size);

這個函式向記憶體申請一塊連續可用的空間,並返回指向這塊空間的指標。

  • 如果開闢成功,則返回一個指向開闢好空間的指標。
  • 如果開闢失敗,則返回一個NULL指標,因此malloc的返回值一定要做檢查。
  • 返回值的型別是 void* ,所以malloc函式並不知道開闢空間的型別,具體在使用的時候使用者自己來決定。
  • 如果引數 size 為0,malloc的行為是標準是未定義的,取決於編譯器。

既然是動態記憶體開闢,那麼有開闢記憶體就一定有釋放記憶體。

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

void free (void* ptr);

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

  • 如果引數 ptr 指向的空間不是動態開闢的,那free函式的行為是未定義的。
  • 如果引數 ptr 是NULL指標,則函式什麼事都不做。
  • 函式執行後,ptr仍指向該地址。

想要使用這兩個函式,我們需要包含標頭檔案 stdlib.h

#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;
}

使用方式與陣列很相似。

如果ptr不置空,會有什麼問題嗎?

自然你是不會犯錯的,但一個工程那麼原始檔,假如你在已經釋放過這邊空間了,但又沒有及時指控,ptr卻仍然指向原來的地址,好比你鄰居有了你家的門鑰匙,這可不合理合法,因此最好釋放記憶體後,將指向該空間的指標都及時置空。

calloc

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

void* calloc (size_t num, size_t size);
  • 函式的功能是為 num 個大小為 size 的元素開闢一塊空間,並且把空間的每個位元組初始化為0。
  • 與函式 malloc 的區別只在於 calloc 會在返回地址之前把申請的空間的每個位元組初始化為全0。

與malloc相比它顯然多了一個功能,不過實在用的很少,完全可以使用malloc + memset兩個函式代替,不過有還是比沒有好的。

來個例子:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* ret = (int*)calloc(10, sizeof(int));
	if (ret)
	{
		//
	}
	free(ret);
	ret = NULL;
	return 0;
}

通過記憶體視窗看到這裡四十個位元組的16進位制全部都成為了0,那麼free又會發生什麼?

free後這裡的資料又都變成了隨機值。

如果需要將開闢的記憶體初始化為0,那麼使用calloc會很方便。

realloc

試想這樣一種場景,假如我們申請了40個byte,但是現在卻不夠用了,那麼只依靠malloccalloc函式,我們怎麼做?難道是另起爐灶,再重新開闢一塊空間,這樣做顯然太笨重了,因此C語言還提供了我們用來動態管理記憶體空間的函式realloc

  • realloc函式的出現讓動態記憶體管理更加靈活。
  • 有時會我們發現過去申請的空間太小了,有時候我們又會覺得申請的空間過大了,那為了合理的時候記憶體,我們一定會對記憶體的大小做靈活的調整。那realloc 函式就可以做到對動態開闢記憶體大小的調整。

函式原型:

void* realloc (void* ptr, size_t size);
  • ptr是要調整的記憶體的地址
  • size是調整後的位元組數
  • 返回值為調整記憶體後的首地址
  • 這個函式調整原記憶體空間大小的基礎上,還會將原來記憶體中的資料移動到新的空間。
  • realloc在調整記憶體空間的是存在兩種情況:
    • 原有空間後有充足的空間
    • 原有空間後沒有充足的空間

會出現這兩種情況的原因是動態開闢的記憶體空間是隨機的,即可能會形成一塊空間上,斷斷續續的被我們所使用。

情況1

當是情況1 的時候,原有空間之後沒有足夠多的空間時,擴充套件的方法是:在堆空間上另找一個合適大小的連續空間來使用。這樣函式返回的是一個新的記憶體地址。

情況2

當是情況2的時候,要擴充套件記憶體就直接原有記憶體之後直接追加空間,原來空間的資料不發生變化。

由於上述的兩種情況,realloc函式的使用就要注意一些。

這是我們有記憶體增大需求的情況,如果是減小呢?

  • malloc在縮小記憶體時,將後面多餘的空間free掉。
  • realloc找不到合適的空間時會返回NULL。
  • 如果ptr為NULL,那麼它的功能和malloc一樣。

所以記得對返回的地址進行判空操作。

常見的動態記憶體錯誤

對空指標的解引用操作

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

p指向棧區而非堆區,free不能釋放不是動態記憶體開闢的空間。

使用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);
}

忘記釋放不再使用的動態開闢的空間會造成記憶體洩漏

動態開闢的記憶體一定要釋放,且對於某一組記憶體操作函式,由誰開闢的,結束後,就由誰釋放。

例如:連結串列的初始化需要動態申請記憶體空間,那麼我們在使用完連結串列後,在銷燬連結串列的函式中就應釋放掉這些記憶體。

動態開闢的空間一定要釋放,並且正確釋放 ,多次的記憶體洩露會導致記憶體空間被浪費,導致卡頓,直至完全卡死。

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

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

  1. 棧區(stack):在執行函式時,函式內區域性變數的儲存單元都可以在棧上建立,函式執行結束時這些儲存單元自動被釋放。棧記憶體分配運算內置於處理器的指令集中,效率很高,但是分配的記憶體容量有限。 棧區主要存放執行函式而分配的區域性變數、函式引數、返回資料、返回地址等。
  2. 堆區(heap):一般由程式設計師分配釋放, 若程式設計師不釋放,程式結束時可能由OS回收 。分配方式類似於連結串列。
  3. 資料段(靜態區)(static)存放全域性變數、靜態資料。程式結束後由系統釋放。
  4. 程式碼段:存放函式體(類成員函式和全域性函式)的二進位制程式碼。

有了這幅圖,我們就可以更好的理解在《C語言初識》中講的static關鍵字修飾區域性變數的例子了。

實際上普通的區域性變數是在棧區分配空間的,棧區的特點是在上面建立的變量出了作用域就銷燬。

但是被static修飾的變數存放在資料段(靜態區),資料段的特點是在上面建立的變數,直到程式結束才銷燬,所以生命週期變長。

柔性陣列

也許你從來沒有聽說過柔性陣列(flexible array)這個概念,但是它確實是存在的。

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

柔性陣列的使用

//程式碼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;

這兩段程式碼可以完成相同的功能。

但是使用柔性陣列有什麼好處呢?

第一個好處是:方便記憶體釋放

如果我們的程式碼是在一個給別人用的函式中,你在裡面做了二次記憶體分配,並把整個結構體返回給使用者。使用者呼叫free可以釋放結構體,但是使用者並不知道這個結構體內的成員也需要free,所以你不能指望使用者來發現這個事。所以,如果我們把結構體的記憶體以及其成員要的記憶體一次性分配好了,並返回給使用者一個結構體指標,使用者做一次free就可以把所有的記憶體也給釋放掉。

第二個好處是:這樣有利於訪問速度.

連續的記憶體有益於提高訪問速度,也有益於減少記憶體碎片。(其實,我個人覺得也沒多高了,反正你跑不了要用做偏移量的加法來定址)

閱讀:C語言結構體裡的成員陣列和指標 | 酷 殼 - CoolShell