天天看點

動态記憶體(malloc、calloc、realloc)詳解前言一、動态記憶體函數的介紹二、常見的動态記憶體錯誤三、關于動态記憶體的經典例題四、柔性數組

目錄

  • 前言
  • 一、動态記憶體函數的介紹
    • 1.1 malloc和free
    • 1.2 calloc
    • 1.3 realloc
  • 二、常見的動态記憶體錯誤
    • 2.1對NULL指針的解引用操作
    • 2.2對動态開辟空間的越界通路
    • 2.3 對非動态開辟記憶體使用free釋放
    • 2.4 使用free釋放一塊動态開辟記憶體的一部分
    • 2.5 對同一塊動态記憶體多次釋放
    • 2.6 動态開辟記憶體忘記釋放(記憶體洩漏)
  • 三、關于動态記憶體的經典例題
    • 例題一
    • 例題二
    • 例題三
    • 例題四
  • 四、柔性數組
    • 4.1 柔性數組的特點
    • 4.2 柔性數組的使用
      • 應用特點1、2
      • 應用特點1、2、3
      • 一般方法

前言

我們在以前開辟空間大小時存在這樣的問題:

  1. 空間開辟大小是固定的。
  2. 數組在申明的時候,必須指定數組的長度,它所需要的記憶體在編譯時配置設定。

    但是對于空間的需求,不僅僅是上述的情況。有時候我們需要的空間大小在程式運作的時候才能知道,那數組在編譯時開辟空間的方式就不能滿足了。這時候就隻能試試動态存開辟了。

一、動态記憶體函數的介紹

1.1 malloc和free

malloc

函數原型:

malloc()

函數向記憶體申請一塊連續可用的空間,并傳回指向這塊空間的指針。

  • 如果開辟成功,則傳回一個指向開辟好空間的指針。
  • 如果開辟失敗,則傳回一個

    NULL

    指針,是以

    malloc

    的傳回值一定要做檢查。
  • 傳回值的類型是

    void*

    ,是以

    malloc

    函數并不知道開辟空間的類型,具體在使用的時候使用者自己來決定。
  • 如果參數

    size

    為 ,

    malloc

    的行為是标準是未定義的,取決于編譯器,它需要引頭檔案

    #include <stdlib.h>

    malloc

    free

    都聲明在

    stdlib.h

    頭檔案中。
  • malloc

    開辟的空間是在堆上申請的。
#include <stdio.h>
#include <stdlib.h>
int main()
{
	//申請空間
	int *p = (int*)malloc(40);//向記憶體(堆區)申請40個位元組,并強制轉換為int*類型
	if (p == NULL)
	{
		return - 1;
	}
	//開辟成功
	int i = 0;
	for ( i = 0; i < 10; i++)
	{
		*(p + i) = i;//初始化數組元素
	}
	//釋放空間
	free(p);//把目前所指的這塊空間還給作業系統
	p = NULL;//釋放空間之後,p還是指向原位址,是以要将p置為空。
	return 0;
}
           

C語言提供了另外一個函數

free

,專門是用來做動态記憶體的釋放和回收的,函數原型如下:

free

函數用來釋放動态開辟的記憶體。

  • 如果參數

    ptr

    指向的空間不是動态開辟的,那free函數的行為是未定義的。
  • 如果參數

    ptr

    NULL

    指針,則函數什麼事都不做。

1.2 calloc

calloc

原型:

  • 函數的功能是為

    num

    個大小為 size 的元素開辟一塊空間,并且把空間的每個位元組初始化為0。
  • 與函數

    malloc

    的差別隻在于:

    malloc

    隻負責在堆區申請空間,并且傳回起始位址,不初始化空間。

    calloc

    在堆區申請空間,并且初始化為0,傳回起始位址。
#include <string.h>
#include <errno.h>
#include <stdlib.h>
int main()
{
	//申請10個int的空間
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));//輸出開辟空間失敗原因
		//如果開辟的空間太大,則有可能導緻開辟空間失敗。比如:int* p = (int*)calloc(100000000, sizeof(int));這樣就會導緻開辟空間失敗。
		return -1;
	}
	//申請成功
	int i = 0;
	for ( i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));//初始化10個元素全為0
	}
           

