主頁 > 軟體設計 > dynamic_memory_allocation(動態記憶體分配)

dynamic_memory_allocation(動態記憶體分配)

2021-09-12 08:10:18 軟體設計

Dynamic Memory Allocation(動態記憶體分配)

?

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

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

malloc

free

calloc

realloc

?

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

4.幾個經典的筆試題

5.柔型陣列

?
?
?

在開始之前,我們需要回顧一下 我們當前所掌握的 記憶體使用方法

當前我們知道的記憶體使用方法

1. 創建一個 變數

int a =10;// (假設)區域變數 - 堆疊區

int g_a =10; // (假設)全域變數 - 靜態區

?
?

2. 創建 一個陣列

區域變數 - 堆疊區

全域變數 - 靜態區

在這里插入圖片描述
?
程式一:

#include<stdio.h>

struct s // 班級成員資訊
{
	char name[20];
	int age;
};

int main()
{
	struct s arr[50];// 50 個 struct s 型別的資料
	// 但是如果班級沒50人,那么就意味著要浪費空間
	// 反之班級人數超過50個,空間就不夠用

	// 如果 在前面加上 int n =0; scanf("%d",&n); n 取代 50 的位數行不行呢?
	// 答案不行 因為 n 是一個變數, 而陣列的元素個數應該給一個常量


	return 0;
}

特點:

1. 空間開辟大小是固定的

2,陣列 在申明 的時候,必須制定陣列的長度,它所需要的的記憶體 在 編譯時 分配

?

C語言是可以創建變長陣列 - c99 中增加了(vs不支持,gcc支持 c99標準 -> gcc test.c - std = c99)

?
?

下面正式進入正文:

為什么存在動態記憶體分配?

對于空間的需求,不僅僅是上述的情況,有時候我們需要的空間大小在程式運行的時候才知道

那陣列的編譯時開辟空間的方式就不能滿足了,這時候就只能試試動態開辟了(在堆上申請空間)

?
?
?
?

動態記憶體函式的介紹

malloc 和 free

malloc :void* malloc(size_t size); // size 開辟的空間大小,單位位元組; void* 是一個指標, 指向的是開辟的空間的起始地址

free:void free(void* memblock); // memblock - 記憶體塊

?
程式二:

#include<stdio.h>
#include<stdlib.h>// malloc 所在頭檔案
#include<errno.h>// errno,h 錯誤碼庫
int main()       // 與 函式 strerror(錯誤資訊報告)  搭配使用,將錯誤碼轉化為錯誤資訊,并回傳它的地址
{
	// 我想向記憶體申請 10個 整形的 空間
	int* p = (int*)malloc(/*INT_MAX*/10 * (sizeof(int)));// 因為  malloc 函式 回傳的是 萬能*(無型別)指標,所以需要轉化型別
	// malloc 函式 也有 開辟空間失敗的時候,
	// 比如 記憶體 只有 4 個 g ,我要開辟 8 g 空間,這肯定是會失敗的,回傳 空指標 NULL
	
	if (p == NULL)
	{
		// 找出列印錯誤原因的方式
		printf("%s\n", strerror(errno));// malloc(INT_MAX)
		                //輸出 Not enough space
	}
	else
	{
		// 正常使用 空間
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			*(p + i) = i;// 賦值 0 1 2 3 4 5 6 7 8 9
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));// 輸出 0 1 2 3 4 5 6 7 8 9
		}
		printf("\n");
	}
	// 當動態申請的空間 不再使用的時候
	// 就會把空間 還給 作業系統
	// 這時候 就會用到 free 函式
	return 0;
}

?
?

說到 free 函式, free 函式 是專門用來做 動態記憶體 的 釋放 和 回收 的

?
程式三:

