05動態記憶體管理

2021-09-27 10:00:50

 我知道的只是  「 肉隨便加  」和  「 要加多少加多少  」 這些詞。    ———— 路飛 

階段2目標:

此階段開始大量刷題,多多參加程式設計類競賽,在實戰中鍛鍊程式設計思維和本領,並且要在不斷複習夯實初階的基礎上,刻意地進行程式設計思維的訓練。學無止境!為了精程序式設計,可以去學習一切為他服務的課程!

目錄

本章重點:

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

二、動態記憶體函數的介紹

1.malloc函數

2.free函數

3.calloc函數

 malloc與calloc的異同:

4.realloc函數   (為了更合理的時候記憶體)

 三、常見動態記憶體錯誤

1.對NULL指標的解除參照操作

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

3.對非動態開闢記憶體使用free釋放

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

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

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

切記: 動態開闢的空間一定要釋放,並且正確釋放

應用:動態通訊錄(優化版)


本章重點:

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

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

  • malloc
  • free
  • calloc
  • realloc

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

4.幾個經典的筆試題

5.柔性陣列

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

我們已經掌握的記憶體開闢方式有:

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

但是,上述開闢空間的方式有兩個特點:

  1. 空間開闢大小是固定的。
  2. 陣列在申明的時候,必須指定陣列的長度,它所需要的記憶體在編譯時分配。

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

二、動態記憶體函數的介紹

我們所知道,我們在堆區實現動態記憶體分配,利用malloc   、   calloc  、   realloc   、  free函數去進行相應的動態分配操作。如圖:

1.malloc函數

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

void*   malloc(  size_t size   );

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

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

解釋:

1.malloc函數的確官方定義是void* 指標,即:萬能型指標。但是我們使用者在使用malloc的時候,需要用具體的型別,來接收,比如:int*  、char* 

這樣寫,在解除參照取空間中的值得時候,才不會報錯;而如果是void* 去接收,解除參照時就會有錯誤,因為編譯器面對void*  該萬能型指標,並不清楚去解除參照幾個位元組。

即:在接收時,需要用具體的資料型別來接收malloc的空間

」錯誤「  程式碼:

int main()
{
	//int arr[10] = { 0 };

	void* p = malloc(40);

	return 0;
}

 正確程式碼:

int* p = (int*)malloc(40);

2.如果開闢失敗,則返回一個NULL指標,即:malloc的返回值一定要做檢查

#include <stdio.h>
#include<stdlib.h>
int main()
{
	//int arr[10] = { 0 };

	//申請空間
	int* p = (int*)malloc(40);//開闢了40個位元組空間,即:10個int型別空間

	//判斷是否 申請失敗
	if (p == NULL)
	{
		printf("申請失敗!");
		return -1;
	}

	//開闢成功了

	//(初始化)/(賦值)
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}

	//釋放空間
	free(p);

	return 0;
}

我們用  *( p + i ) = i;的方式來賦予空間內以初值,用free函數釋放掉指標p維護的10個int型別的空間。而我們偵錯發現,free掉p之後,僅僅是那10個空間被釋放掉了,而指標p仍舊是指向那塊空間的地址,構成了一個野指標,所以以後就很危險,需要將p=NULL;讓程式更安全

#include <stdio.h>
#include<stdlib.h>
int main()
{
	//int arr[10] = { 0 };

	//申請空間
	int* p = (int*)malloc(40);//開闢了40個位元組空間,即:10個int型別空間

	//判斷是否 申請失敗
	if (p == NULL)
	{
		printf("申請失敗!");
		return -1;
	}

	//開闢成功了

	//(初始化)/(賦值)
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}

	//釋放空間
	free(p);
	p = NULL;
	return 0;
}

2.free函數

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

void  free( void *memblock );

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

  1. 如果引數 ptr 指向的空間不是動態開闢的,那free函數的行為是未定義的。
  2. 如果引數 ptr 是NULL指標,則函數什麼事都不做。
     