1.3 realloc

  • realloc

    函數的出現讓動态記憶體管理更加靈活。
  • 有時會我們發現過去申請的空間太小了,有時候我們又會覺得申請的空間過大了,為了合理的使用記憶體,我們就要對記憶體的大小做靈活的調整。

    realloc

    函數就可以做到對動态開辟記憶體大小的調整。

函數原型:

  • ptr

    是要調整的記憶體位址。
  • size

    調整之後新大小。
  • 傳回值為調整之後的記憶體起始位置。
  • 這個函數在調整原記憶體空間大小的基礎上,還會将原來記憶體中的資料移動到 新的空間。

realloc

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

情況1:原有空間之後有足夠大的空間。

動态記憶體(malloc、calloc、realloc)詳解前言一、動态記憶體函數的介紹二、常見的動态記憶體錯誤三、關于動态記憶體的經典例題四、柔性數組

情況2:原有空間之後沒有足夠大的空間。

動态記憶體(malloc、calloc、realloc)詳解前言一、動态記憶體函數的介紹二、常見的動态記憶體錯誤三、關于動态記憶體的經典例題四、柔性數組

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

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

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
int main()
{
	//申請10個int的空間
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));//輸出開辟失敗原因
		return -1;
	}
	//申請成功
	int i = 0;
	for ( i = 0; i < 10; i++)
	{
		 *(p + i) = i;//初始化
	}
	//空間不夠,增加空間至20個int
	int *ptr = (int*)realloc(p, 20 * sizeof(int));//傳回的是調整之後那塊空間的起始位址
	if (ptr != NULL)
	{
		p = ptr;   //如果增容成功,則賦給p
	}
	else
	{
		return -1;
	}
	for ( i = 10; i < 20; i++)
	{
		*(p + i) = i;//初始化剩下的10個整形
	}
	//列印
	for ( i = 0; i < 20; i++)
	{
		printf("%d ", *(p + i));//列印1-20
	}
	//釋放空間
		free(p);
		p = NULL;
	return 0;
}
           

二、常見的動态記憶體錯誤

2.1對NULL指針的解引用操作

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int *p = (int*)malloc(20);
	//if (p == NULL)
	//{
	//	return -1;
	//}
	*p = 0;//這樣寫代碼是有風險的
	return 0;
}
           

如果

malloc

開辟空間失敗,代碼走到

*p = 0;

p

為空指針,

*p

對空指針進行解引用操作,這種操作是有問題的。應該在代碼中加上判斷p是否為空的語句。

2.2對動态開辟空間的越界通路

int main()
{
	int *p = (int*)malloc(200);  //開辟200個位元組空間,(200/4)最多50個元素
		if (p == NULL)
		{
			return -1;
		}
		//使用
		int i = 0;
		for ( i = 0; i < 80; i++)//這裡造成越界通路
		{
			*(p + i) = i;
		}
		free(p);
	    p = NULL;
	return 0;
}
           

2.3 對非動态開辟記憶體使用free釋放

int main()
{
	int a = 10;
	int *p = &a;
	free(p);//錯誤,釋放了一塊非堆上的空間
	p = NULL;
	return 0;
}
           

free

隻能釋放動态記憶體開辟的空間。

2.4 使用free釋放一塊動态開辟記憶體的一部分

int main()
{
	int *p = (int*)malloc(10*sizeof(int));
		if (p == NULL)
		{
			return -1;
		}
		//使用
		int i = 0;
		for ( i = 0; i < 10; i++)
		{
			*p++ = i;//這裡執行完之後,p已經不再指向空間的起始位置了
		}
		//釋放
		free(p);
		//釋放掉的是開辟空間之後的部分,不能從中間的一部分釋放,要從起始位置開始釋放空間
		p = NULL;
	return 0; 
}
           

釋放掉的是開辟空間之後的部分,不能從中間的一部分釋放,要從起始位置開始釋放空間。

2.5 對同一塊動态記憶體多次釋放