#include<stdio.h>
#include<stdlib.h>// malloc,free 所在頭檔案
#include<errno.h>
int main()
{
	// 我想向記憶體申請 10個 整形的 空間
	int* p = (int*)malloc(10 * (sizeof(int)));
	if (p == NULL)
	{
		// 找出列印錯誤原因的方式
		printf("%s\n", strerror(errno));// malloc(INT_MAX)
		                //輸出 Not enough space
	}
	else
	{
		// 正常使用 空間
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			*(p + i) = i;// 0 1 2 3 4 5 6 7 8 9
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));// 0 1 2 3 4 5 6 7 8 9
		}
		printf("\n");
	}
	// 當動態申請的空間 不再使用的時候
	// 就會把空間 還給 作業系統
	// 這時候 就會用到 free 函式
	free(p);// 用完了再釋放,也就是說 在列印完之后(呼叫完之后),再釋放(p的值沒有改變,還可以通過它找到該空間)
	//  雖然當前空間 不屬于當前程式,但是依然 可以通過 p  找到  該空間,很有可能會破壞這個空間
	//  所以先在這個 指標 依然很危險

	p = NULL; // free函式 釋放完空間后,不會改 p 的值,所以我們自己主動改
	return 0;
}
// 最后請注意:free 是用來 釋放 動態開辟的記憶體

// free 函式 的 特殊情況:
//1. 如果引數 ptr 指向的空間不是動態開辟的,那 free 函式的行為是未定義的
//2. 如果引數 ptr 是 NULL 指標,則函式 什么 都不做,

為了方便你們進一步了解 free 函式

舉個 special 例子:

你呢,跟你 girlfriend 前期 相處非常好,你把女友電話記得牢牢的,有助于交流,

但是,有一天 你女朋友看不上你了,就 free§ [ 跟你分手了 ]

但是你(p)還死皮賴臉的記住你前女友的號碼(動態開辟空間的地址),隨時可能打電話騷擾她,并且找到她,影響她的生活,說明你很危險(訪問該空間內容,并修改,該指標很危險)

這時候,你前女友為了你能斷開念想 (其實保護自己 == 維護程式安全)

你前女友 給你當頭一棒(爆頭),讓你失憶了,(p = NULL)

我只能說 殺人誅心!!!(還好我單身,暫時不用擔心被爆頭的危險,)

?
?
?
?

calloc 函式 :void* calloc (size_t num,size_t size);

num 元素個數,size每個元素的長度【大小】(位元組)

1. 函式的功能是:開辟一塊空間 num*size ( num 個 大小為 size 的 元素) ,并且把空間的每個位元組初始化為 0

2.與函式 malloc 的區別 只在于 calloc 會在回傳 地址之前 把申請的空間的 每個位元組初始化為全0

?
程式四:

 用法跟 malloc 函式差不多
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main()
{                     // 開一塊空間,空間大小為 10 * int == 40 位元組
	int* p = (int*)calloc(10, sizeof(int));// 比 malloc 函式 多一個元素個數 引數
	if (p == NULL)    
	{
		printf("%s\n", strerror(errno));
	}
	else
	{
		int i = 0;
		for (i = 0; i < 10; i++)// 因為 p 為整形指標變數,指向一個 int 型別 元素,所以有 10個 int 元素
		{
			printf("%d ", *(p + i));// 0 0 0 0 0 0 0 0 0 0
		}  //calloc 在開辟好空間后,會把空間的所有位元組內容全部初始化為 零
	}
	free(p);
	p = NULL;// 狗頭拿來!!
	return 0;
}

?
?

realloc 函式 : void* realloc (void* memblock,size_t size);

memblock - 記憶體塊 (指標 指向 之前已經開辟了的記憶體塊)

size : 新的大小

1. realloc 函式 的 出現 讓動態記憶體管理更加靈活

2. 有時我們會發現過去申請的空間太小了,有時候我們又會覺得申請的空間過大了, 為了合理的使用記憶體,我們一定會對 記憶體的大小 做靈活的調整, realloc 函式就可以做到 對 動態開辟記憶體 大小的調整,

3. memblock 是要調整的記憶體塊的地址

4.size 調整之后 新空間的大小

5.回傳值為 調整之后的記憶體起始位置

6.這個函式調整 原記憶體空間大小 的基礎上,還會將原來記憶體中的資料移動到 新 的空間

