c語言 --- 動態內存開闢

我們在學習內存的時候,都會知道下面三種內存開闢的方式:

  • 1.從靜態存儲區分配,生命週期隨程序的結束而結束,比如全局變量,static變量
  • 2.從棧空間分配,函數調用完其被自動釋放
  • 3.從堆空間分配,即動態內存開闢,比如:malloc,calloc,realloc,何時申請何時釋放

其中最主要的就是第三種內存開闢方式,下面就一一進行總結。

一.爲什麼存在動態內存開闢

int val = 20;//在棧空間上開闢四個結點
char arr[10] = {0};//在棧空間上開闢十個連續字節的空間

特點:

  • (1)空間開闢大小是固定的
  • (2)數組在聲明的時候,必須指定數組的大小,它所需要的內存在編譯時分配

對於其他很多種情況來說,我們所需要的空間大小在運行時纔會知道,那麼數組編譯時開闢空間的大小就不能滿足,這時候就只能用動態內存開闢。

二.malloc函數和free函數

1.c語言中的動態內存開闢函數:

void * malloc(size_t size);
#include<stdio.h>
int main()
{
	int * ptr = (int *)malloc(10 * sizeof(int));
	if(NULL != ptr)
	{
		for(int i = 0;i < 10;++i)
		{
			*(p + i) = i;
		}
	}
	free(ptr);
	ptr = NULL;
}
  • (1)這個函數向內存申請一塊連續可用的空間,並返回指向這個空間的指針。
  • (2)如果開闢成功,則返回好一個指向開闢空間的指針。
  • (3)如果開闢失敗,則返回nullptr指針,因此malloc的返回值要做類型檢查
  • (4)返回值的類型是void* ,所以malloc函數並不知道開闢空間的類型,具體在使用的時候由使用者來進行決定
  • (5)如果參數size爲0,malloc的行爲是標準未定義的,取決於編譯器。

2.c語言還提供了另一個函數,用來做動態內存的釋放和回收,函數如下:

void free(void * ptr)
  • (1)free函數用來釋放動態內存的開闢。
  • (2)如果參數ptr指向的空間不是動態開闢的,那麼free的行爲是未定義的。
  • (3)如果參數是空指針,則函數什麼事情都不用做。

3.malloc函數的底層實現
在這裏插入圖片描述
不同操作系統下的malloc函數的實現方式略有不同,在這裏,我簡單說一下linux下的實現原理。
linux維護一個break指針,這個指針指向堆空間的某個地址,從堆起始地址到break之間的地址空間是映射好的,可以供進程訪問;而從break往上,是未映射的地址空間,如果訪問這段空間則程序會報錯。
我們用malloc進行內存分配就是從break往上進行。獲得break指針的位置也就得到了堆內存申請的起始地址,malloc實際上可用空間用一個空閒鏈表連接起來,若用戶申請空間,就遍歷該鏈表,找到第一個滿足空間的空閒塊,將其進行拆分,返回合適大小的空間給用戶,將剩下的部分鏈接到鏈表中。
當調用free函數釋放空間時,就會把這塊空間連接到空閒鏈表上。到最後,該空閒鏈表就會被切成很多小的內存塊,一旦用戶申請一塊較大的空間的時候,空閒鏈中的空間大小都無法滿足需求,malloc會申請延時,對空閒鏈表進行檢查,內存重新整理,把相鄰的小片段合成大的空閒塊。

搜索空閒塊最常見的算法:首次適配,下一次適配,最佳適配
首次適配:第一次找到足夠大小的空間就進行分配,這種方法會產生很多的內存碎片。
下一次適配:也就是說第二次找到足夠大小的內存塊就進行適配,這樣會產生比較小的內存碎片。
最佳適配:對堆進行徹底的搜索,從頭開始,遍歷所有的內存塊,使用數據區大小大於size且差值最小的塊作爲此次分配最小的塊。

malloc函數中實際調用了brk(),sbrk()函數

int brk(void * addr);
void * sbrk(intptr_t increment);

這兩個函數都用來改變“program break”(程序中斷點的位置),改變數據段長度,實現虛擬內存到物理內存的映射,brk()函數直接修改有效訪問範圍的末尾地址實現分配和回收。
注意
當使用malloc分配過大的空間,malloc將不在從堆中分配空間,而是使用mmap()這個系統調用從影射區尋找可用的內存空間。

  • (1)當開闢的空間小於128k時,調用brk()函數,malloc的底層實現是系統調用brk(),其主要移動指針_enddata來開闢空間。
  • (2)當開闢的空間大於128k時,mmap()系統調用函數在虛擬地址空間中(堆和棧中間,稱爲文件映射區域的地方)找一塊空間來開闢。