int main()
{
	int *p = (int*)malloc(40);
			if (p == NULL)
			{
				return -1;
			}
		//使用
			//....
			
			//釋放
			free(p);
			//p = NULL;
			free(p); 
			//p = NULL;//兩次釋放,每次釋放完之後,要将釋放的指針變量指派為空
	return 0; }
           

兩次釋放,每次釋放完之後,要将釋放的指針變量指派為空。

2.6 動态開辟記憶體忘記釋放(記憶體洩漏)

在堆區上申請的空間有兩種回收的方式:

  1. 主動

    free

  2. 當程式退出的時候,申請的空間也會回收。
int main()
{
	int *p = (int *)malloc(40);
	if (NULL == p)
	{
		return -1;
	}
	//使用
	//....
			
	//忘記釋放了
	getchar();
	return 0;
}
           

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

動态開辟的空間一定要釋放,并且正确釋放。

三、關于動态記憶體的經典例題

例題一

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void GetMemory(char *p)
//str傳給p的時候,是值傳遞,p是str的一份臨時拷貝,是以當malloc開辟的空間起始位址放在p中的時候,不會影響str,str依然為NULL
{
	p = (char *)malloc(100);//當str是NULL,strcpy想把helloworld拷貝到str所指向的空間,程式就崩潰了,因為NULL指針
}
void Test(void)
{
	char *str = NULL;
	GetMemory(str);//str還是原來的空指針
	strcpy(str, "hello world");//拷貝失敗,程式崩潰
	printf(str);
}
int mian()
{
	Test();
	return 0;
}
           
  1. 程式運作會崩潰,

    str

    傳給

    p

    的時候,是值傳遞,

    p

    str

    的一份臨時拷貝,是以當

    malloc

    開辟的空間起始位址放在

    p

    中的時候,不會影響

    str

    str

    依然為NULL。當

    str

    NULL

    strcpy

    想把

    helloworld

    拷貝到

    str

    所指向的空間,程式就崩潰了,因為

    NULL

    指針指向的空間是不能直接通路的。
  2. 并且這個程式還存在記憶體洩露,

    malloc

    開辟的空間沒有被

    free

  3. GetMemory(str);

    函數一但傳回,形參

    p

    就會被銷毀,銷毀之後就會找不到開辟空間的起始位址,就不能回收掉這塊空間了。

代碼改正:

方法一:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void GetMemory(char **p)

{
	*p = (char *)malloc(100);
}
void Test(void)
{
	char *str = NULL;
	GetMemory(&str);

	strcpy(str, "hello world");
	printf(str);
	//釋放
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}
           

改法二:通過傳回值的方式

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//char* GetMemory(char *p)
char* GetMemory()//也可以不傳參
{
	//p = (char *)malloc(100);
	char *p = (char *)malloc(100);
	return p;//傳回位址
}
void Test(void)
{
	char *str = NULL;
	str = GetMemory(str);//傳回值用str接收
	strcpy(str, "hello world");
	printf(str);
	//釋放空間
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}
           

例題二

傳回棧空間位址的問題:

#include <stdio.h>
#include <stdlib.h>
char *GetMemory(void)
{
	char p[] = "hello world"; //建立局部數組,局部數組是放在棧上的,出了這個範圍p就會被銷毀
	//傳回p之後,這個函數棧幀銷毀,這塊空間就不屬于p了
	return p;
}
void Test(void)
{
	char *str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();//列印出随機值
	return 0;
}
           

程式運作之後列印出的是随機值,

GetMemory

函數裡面建立局部數組,局部數組是放在棧上的,出了這個範圍

p

就會被銷毀,傳回

p

之後,這個函數棧幀銷毀,這塊空間就會還給作業系統,就不屬于

p

了。

例題三

void GetMemory(char **p, int num)
{
	*p = (char *)malloc(num);
}
void Test(void)
{
	char *str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str); //記憶體洩露,要free
	//free(str); //要加上free
	//str = NULL;
}
int main()
{
	Test();
	return 0;
}
           

存在記憶體洩露,開辟空間用完之後要記得free。

例題四

void Test(void)
{
	char *str = (char *)malloc(100);
	strcpy(str, "hello");
	free(str);
	//str = NULL; //free之後要将str置為空。
	if (str != NULL)//非法通路記憶體,str此時是野指針
	{
		strcpy(str, "world");
		printf(str);
	}
}
int main()
{
	Test();
	return 0;
}
           