7.realloc 在 調整記憶體空間 時,存在兩種情況:

在這里插入圖片描述
?
程式五:

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
int main()
{
	int* p = (int*)malloc(20);// 開辟 20 byte 動態空間
	if (p == NULL) // 防止malloc 開辟動態空間失敗
	{
		printf("%s\n", strerror(errno));// 列印錯誤資訊
	}
	else
	{  // 正常使用空間
		int i = 0;// 20 位元組 ==  5* (sizeof(int))
		for (i = 0; i < 5; i++)
		{
			*(p + i) = i;// 此時 就是在使用 malloc 函式開辟的空間(20byte)
		}               
	}
	// 假設 malloc 函式 開辟的 20 byte 的空間,不滿足我們的需求(不夠)
	// 我們希望能夠有 40 個 byte 的空間
	//這里 就可以使用 realloc 函式 來調整動態開辟的記憶體
	int* p2 = (int*)realloc(p, 40);//  10 * int == 40 byte
	/* p */
    
    //  記住 只要開辟動態空間的時候,最好判斷一下,無論是 malloc ,calloc還是realloc 函式 都有可能開辟空間失敗
    //  在下面的解決方案中,已得到解決 
    
	// 你會發現 原本 整個空間是 由 p 來維護的
	// 但是 現在你會發現 變成了 p2 來維護 了

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p2/* p */ + i));// 0 1 2 3 4 隨機值1 隨機值2  隨機值3  隨機值4  隨機值5
	}                                // 隨機值 是因為 malloc 不會像 calloc 函式 一樣開辟完動態空間之后,初始化為 0
	printf("\n");
	for (i = 5; i < 10; i++) // 使用 增大后的空間
	{
		*(p2/* p */ + i) = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p2/* p */ + i));// 0 1 2 3 4 5 6 7 8 9
	}                 // 如果保持統一 ,p2 換成 p,會出現問題(p已將找不到了)
	             // 因為 realloc 函式,是重新創建一個(40 byte)空間
	             // 把  p 的內容拷貝下來,把拷貝下來的資料 放進 這新創建的空間里
	             // 然后把 p 釋放(p = NULL)
	             // 最后 回傳新空間的地址
	             // 輸出這個新創建的空間 的結果 讓你 感覺 空間確實變大了
	             // 實際上 已經 不是 原先的空間 了
	return 0;
}

?

解決方案:

程式六:

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
int main()
{
	int* p = (int*)malloc(20);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
	}
	else
	{
		int i = 0;// 20 位元組 ==  5* (sizeof(int))
		for (i = 0; i < 5; i++)
		{
			*(p + i) = i;// 此時 就是在使用 malloc 函式開辟的空間(20byte)
		}
	}

	int* ptr = (int*)realloc(p, 40);//  拿一個新的指標變數 ptr 先來接收 realloc 的回傳地址
	if (ptr != NULL) // 判斷 realloc 開辟動態空間是否 成功
	{
		p = ptr;// 防止開辟失敗,把 p 改掉了

		int i = 0;
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));// 0 1 2 3 4 隨機值1 隨機值2  隨機值3  隨機值4  隨機值5
		}
		printf("\n");
		for (i = 5; i < 10; i++)
		{
			*(p + i) = i;
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));// 0 1 2 3 4 5 6 7 8 9
		}
	}
	// 釋放記憶體
	free(p);// 如果 realloc 開辟動態空間失敗,回傳 NULL,而 free(NULL),free 函式什么都不會做
	p = NULL;
	// 一定 要釋放記憶體,并置為空指標,永絕后患!
	return 0;
}

realloc 如果 p 指向的空間之后 有 足夠的記憶體空間可以追加,則追加上,回傳原先的舊地址(p)

如果 p 指向的空間之后 沒有 足夠的記憶體空間可以追加,則重新找一個新的記憶體區域,開辟一塊新的空間, 來滿足你對空間需求,同時把 原來記憶體 的 資料 拷貝下來,把拷貝下來的資料放到新空間里,且把原來的空間釋放,最后回傳新空間的地址

?
?