類Unix操作系統的內存分配函數 sbrk/brk 都可以用來分配內存空間,也可以釋放內存空間。sbrk和brk本質上是一樣的。只是參數不同,所以可以互用(如sbrk分配的空間可以用brk來釋放)。
(1)sbrk 參數n是一個整數 n>0 當前位置向後移 相當於分配內存空間 n<0 當前位置向前移 相當於釋放內存空間 n=0 當前位置移動0個字節(不動) 主要是爲了返回當前位置 sbrk(0) : 當程序中第一次調用sbrk(0)時,系統並不會分配物理空間,只是返回一個未使用的虛擬內存地址之後再分配空間時,系統會將分配的物理空間和此虛擬地址進行映射。以後調用sbrk(0),僅僅是返回當前位置。
(2)brk brk的功能和實現與sbrk完全一樣,不同的是,brk使用絕對地址來指定當前位置要移到哪裏去,如果向後移就是分配空間,向前移就是釋放空間。 一般情況下,我們會用sbrk分配空間,用brk釋放空間。

三.calloc函數

c語言還提供了一個函數叫calloc,calloc函數也用來動態內存分配,原型如下:

void* calloc(size_t num,size_t size);
  • (1)函數的功能是爲num個大小爲size的元素開闢一塊空間,並且把空間的每個字節都初始化爲0
  • (2)與函數malloc的區別只在於calloc會在返回地址之前把申請的空間的每個字節初始化全爲0。
#include<stdlib.h>
#include<stdio.h>
int main()
{
	int *p = calloc(10,sizeof(int));
	if(nullptr != p)
	{
		//對這塊空間進行使用
	}
	free(p);
	p = NULL;
	return 0;
}

如果我們要對申請的空間內容進行初始化,那麼就可以用calloc函數。

四.realloc函數

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

函數原型如下:

void* realloc(void * ptr , size_t size);
  • ptr是要調整的內存地址
  • size調整之後新大小
  • 返回值爲調整之後內存的起始位置
  • 這個函數調整原有內存空間大小的基礎上,還會將原來內存中的數據移動到新的空間。

realloc函數調整空間的兩種情況:

  • 1.原有空間之後有足夠大的空間
  • 2.原有空間之後沒有足夠大的空間

在這裏插入圖片描述
**情況1:**要擴展內存的時候就直接在原有內存之後直接追加空間,原有空間數據不會發生變化。
**情況2:**原有空間之後沒有足夠多的空間,在堆上找到一個連續空間來進行使用。這樣返回的是一個新的內存地址。

#include<stdio.h>

int main()
{
	int *ptr = malloc(100);
	if(ptr != NUll)
	{
		//對開闢的內存進行處理
	}
    
    int *p = NULL;
    p = realloc(ptr,1000);
    if(p != NULL)
    {
    	ptr = p;
    }
    free(ptr);
    return 0;
}

五.常見的內存錯誤

  • 1.對NULL指針的解引用操作
  • 2.對動態內存開闢空間的越界訪問
  • 3.對非動態開闢內存使用free函數
  • 4.使用free釋放一塊動態開闢內存的一部分
  • 5.對同一塊空間進行多次釋放
  • 6.動態內存開闢忘記釋放

面試題:
題目1.

void GetMemory(char * p)//錯誤:傳參出錯char** p
{
	p = (char *) malloc(100); //*p = (char *) malloc(100);
}
void Test(void)
{
	char * str = NULL;
	GetMemory(str);//GetMemory(&str);
	strcpy(str,"hello world");
	printf(str);
	//free(str);
	//str = NULL:
}
//程序崩潰
//錯誤1:沒有釋放

題目2:

char *GetMemory(void)
 {   
       char p[] = "hello world"; 
       return p;		//這裏的p出了作用域已經被銷燬了。這塊空間由可能被別人使用。
} 
void Test(void)
 {    
 		char *str = NULL;    
 		str = GetMemory();    
 		printf(str); 
}

題目3:

void GetMemory2(char **p, int num) 
{
    *p = (char *)malloc(num);
}
void Test(void)
{
    char *str = NULL;    
    GetMemory(&str, 100);    
    strcpy(str, "hello");    
    printf(str); 
    //釋放空間
    //free(str);
    //str = NULL:
    //free函數爲什麼沒有把free置爲空指針?
    //沒有,因爲free傳的是地址的內容,沒有傳地址。
}

題目4:

void Test(void) 
{    
	char *str = (char *) malloc(100);    
	strcpy(str, “hello”);    
	free(str);    					//空間釋放,但是指針沒有指向NULL
	//str = NULL;
	if(str != NULL)    
	{        
		strcpy(str, “world”);        //訪問內存空間出錯的問題。
		printf(str);    
	} 
} 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章