非法通路記憶體,

str

此時是野指針,

free

之後要将

str

置為空。

C/C++程式記憶體配置設定的幾個區域:

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

四、柔性數組

C99

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

struct st_type
{
	int i;
	int a[0];//柔性數組成員
	//int a[];//或者寫成這種,也是柔性數組成員
};
           

4.1 柔性數組的特點

  1. 結構中的柔性數組成員前面必須至少一個其他成員。
  2. sizeof

    傳回的這種結構大小不包括柔性數組的記憶體。
  3. 包含柔性數組成員的結構用

    malloc ()

    函數進行記憶體的動态配置設定,并且配置設定的記憶體應該大于結構的大小,以适應柔性數組的預期大小。

4.2 柔性數組的使用

應用特點1、2

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

應用特點1、2、3

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
 struct st_type
{
	int i;
	int a[0];//柔性數組成員
	//int a[];
};
 int main()
 {
	// 包含柔性數組成員的結構用malloc()函數進行記憶體的動态配置設定,并且配置設定的記憶體應該大于結構的大小,以适應柔性數組的預期大小
	 struct st_type* ps = (struct st_type*)malloc(sizeof(struct st_type) + 10 * sizeof(int)); //4+40
	 if (ps == NULL)
	 {
		 printf("%s\n", strerror(errno));//列印出錯誤資訊
		 return -1;
	 }
	 //開辟成功
	 ps->i = 100;//給i指派100
	 for (int i = 0; i < 10; i++)
	 {
		 ps->a[i] = i;//初始化數組為1-9
	 }
	 //列印數組a
	 for (int i = 0; i < 10; i++)
	 {
		 printf("%d ", ps->a[i]);
	 }
	 //a數組空間如果不夠,希望調整為20個整型資料(擴容)
	 struct st_type* ptr = (struct st_type*)realloc(ps, sizeof(struct st_type) + 20 * sizeof(int));
	 if (ptr == NULL)
	 {
		 printf("擴充空間失敗\n");
		 return -1;
	 }
	 else
	 {
		 ps = ptr;
	 }
	 //使用
	 //...

	 //釋放
	 free(ps);
	 ps = NULL;
	 return 0;
 }

           

一般方法

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct st_type
{
	int i;
	int* a;
};
int main()
{
	struct st_type* ps = (struct st_type*)malloc(sizeof(struct st_type));
	ps->i = 100;
	ps->a = (int*)malloc(10 * sizeof(int));//給a開辟40個位元組空間
	for (int i = 0; i < 10; i++)
		{
			ps->a[i] = i;
		}
		//列印
		for (int i = 0; i < 10; i++)
		{
			printf("%d ", ps->a[i]);
		}
		//a指向的空間不夠,擴容
	int* ptr = (int*)realloc(ps->a, 20 * sizeof(int));//調整新的大小為80個位元組
	if (ptr == NULL)
		{
			printf("擴充空間失敗\n");
			return -1;
		}
		else
		{
			ps->a = ptr;
		}
	 free(ps->a); //先釋放a
	 ps->a = NULL;
	 free(ps);//後釋放ps
	//調用free可以釋放結構體,這個結構體内的成員也需要free
	return 0; 
}
           

上述 2個代碼段可以完成同樣的功能,但是 使用柔性數組實作有兩個好處:

  • 第一個好處是:友善記憶體釋放。如果我們的代碼是在一個給别人用的函數中,你在裡面做了二次記憶體配置設定,并把整個結構體傳回給使用者。使用者調用free可以釋放結構體,但是使用者并不知道這個結構體内的成員也需要free,是以你不能指望使用者來發現這個事。是以,如果我們把結構體的記憶體以及其成員要的記憶體一次性配置設定好了,并傳回給使用者一個結構體指針,使用者做一次free就可以把所有的記憶體也給釋放掉。
  • 第二個好處是:這樣有利于通路速度。連續的記憶體有益于提高通路速度,也有益于減少記憶體碎片。

以上。