另外再補充一點

realloc 函式 可以實作與 malloc 函式 一樣的功能

程式七:

#include<stdio.h>
int main()
{
	int* p = (int*)realloc(NULL, 40);// 此時它的功能 與 malloc 函式一樣
	                     //  == (int*)malloc(40);
}

?
?
?
?

常見的動態記憶體錯誤

1. 對 NULL 指標的解參考 操作

程式八:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(40);
	//  萬一 malloc 失敗,p 被賦值為 NULL
	for (int i = 0; i < 10; i++)
	{
		*(p + i) = i;// 非法地址,對 NULL 指標 進行解參考
	}
	free(p);
	p = NULL;
	return 0;
	//解決方案 :在使用開辟的動態空間之前,用 assert(p),記得加上頭檔案 assert.h; 或 加上 if(p != NULL) 判斷陳述句,對 p 進行相關的判斷
}

?
?
?

2,對動態開辟的記憶體 的 越界訪問

程式九:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(20);// 開辟了 5個 int 元素的空間
	if (p == NULL)// 判斷 malloc 函式 開辟動態空間 是否成功
	{
		return  0; //  失敗
	}
	else//  成功
	{  
		int i = 0;    
		for (i = 0; i < 10; i++) // 我只有 5個 int元素,而現在 要訪問 第 6~10 個 int 元素,形成了越界訪問,導致程式崩潰
		{                      // 改成 5 就沒問題了
			*(p + i) = i;           
		}
	}
	free(p);
	p = NULL;
}

?
?
?

3, 對 非動態 開辟記憶體使用 free(釋放)函式

程式十:

#include<stdio.h>
int main()
{
	int a = 10;// 在 堆疊上 開辟空間
	int* p = &a;
	*p = 20;


	free(p); // free 對 非動態 開辟記憶體 使用,會造成程式崩潰
	p = NULL;
	return 0;
}

?
?
?

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

程式十一:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		return 0;
	}
	else
	{
		int  i = 0;
		for (i = 0; i < 10; i++)
		{
			*p++ = i;// 解決方法  *(p+i)= i; 這樣避免了 p 的地址被改變
		}
		// 回收空間
		free(p);// 如果這樣直接這樣釋放空間,會導致程式崩潰
		// 因為 p 已經不再指向該 動態開辟空間 的 起始位置,它釋放第 10 個元素之后的空間(釋放一部分動態開辟的空間)
		// free 只能釋放 動態開辟空間 的起始地址(要釋放就全釋放)
	}
	p = NULL;
	return 0;
}


?
?
?

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

free 釋放 一塊動態開辟空間后,那個 空間 的地址并沒有改變,此時再 free 就可能出現問題

程式十二:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(40);
	if (p = NULL)
	{
		return 0;
	}
    // 使用
	// 釋放
	free(p);
	free(p);// free 釋放 一塊動態開辟空間后,那個 空間 的地址并沒有改變,此時再 free 就可能出現問題
	return 0;
}	//  誰申請 誰回收
	//  在 free(); 后面 手動 把 p = NULL; 后面就算再接上一個 free,也不會有問題
	// 因為 根據C語言標準:free(NULL); free 什么都不做

?
?
?

6. 對動態開辟記憶體 忘記釋放(記憶體泄露)

這種錯誤的出現,會導致計算機記憶體被耗干,導致卡死,畢竟計算機記憶體有限

程式十三:

#include<stdio.h>
#include<windows.h>
int main()
{
	
	while (1)
	{
		malloc(1);
		// 通過 三點(Ctrl、Alt 點 .)一線(同時按),選擇任務管理器 -》 性能 
	    // 通過觀察 記憶體 占用 顯示發現,記憶體正在被大量消耗,
		// 原因是 程式一直在申請空間,不回收,導致大量記憶體空間被占用(這些你不用,別人也用不了,因為你沒還),這就是 記憶體泄露
	}

}

?
?
?

接下來我們來看幾道面試題,來加深我對 動態記憶體分配 函式的 理解

?

題目 1

程式十四:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>