解釋:

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

2.如果引數 ptr 是NULL指標,則函數什麼事都不做。

假如  p指標並不是malloc來的,那麼對於free(p);在C語言中是未定義的,即:free也不知道該怎麼辦了。

總結:

除非 p 本身等於NULL,否則free以後不會等於NULL。因為free不對指標的值做任何操作,而只是試圖改變指標指向的一片連續的記憶體空間的狀態。如果這片記憶體空間是malloc或其它相容方式(例如POSIX庫函數strdup)分配過來的,那麼會釋放這片空間,釋放的空間可以之後再次被分配。如果指標本來就等於NULL,則呼叫free不會有任何作用。除以上兩種情況外(包括再次free已經被free過的非空指標),free的行為是未定義的,比較有可能的是free這個指標程序在某個時刻突然莫名其妙地崩潰

3.calloc函數

C語言還提供了一個函數叫 calloc , calloc 函數也用來動態記憶體分配。原型如下:

void*  calloc( size_t num ,  size_t size );

函數的功能是為 num 個大小為 size 的元素開闢一塊空間,並且把空間的每個位元組初始化為0
與函數 malloc 的區別只在於 calloc 會在返回地址之前把申請的空間的每個位元組初始化為全0。 舉個例子:

#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
	int* p = calloc(10, sizeof(int));

	//errno是儲存錯誤資訊
	//用strerror函數返回 錯誤碼所對應的錯誤資訊

	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}

	//開闢成功了

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}

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

	return 0;
}

可能對於malloc與calloc有些疑問?什麼情況下會開闢失敗??     當然,記憶體空間不是無限大的,如果你開闢的過多,就會開闢失敗,如:

 malloc與calloc的異同:

同:都可以動態分配記憶體空間

異:

malloc函數僅僅是申請記憶體空間,並且返回起始地址,並不去初始化;而calloc函數既申請記憶體空間,並且返回起始地址,又去初始化每個位元組為0。

總結:

在應用中,想要初始化就應用calloc,不想初始化就用malloc即可。

4.realloc函數   (為了合理的時候記憶體

(追加增容)

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

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

  1. ptr 是要調整的記憶體地址。
  2. size 是調整之後新大小空間。
  3. 返回值為調整之後的記憶體起始位置。(  起初整個記憶體的開頭  )
  4. 這個函數調整原記憶體空間大小的基礎上,還會將原來記憶體中的資料移動到的空間。
  5. realloc在調整記憶體空間的是存在兩種情況:
  • 情況1:原有空間之後有足夠大的空間
  • 情況2:原有空間之後沒有足夠大的空間
     

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

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

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

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

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

#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
	//calloc,申請空間,並初始化為0
	int* p = calloc(10, sizeof(int));

	//errno是儲存錯誤資訊
	//用strerror函數返回 錯誤碼所對應的錯誤資訊

	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;
	}
	//以防空指標,造成越界存取,程式崩潰
	else
	{
		return -1;
	}
	//給新開闢的空間賦值
	for (i = 10; i < 20; i++)
	{
		*(p + i) = i;
	}

	//列印
	for (i = 0; i < 20; i++)
	{
		printf("%d ", *(p + i));
	}
	//釋放空間
	free(p);
	p = NULL;

	return 0;
}

 三、常見動態記憶體錯誤

1.對NULL指標的解除參照操作

錯誤寫法:

	int* p = (int*)malloc(20);
	*p = 20;//直接這樣寫,是有風險的!!

	free(p);

 改寫:

	int* p = (int*)malloc(20);
	if (p == NULL)
	{
		return -1;
	}

	*p = 20;//直接這樣寫,是有風險的!!

	free(p);

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

錯誤寫法:

int main()
{
	
	int* p = (int*)malloc(200);
	if (p == NULL)
	{
		return -1;
	}
	int i = 0;
	for (i = 0; i <= 80; i++)
	{
		*(p + i) = i;//當i是10的時候越界存取
	}
	free(p);
	p = NULL;

	return 0;
}