void GetMemory(char* p)// p 是 str 的一份臨時拷貝,即 p= NULL;
{
	// malloc  在堆上開辟了一塊空間,把該動態開辟空間的起始地址賦給 p
	p = (char*)malloc(100);// 此時 p 指向 malloc 開辟的動態空間

	// 動態開辟空間,呼叫完了,沒有釋放空間,會造成 記憶體泄露
    // 而且 這函式呼叫完后之后,這塊函式空間會被回收
	// 到時我們再也找不到 這動態開辟的空間的起始地址
	// 也就是說 出了這個函式  想解決 記憶體泄露 這個問題,都解決不了(p 是該函式的形參,只在該函式內部有效)

 再舉個例子,
  假設有一個臥底(malloc開辟的動態空間),他的資訊(空間的地址)在警察系統中都刪掉了,只有 p 知道,
 后來,有一天 p 死掉了(出了函式,銷毀了 區域變數 p).他也沒有記本本告訴別人(沒有回傳這塊空間的地址),
 那么這個臥底再也證明不了自己的身份(丟失了開辟動態空間的地址),就像無間道里一樣,
 沒有人可以證明 他 是一個警察了(沒有人知道 這塊的空間的地址)



}

void test(void)
{
	char* str = NULL;// dtr 的 空間 放了一個 空指標
	GetMemory(str);// 傳值:傳的是 指標變數 str 本身(內容),不是地址

	strcpy(str, "hello world");// str 還是 空指標,因為 GetMemory(str) 沒有 回傳值,也沒有通過地址 改變str的值
	// 該運算式 意思是把 "hello world" 這個字串 賦給 str 這個空指標 所指向的空間(無效的空間)
	// "hello world" 是不能賦過去的,這樣會導致程式崩潰,因為訪問記憶體已經失敗了
	//  str并沒有 指向 有效空間里,你非要把 "hello world" ,那我們就 解參考空指標,對空指標進行相印的遍歷
	//  空指標的值 == 0, 0 作為地址,找一個空間,在 strcpy 肯定是要 str++ ,往后面找空間,(0 作為地址,對指向的空間賦值,然后++,再進行賦值,以此類推)
	//  這樣做 必然 會 造成  非法訪問記憶體,從而導致 程式崩潰,

// 因為程式已經崩潰了,也就更談不上后面的列印,雖然 列印的格式沒有錯誤,但已經列印不出東西了

	printf(str); // 通過下面一個程式,可以說明該 列印方式 沒有問題
	// == printf("hello world");
}

int main()
{
	test();
	return 0;
}

附加 printf 程式 : printf("%s\n", str) == printf(str)

程式十五:

#include<stdio.h>
int main()
{
	char* str = "abcdef";
	printf("%s\n", str);// abcdef
	printf(str);// abcdef
	return 0;
}

?
?

現在 我們開始 幫助 這個臥底 證明他的身份了(修改程式)

改正1

程式十六:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void GetMemory(char** p)// str 是一個一級指標,要二級指標來接收它地址
{
	// *p == str (通過地址 找到了 str)
	*p = (char*)malloc(100);// 把 malloc 開辟的動態空間的起始地址賦給 *p,也就是 賦給 str 
	
    // 也就說 p 在死之前,把 臥底的身份寫在小本本上, 送給 str 這個人,所以 str 知道臥底的真實身份
}

void test(void)
{
	char* str = NULL;

             //傳址
	GetMemory( &str );// GetMemory - 獲取一塊記憶體塊;
	 //把這塊記憶體 存入 str中,方便下方字串 拷貝
	
	strcpy(str, "hello world");// 此時 str 不再是 NULL, 因為 GetMemory 賦給 它 一個 有效的 動態空間 的地址
	// strcpy 可以 吧 "hello world" 賦給 str 指向的空間(malloc 函式 開辟的動態空間)
	
	printf(str); // hello world
		//  str 跟這個臥底 對了一波 暗號 ==
		
	//但是別忘了 釋放動態開辟的空間,我 們通過的 指標的方式(*p == str), 把地址帶回來了 
	free(str);
	str = NULL; // 并 向臥底保證,目前并不會揭穿他的身份,因為臥底不想暴露身份(其實我懷疑他上癮了,)
}

int main()
{
	test();
	return 0;
}

?
?

改正2

程式十七:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

char* GetMemory(char* p)// str存的值是一個地址,要一個指標來接收它地址
{    
	p = (char*)malloc(100);    //可以這么理解, 臥底(malloc 開辟的動態空間),通過 p 死之前,留下的資訊,去找 str 這個知情人,
	return p;
}

void test(void)
{
	char* str = NULL;
	str = GetMemory(str);//傳值
	// 臥底找到了 str 這個知情人

	strcpy(str, "hello world");// 保險起見,對了波暗號

	printf(str); // hello world

	free(str); //  str 確認了其身份,并保證不泄露他的身份,成為他證人
	str = NULL;// 不曾想,臥底趁str不注意,宰了 str 這個人,(看來他是真的上癮了)
}

int main()
{
	test();
	return 0;
}

?
?
?

題目 2

程式十八:

#include<stdio.h>

char* GetMemory(void)
{
	char p[] = "hello world";// 區域陣列( 堆疊上 開辟 !!!!),出了函式 就會被銷毀(還給作業系統)
	return p;// 這里回傳的是一個野指標
}

void test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);// 訪問野指標,屬于非法訪問,而且再次訪問 不一定就是 hello world,很可能是亂碼
}

int main()
{
	test();
	return 0;
}

?
?
?

題目 3

程式十九:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void GetMemory(char* *p, int num)
{
	*p = (char*)malloc(num); // 通過地址 改變了 str 的值
}

void test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	// if(str != NULL) 或者 assert(str),判斷 malloc 開辟動態空間 是否成功,
	// 萬一開閉失敗,str 為 NULL, 導致拷貝失敗,程式崩潰,這也是一個隱藏問題
	strcpy(str, "hello world");
	printf(str);// 列印 hello world ,但存在記憶體泄露,未釋放動態開辟的空間

	// free(str);
	// str = NULL;

}

int main()
{
	test();
	return 0;
}

?
?
?

題目 4

程式二十:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void test(void)
{
	char* str = (char*)malloc(100);
	if (str)// 判斷 malloc 函式 是否開辟空間成功
	{
		strcpy(str, "hello");
		free(str);// 釋放了記憶體,但沒有 將其 置為 NULL
		
		//加上  str = NULL;   沒有問題
		
		if (str != NULL)// 條件為真
		{
			strcpy(str, "world");// 非法訪問 記憶體,str 所指向的空間已不屬于它
			printf(str);// 可能輸出 world ,也可以是亂碼,因為這塊空間已經被釋放,你再對它進行訪問操作
			            // 什么情況都可能出現
		}
	}
}

int main()
{
	test();
	return 0;
}

?
?
?
?

柔型陣列

在 c99 標準中,結構中的最后一個元素允許是未知大小的陣列,這叫做 柔型陣列 成員

?
?

柔性陣列的 特點:

1.結構體中的柔型陣列成員前面必須至少一個其他成員

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

3.包含柔型陣列成員的結構 用 malloc 函式 進行記憶體的動態分配,并且分配的記憶體 應該大于 結構的大小,以適應 柔型陣列 的 預期大小

?
?
?

程式二十一:

#include<stdio.h>

struct s
{
	int n;

	// 第一種寫法
	int arr[];//  最后一個成員,可以是未知大小的

	// 第二種寫法
	// int arr2[0];   

	// 以上 這種陣列 的 大小是未知的 ,被稱為 柔性陣列 成員
	// 柔性 : 這個 陣列 的 大小 是 可調整的
};
int main()
{

	return 0;
}

因為編譯器的原因,可能有一種寫法不支持,這時可以寫另一種寫法

?
?
?
?

柔性陣列的使用

程式 二十二:
在這里插入圖片描述