改寫:

int main()
{
	//此處的200,不是200個空間,而是200個位元組,對應到int型別只是50個空間
	//而 存取的是0 ~ 80,造成了越界存取
	int* p = (int*)malloc(200);
	if (p == NULL)
	{
		return -1;
	}
	int i = 0;
	for (i = 0; i <= 50/*80*/; i++)
	{
		*(p + i) = i;//當i是10的時候越界存取
	}
	free(p);
	p = NULL;

	return 0;
}

3.對非動態開闢記憶體使用free釋放

錯誤程式碼:

int main()
{
	int a = 10;
	int* p = &a;
	free(p);
	p = NULL;

	return 0;
}

該錯誤在於,變數a在記憶體的棧區,而free是釋放動態記憶體的,即:對堆區的空間才有用,此時程式會崩潰。

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

	free(p);
	p = NULL;

	return 0;
}

錯誤原因:

p最後指向的不再是起始地址,而free釋放就要釋放全部(   free的脾氣   )。

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

錯誤程式碼:

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		return -1;
	}

	//使用
	//...

	//釋放
	free(p);
	free(p);
	p = NULL;

	return 0;
}

 對於不屬於自己的指標(    已經釋放過了,p不再維護該動態分配的空間了   ),再次進行free(p),程式就會崩潰;  請思考,這樣寫對不對呢?

	//釋放
	free(p);
	p = NULL;

	free(p);
	p = NULL;

這樣寫,是對的,因為p已經置NULL了,而對於NULL,我們知道,free(BULL);是沒錯的,只不過是沒有用處的寫法罷了!

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

錯誤程式碼:

//動態開闢的記憶體忘記釋放
//在堆區申請的空間,有2種方式可以回收
//1.主動free
//2.程式結束時,作業系統會自動回收

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		return -1;
	}

	//使用
	//...

	//忘記釋放了

	return 0;
}

錯誤原因:

我們在堆區開闢的記憶體,一直未釋放,就會一直存在與堆區,造成記憶體被佔用,對應到後端程式就是,執行變卡;這樣一塊記憶體,我們不能夠對他進行使用,也沒有去釋放掉,就造成了 「   記憶體漏失  」。

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

切記: 動態開闢的空間一定要釋放,並且正確釋放

應用:動態通訊錄(優化版)

四、經典面試題目

題目1

#include<stdio.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);
}
int main()
{
    Test();
    return 0;
}

輸出結果是什麼? ————    程式會崩潰!!!

講解:

其實,GetMemory(str);傳遞的是     ,所以單向值傳遞,並沒有改變str指標的指向NULL,自然而然  hello  world  拷貝不到str中。

更改程式碼:

將GetMemory(&str);中的str取地址,傳遞過去;既然str是char*型別,那麼&str傳遞,則用char**  來接收;

在void GetMemory(char** p)中,*p解除參照,則取到str指標。

free(str);釋放指標                 str = NULL;置空