#include<stdio.h>
#include<stdlib.h>
struct s // 該結構體大小為 4
{
	int n;
	int arr[];
};
int main()
{
	//struct s s;         //sizeof 回傳的這種結構大小不包括柔型陣列的記憶體
	//printf("%d\n", sizeof(s));// 4, 也就說沒有包含 這個 柔性陣列 的大小

	struct s* ps = (struct s*)malloc(sizeof(struct s) + 5 * sizeof(int));
	//  開辟 24 byte 的空間
	//  口口口口    口口口口口口口口口口口口口口口口口口口口
	//      n                       arr
	//  然后把這塊空間的地址賦給 結構體指標變數 ps
	// 對于 ps 來說 這塊空間是 一個結構體的空間,前四個位元組 是 成員 n 的空間, 后 20 個位元組 是 成員 arr 的空間
	int i = 0;
	if (ps)// 判斷 malloc 開辟動態空間是否成功 如果 ps = NULL == 0,因為 NULL 的值 為 0,條件為假跳過if陳述句
	{
		ps->n = 100;
		for (i = 0; i < 5; i++)
		{
			ps->arr[i] = i;//  0 1 2 3 4 
			printf("%d ", ps->arr[i]); // 0 1 2 3 4
		}
		printf("\n");
		
	// 假設 前面 的 5 個int, 不夠怎么辦,要10個int , 這時就要用 realloc 函式 來擴展
	struct s* ptr = (struct s *)realloc(ps, 44);// 4 byte 結構體大小 + 柔性陣列的大小 40 byte
	if (ptr)// 判斷 realloc 創建 動態 是否成功
	{
		ps = ptr;
		for (i = 5; i < 10; i++)
		{
			ps->arr[i] = i;//  5 6 7 8 9 
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", ps->arr[i]); // 0 1 2 3 4 5 6 7 8 9
		}
		free(ps);// 用完 動態空間,一定要記得釋放
		ps = NULL;
	}
	}
	return 0;
}

?
?
?

程式 二十三 (替代柔型陣列的方法):

在這里插入圖片描述
在這里插入圖片描述

#include<stdio.h>
#include<stdlib.h>

struct s
{
	int n;
	int* arr;// arr 整形指標
};

int main()
{
	struct s* ps = (struct s*) malloc(sizeof(struct s));// n 和 指標變數 arr 的 空間
	int i = 0;
	if (ps)
	{
		ps->arr = malloc(20);// 開辟一塊 20 byte 的動態空間,將其地址 賦給 指標變數 arr
		if (ps->arr)
		{
			for (i = 0; i < 5; i++)// 賦值
			{
				ps->arr[i] = i;
				printf("%d ", ps->arr[i]);// 0 1 2 3 4  
			}            // 圖方便 少寫一個 列印 for 回圈
			printf("\n");
		}
		//調整大小
		int* ptr = (int*)realloc(ps->arr,40);// 開辟一塊 40 byte 的動態空間,將其地址 賦予 指標變數  arr
		if (ptr) // 判斷 realloc 開辟動態空間是否成功
		{
			ps ->arr = ptr;
			for (i = 5; i < 10; i++)
			{
				ps->arr[i] = i;
			}
			for (i = 0; i < 10; i++)
			{
				printf("%d ", ps->arr[i]);// 0 1 2 3 4 5 6 7 8 9
			}
		}
	}
	free(ps->arr);// 要先釋放arr,如果先釋放 ps 會找不到指標變數 arr
	free(ps);
	ps->arr = NULL;
	ps = NULL;
	return 0;
}

?
?
?

那么 柔型陣列 和 替代方法 有何區別

?
?

我們先來對比一下:

柔性陣列方法里(在 c99 標準中),在計算 結構體大小時,是沒有包含 arr 陣列的大小,而且開辟空間之前,我們主動算一個大小,把 n 和 陣列arr 的大小(空間),一次性開辟成功,再交由 ps(接收開辟動態空間的地址) 來維護,柔性陣列 其實是 把 n 和 陣列arr 開辟到一塊連續的空間里)

?

而 第二種方法: 先 malloc 創建這個結構體 空間(包含 n 和 arr),然后 再由 malloc 開辟出一個動態空間,將其地址嫁給 指標變數 arr,( malloc 開辟了二次動態空間)

?

區別:

柔性一次性開辟好空間, 替換的方法 需要 二次

意味著釋放(free)動態空間的時候,柔性 一次釋放,替換的方式需要 二次

那么就是說?在替換方法中?malloc 開辟的動態空間越多, free也就越多,就很有可能會出現?忘記釋放動態空間?而造成?記憶體泄漏?的問題,或者說 釋放 錯誤物件 的 空間

而 在柔性陣列中,只需要 malloc 一次,free也只需要一次,這樣出錯概率小

?
?
?

注意 malloc 函式 在開辟空間的時候,它覺得那個空間是空的,就在那里開辟空間

這樣開辟開辟的方式,必然會導致 記憶體 里面 會有一些 空間 不大不小,導致無法使用,而造成空間的浪費(記憶體碎片)

記憶體碎片 越多,空間利用率 越差,(malloc 用的越多,出現記憶體碎片概率就越高,空間的利用率越低)

所以 使用 柔性陣列,出現的記憶體碎片的概率很低,空間利用率高,

在這里插入圖片描述

替換柔性陣列的方法: 根據 malloc 函式的 特性:它覺得那個空間是空的,就在那里開辟空間

它所創建 空間 不是連續的(就是 a 這里 我創建一塊,b哪里創建一塊空間,在記憶體上 兩者之間并不相連[連續],)

柔性陣列 : n 和 arr 都在一塊連續的記憶體塊 上存盤 (在一塊連續的記憶體塊上,訪問記憶體 的 效率更高)

?
?
?
?

在這里我們先來了解一些東西

存盤器 - 金字塔層次結構:越靠近CPU速度越快,容量越小,價格越貴

金字塔層次結構

在這里插入圖片描述

當 CPU 處理資料的時候 ,從暫存器里拿,因為它更快,

這時候我們就會把 記憶體的資料 , 放到 高速快取 里,高速快取的資料 放到 暫存器 里 ,CPU 在往 暫存器里拿,資料最終都是來自于記憶體

?

區域性原理:

時間區域性(temporal locality):如果一個資料被訪問了,那么它在短時間內還會被再次訪問,如 LRU 快取機制,將頻繁訪問的資料保存在記憶體中,

空間區域性(spatial locality):如果一個資料被訪問了,那么和它相鄰的資料也很快會被訪問,如果陣列的 CPU 預讀功能,

?
?

在這里我們只需了解一下,不必太過深入

當我們訪問一個記憶體(空間)資料的時候,接下來 80% 的可能性,你訪問是它周邊的 資料,

記憶體中,如果我們存放資料的時候,是連續存放的話,那么接下來,我們就把這些連續的資料 放到 暫存器 里,這樣我們的命中率就提升了,這時候計算機的訪問效率更高些,

在這里插入圖片描述

也就是說 替換柔型陣列的方法 ,開辟 的 兩個 空間 并不連續,所在訪問是可能 第一次訪問不到,因為 太分散了(不連續)

意味著 cpu 往暫存器里拿,很有很有可能拿不到我們想要的,它就往 高速快取 里面拿,高速快取沒有,就往記憶體里拿.

這樣我們命中率大大下降,計算機的訪問的效率更低,

?
?

柔型陣列的優點:

1.開辟一塊連續的空間,命中率就提升,計算機的訪問效率更高,

2.出現 記憶體碎片 的 概率很低,空間利用率高,

3.只需要 malloc 一次,free也只需要一次,這樣出錯概率小(忘記釋放動態記憶體,或者釋放錯誤的空間物件)

所以綜合來說 來說:

柔型陣列的優點就是:方便記憶體釋放,訪問速度更快,

?
?

以上就是 動態記憶體分配(Dynamic Memory allocation)的全部內容,

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/299379.html

標籤:其他

上一篇:C語言sizeof與strlen詳解(附大量筆試題題解程序)

下一篇:學透CSS-當CSS遇到古詩和月亮,月亮動起來!!!

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more