#include<stdio.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<string.h>
char* GetMemory()
{
	char* p = (char*)malloc(100);
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	strcpy(str, "hello world");
	printf(str);
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

小知識點

	printf(p);
	char* p = "hello world";
	printf("hello world");
	printf(p);
	printf("%s", p);

這三種寫法都是一樣的結果:

大概你可能會疑惑第二種寫法,我們拿第一個寫法舉例,列印的是  hello world ,是因為傳進去的其實並不是常數字串,而是字串的首地址,即:字元h的地址,然後完成列印。

同樣的,我們指標p中儲存的也是常數字串「  hello world  」 ,printf(p);自然就是儲存字串的第一個字元h的地址,進而完成字串的列印。

題目2

#include<stdio.h>
#include<string.h>
char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

輸出結果是 亂碼....................              為什麼呢??

類似於賓館退房

(char* GetMemory(void)中的p是區域性變數!  函數呼叫完變數就銷燬了,雖然返回了p的地址,但是後面再去存取這個地址,已經不是p去維護了,造成了非法存取空間,自然會列印出亂碼。)

GetMemory函數棧幀建立完,會銷燬,之後str是再找不到那塊空間的了。

延伸題目,深入理解

#include<stdio.h>
#include<string.h>
int* Test()
{
	int n = 10;
	return &n;
}
int main()
{
	int* p = Test();
	printf("%d", *p);

	return 0;
}

講解:

先建立main函數的函數棧幀,繼而建立Test函數的函數棧幀,其中存放變數n = 10,返回的n的地址假設為0x0012ff43,再main函數中用指標p來接收n的地址0x0012ff43;而Test函數的棧幀呼叫完就會銷燬,通過0x0012ff43地址再去存取那塊空間,自然會造成非法存取;

當然,編譯器有時候並不會很強大——報錯。        對於該程式,Test函數的那塊函數棧幀被銷燬(即:該塊空間的使用許可權還給了作業系統【這塊空間仍然存在,只是許可權還回去了】,空間的值可能被覆蓋也可能沒有被覆蓋,具體看是否又建立了棧幀覆蓋該區域),再去解除參照存取那塊空間,就有可能存取的是隨機值。

而該程式中,是有可能列印10的。是的,結果當然依舊列印出來也是10,這是因為列印之前,並沒有其他的操作來佔用這塊釋放的Test函數的函數棧幀,所以列印出來了10;那如果我這樣呢?     -------------  結果如下:

這是因為,列印了" 嘿嘿 "建立了函數棧幀,覆蓋掉了Test函數的棧幀(其實他已經被銷燬了)。

題3

#include<stdio.h>
#include<string.h>
void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}
int main()
{
	Test();
	return 0;
}

其實是和上兩個題目一個意思。該題中先建立main函數棧幀,然後建立了Test函數棧幀,malloc空間給str指標接收,拷貝hello進去,然後將str釋放掉(注意,釋放指的是該塊空間的使用許可權還給了作業系統),但是str仍然是指向malloc出來的那塊空間的,只不過空間我們沒有許可權罷了;str!=NULL成立,而strcpy(str, "world");想要將world拷貝到str中,但是str指向的空間我們並沒有許可權在去存取,將    world   拷貝到str屬於非法存取

總結:題1題2題3,都屬於一類題目,返回棧空間地址的問題。注意棧空間地址不要輕易返回,因為棧出了範圍會銷燬。

注意:銷燬的意思只是指   該棧空間的使用許可權還給了作業系統,這塊空間仍然存在!!

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

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

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

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

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

六、柔性陣列

1.什麼是柔性陣列

也許你從來沒有聽說過柔性陣列(flexible array)這個概念,但是它確實是存在的。 C99 中,結構中的最後一個元素允許是未知大小的陣列,這就叫做『柔性陣列』成員。

例如:

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

有些編譯器會報錯無法編譯可以改成:

typedef struct st_type
{
	int i;
	int a[];//柔性陣列成員
};

2.柔性陣列的特點

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

解釋:
1.結構中的柔性陣列成員前面必須至少一個其他成員

錯誤寫法:

typedef struct st_type
{
	int a[];/
};

正確寫法:

typedef struct st_type
{
	int i;
	int a[];//柔性陣列成員
};

2.sizeof 返回的這種結構大小不包括柔性陣列的記憶體

typedef struct st_type
{
	int i;
	int a[0];//柔性陣列成員
};
printf("%d\n", sizeof(st_type));
//輸出的是4,
//因為sizeof 返回的這種結構大小不包括柔性陣列的記憶體
//所以,僅有i的4位元組大小

3.包含柔性陣列成員的結構用malloc( )函數進行記憶體的動態分配,並且分配的記憶體應該大於結構的大小,以適應柔性陣列的預期大小。