🎉🎇🎆👀😄??👍💪
- 🎓為什么存在動態記憶體分配
- 🎓動態記憶體函式的介紹
- ??malloc和free
- ??calloc
- ??realloc
- 🎓常見的動態記憶體錯誤
- 🎓幾個經典的筆試題
- 🎓 C/C++程式的記憶體開辟
- 🎓 柔性陣列
🎓為什么存在動態記憶體分配
常見的創建變數,開辟空間,分配記憶體的方式如下:
int val = 20;//在堆疊空間上開辟四個位元組
char arr[10] = {0};//在堆疊空間上開辟10個位元組的連續空間
??上面的方式稱為靜態開辟,它有如下幾個特點:
- 空間開辟大小是固定的,
- 陣列在宣告的時候,必須指定陣列的長度,它所需要的記憶體在編譯時分配,但是對于空間的需求,不僅僅是上述的情況,有時候我們需要的空間大小在程式運行的時候才能知道,那陣列的編譯時開辟空間的方式就不能滿足了,
這時候就只能試試動態記憶體開辟了,
如通過下面的簡單例子,你就明白,下面是一個簡略的未完善的代碼:
假設我創建一個學生結構體陣列,結構體成員包括姓名,性別,年齡,假設我首先靜態的給陣列開辟100個元素空間,之后向里面又添加元素,但慢慢的里面的元素個數會超過我們定義的最大元素個數,可能之后造成陣列越界,不夠用等情況,所以這種情況我們可以動態的開辟陣列空間,夠了就用,不夠繼系開辟即可,
#include<stdio.h>
#define MAX 1000//初始大小,100個學生
#define ADD 5//增加5個學生
struct student
{
char name[20];
char sex[4];
int age;
}student;
//靜態版本
//struct student data[MAX];
//動態版本
struct student *data;
void add(struct student* pc)
{
data = (struct student*)malloc((MAX+ADD) * sizeof(struct student));
if (data == NULL)
{
perror("InitContact");
return;
}
//.............................
//.............................
}
int main()
{
add(data);
return 0;
}
🎓動態記憶體函式的介紹
??malloc和free
這兩個函式總是成對出現的,一個開辟記憶體,一個釋放記憶體,這兩個函式的單獨使用極有可能會導致程式出錯,
動態記憶體開辟的函式malloc
函式原型 void* malloc (size_t size);
函式說明
- 這個函式向記憶體申請一塊連續可用的空間,并回傳指向這塊空間的指標, 如果開辟成功,則回傳一個指向開辟好空間的指標
- size_t size表示開辟幾個位元組大小的空間
- 如果開辟失敗,則回傳一個NULL指標,因此malloc的回傳值一定要做檢查, 回傳值的型別是 void* ,所以malloc函式并不知道開辟空間的型別,具體在使用的時候使用者自己來決定,
- 如果引數 size為0,malloc的行為是標準是未定義的,取決于編譯器,
動態記憶體釋放函式free
函式原型 void free (void* ptr);
函式說明
- -ptr 傳過來的是開辟空間的起始地址, 如果引數 ptr 指向的空間不是動態開辟的,那free函式的行為是未定義的,如果為空指標什么也不會發生
- 如果引數 ptr 是NULL指標,則函式什么事都不做, free函式釋放記憶體空間后,并不會將接受開辟空間起始地址的的指標置為空指標
malloc和free都宣告在 stdlib.h 頭檔案中,接下來我舉一個開辟記憶體釋放記憶體的例子:
#include <stdlib.h>
int main()
{
//動態記憶體開辟的
int* p = (int*)malloc(10*sizeof(int));//void*
//使用這些空間的時候首先判斷空間是否開辟成功
if (p == NULL)
{
perror("main");//如果在main函式里面,開辟空間失敗,通過perror函式列印錯誤資訊
return 0;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;//通過指標加減整數的方式賦初值
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));//p[i] --> *(p+i)這兩種訪問方式等價的,p指向開辟的那塊空間的起始地址,
//相當于陣列名通過陣列名加下標的方式訪問訪問開辟的空間
}
//回收空間
free(p);///free函式釋放P指向的記憶體空間,但不會把p指標里面地址的內容釋放,這可能就會造成,p又通過地址訪問之前的記憶體空間,造成記憶體非法訪問
p = NULL;//所以自己動手把p置為NULL
return 0;
}
💏💏這里有幾個細節的地方學要注意:
malloc開辟空間后,free函式釋放P指向的記憶體空間,但不會把p指標里面地址的內容釋放,這可能就會造成,p又通過地址訪問之前的記憶體空間,造成記憶體非法訪問,所以一定要手動的把把P置為NULL
??calloc
C語言還提供了一個函式叫 calloc , calloc 函式也用來動態記憶體分配,
函式原型如下: void* calloc (size_t num, size_t size); 函式說明:
- 函式的功能是為 num 個大小為 size 的元素開辟一塊空間,并且把空間的每個位元組初始化為0,
- 與函式 malloc 的區別只在于calloc 會在回傳地址之前把申請的空間的每個位元組初始化為全0,
舉個例子我們使用calloc開辟10個整形空間的大小
#include <stdlib.h>
int main()
{
//int*p = (int*)malloc(40);
int* p = calloc(10, sizeof(int));
if (p == NULL)
return 1;//如果為空直接結束后面的執行
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d\n", *(p + i));
}
free(p);
p = NULL;
return 0;
}
列印calloc開辟空間里面的內容如圖:

所以如果我們對申請的記憶體空間的內容要求初始化,那么可以很方便的使用calloc函式來完成任務,
??realloc
realloc函式的出現讓動態記憶體管理更加靈活,有時會我們發現過去申請的空間太小了,有時候我們又會覺得申請的空間過大了,那為了合理地管理記憶體,我們一定會對記憶體的大小做靈活的調整,那 realloc 函式就可以做到對動態開辟記憶體大小的調整,
函式原型如下: void* realloc (void* ptr, size_t size);
函式說明:
- 這個函式可以在原有的其它記憶體函式開辟空間的基礎上,繼系管理空間的大小,也可以自己重新開辟一塊新的記憶體空間,開辟空間時不初始化里面的內容,
- ptr 是要調整的記憶體地址(原記憶體的起始地址), size 為調整之后記憶體空間的新大小 回傳值為調整之后的記憶體起始位置,
- 這個函式調整原記憶體空間時,如果原記憶體空間后面有足夠的空間則開辟相應的空間,如果原記憶體空間后面沒有足夠的空間可以開辟,就在堆區重新找一塊空間開辟記憶體,之后還會將原來記憶體中的資料移動到新 的空間,
realloc在調整記憶體空間的是存在兩種情況:
情況1:原有空間之后有足夠大的空間
情況2:原有空間之后沒有足夠大的空間
以下面的的代碼為例,下圖分析兩種情況:
情況一:

情況二:

#include <stdlib.h>
int main()
{
int* p = (int*)calloc(3, sizeof(int));//開辟3個大小的整形空間
if (p == NULL)
{
perror("main");
return 1;
}
//如果這里需要p指向的空間更大,需要6個int的空間
//realloc調整空間
int*ptr = (int*)realloc(p, 6*sizeof(int));
if (ptr != NULL)
{
p = ptr;
}
//使用
int i = 0;
for (i = 0; i < 6; i++)
{
printf("%d ", *(p + i));
}
//回收空間
free(p);
p = NULL;
return 0;
}
??當是情況1 的時候,要擴展記憶體就直接原有記憶體之后直接追加空間,原來空間的資料不發生變化,當是情況2 的時候,如果原記憶體空間后面沒有足夠的空間可以開辟,就在堆區重新找一塊空間開辟記憶體,之后還會將原來記憶體中的資料移動到新的空間,
🎓常見的動態記憶體錯誤
👀對NULL指標的解參考操作
空指標就是沒有任何指向的指標,不能對它進行解參考,加減整數等操作,任何對它的操作都是不和法的,都會造成程式的崩潰,malloc函式在開辟記憶體失敗時,會回傳空指標,所以在對malloc函式開辟的空間進行使用之前要判斷是否為回傳空指標,
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int* p = (int*)malloc(10000000000000000);
//開辟失敗,回傳空指標沒有判斷就使用
//使用
int i = 0;
for (i = 0; i < 6; i++)
{
printf("%d ", *(p + i));
}
}
👀對動態開辟空間的越界訪問
其實這個很好理解,就像靜態的創建10個陣列元素,你可不能訪問20個元素啊;如下面的代碼,malloc動態的開辟了10整形大小的空間,下面使用空間時卻訪問了40個整形元素,這也是不合法的,
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
return 1;
}
int i = 0;
//越界訪問
for (i = 0; i < 40; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
🔧🔩對非動態開辟記憶體使用free釋放
通過什么方式創建,就要通過什么方式釋放
int main()
{
int arr[10] = { 0 };//堆疊區
int* p = arr;
//使用
free(p);//使用free釋放非動態開辟的空間
p = NULL;
return 0;
}
??使用free釋放一塊動態開辟記憶體的一部分free(p),p一定要指向開辟空間的起始位置,這樣才能釋放開辟的整塊動態空間,如果p因為使用原因進行了移動一定要定義另一個指標記錄p的開始指向位置,否則進行的記憶體釋放是區域的記憶體釋放,下面代碼中通過指標的移動對空間內容進行了賦值,p發生了移動,但沒有記錄p的起始位置,
int main()
{
int* p = malloc(10 * sizeof(int));
if (p == NULL)
{
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*p++ = i;
}
free(p);
p = NULL;
return 0;
}
🎸🎧對同一塊動態記憶體多次釋放
對于開辟的一塊動態空間,一次釋放就行,但有時由于程式的復雜,在多個函式里面使用這塊空間,也就可能會進行多次釋放,
int main()
{
int* p = (int*)malloc(100);
//使用
//釋放
free(p);
p = NULL;
test()
{
//釋放
free(p);
return 0;
}
}
👻🤡動態開辟記憶體忘記釋放(記憶體泄漏)
如下p是區域變數,出了這個函式就銷毀了,下面的main 函式里面就不能使用了,之后就找不到p原來指向的空間的內容了,下面就不能把這塊空間釋放掉,造成記憶體泄漏,時間一長會消耗很多記憶體,如果服務器里面有記憶體泄漏,導致這個服務器崩潰,
void test()
{
int* p = (int*)malloc(100);
if (p == NULL)//p是區域變數,出了這個函式就銷毀了
{
return;
}
else
{
//使用
}
//一塊空間開辟了自己用,用完了一定要記得釋放,這里不釋放,下面就不能把這塊空間釋放掉,造成記憶體泄漏
}
int main()
{
test();//函式呼叫
//....
return 0;
}
🎓幾個經典的筆試題
📝題目一:
首先思考這個代碼有哪些問題??
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;
}
這個代碼有兩個問題:
?1. 對空指標的使用
? 2. 存在記憶體泄漏
圖示解釋📐????

??代碼修改的兩種方法
??在函式呼叫時,傳值呼叫是無法改變實參的大小的,要傳地址,
//改1
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;
}
//改:2
//char* GetMemory(char* p)//改變函式的回傳值,回傳動態開辟空間的起始地址(char*)
//{
// p = (char*)malloc(100);
// return p;
//}
//void Test(void)
//{
// char* str = NULL;
// str = GetMemory(str);
// strcpy(str, "hello world");
// printf(str);//?
// //printf("hello world");//char *p = "hello world";
// free(str);
// str = NULL;
//}
//int main()
//{
// Test();
// return 0;
//}
📝題目二:
?靜態開辟的空間是在堆疊上,堆疊上開辟的空間出了作用域就銷毀了,動態開辟的空間是在堆上開辟的,要么自己手動釋放空間,要么程式結束自動釋放空間,所以下面的代碼,隨機列印記憶體中的一些值,沒有達到預想到的效果,
//GetMemory 函式內部創建的陣列是在堆疊區上創建的
//出了函式,p陣列的空間就還給了作業系統
//回傳的地址是沒有實際的意義,如果通過回傳的地址,去訪問記憶體就是非法訪問記憶體的
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);//沒有回傳確定的地址,隨機列印記憶體中的一些值
}
📝題目三
?回傳&x是無效的,區域變數回傳地址是無效的
int test (void)
{
int x = 10;
return (&x);
}
📝題目四
?區域變數不初始化,隨機賦值,導致指向不明確,解參考有問題,野指標問題,
int test(void)
{
int *x;
*x = 10;
return x;
}
📝題目五
?開辟空間沒有free釋放掉
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
int main()
{
Test();
return 0;
}
📝題目六
?動態空間開辟好后,free釋放開辟的空間,那么維護開辟空間起始位置的指標也應該置為空指標,這是緊緊相連的步驟,不可缺少,雖然free(str)釋放了開辟好的空間,但str里面任然存盤著開辟空間的起始地址,free不會釋放strl里面的內容的,導致非法訪問,導致程式錯誤,
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
//這里可以加上str=NULL就對了
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
🎓 C/C++程式的記憶體開辟
💡C/C++程式記憶體分配的幾個區域如下圖:
不同的變數,程式在記憶體中占有不同的區域,理解他們所在的區域,理解他們的作用域與生命周期,可以幫助我們更好地撰寫程式,

- 堆疊區(stack):在執行函式時,函式內區域變數的存盤單元都可以在堆疊上創建,函式執行結束時這些存盤單元自動被釋放,堆疊記憶體分配運算內置于處理器的指令集中,效率很高,但是分配的記憶體容量有
限, 堆疊區主要存放運行函式而分配的區域變數、函式引數、回傳資料、回傳地址等, - 堆區(heap):一般由程式員分配釋放, 若程式員不釋放,程式結束時可能由OS回收 ,分配方式類似
于鏈表, - 資料段(靜態區)(static)存放全域變數、靜態資料,程式結束后由系統釋放,
- 代碼段:存放函式體(類成員函式和全域函式)的二進制代碼,
編譯產生的可執行代碼(可執行程式)是放在代碼段區域的,常量字串
也是放在代碼段里面的,
💡有了這幅圖,我們就可以更好地理解static關鍵字修飾的區域變數了
實際上普通的區域變數是在堆疊區分配空間的,堆疊區的特點是在上面創建的變數出了作用域就銷毀,
但是被static修飾的變數存放在資料段(靜態區),資料段的特點是在上面創建的變數,直到程式結束才銷毀所以生命周期變長,
🎓 柔性陣列
柔性陣列(flexible array)這個概念我們很少聽到,但是它確實是存在的, C99 中,結構中的最后一個元素允許是未知大小的陣列,這就叫做柔性陣列成員,我自己對于柔性陣列的理解就是,柔性陣列是定義在結構體當中的一個成員,它的起始大小為零,在使用程序中,根據情況的需要,通過動態記憶體開辟函式改變它的大小,達到陣列內容改變的效果,
如:
struct S
{
int n;
int arr[];//大小是未知
// int arr[0];//大小是未知,這種柔性陣列的寫法也對,大小未知,不是柔性陣列,這個寫法就是非法的
};
??柔性陣列的特點:
- 結構體中的柔性陣列成員前面必須至少一個其他成員,
- sizeof 回傳的這種結構大小不包括柔性陣列的記憶體,
- 包含柔性陣列成員的結構用malloc ()函式進行記憶體的動態分配,并且分配的記憶體應該大于結構的大小,以適應 柔性陣列的預期大小,
?? 如下代碼進行結構體的大小的計算:
#include<stdio.h>
struct s
{
int i;
int a[];//柔性陣列成員
}a;
int main()
{
printf("%d\n", sizeof(struct s ));//輸出的是4
return 0;
}
下面為柔性陣列的簡單使用:
代碼一
首先陣列元素個數為10個,不夠時再動態的開辟為20個
#include<stdio.h>
struct S
{
int n;//4
int arr[0];//大小是未知
};
int main()
{
//期望arr的大小是10個整形
struct S*ps = (struct S*)malloc(sizeof(struct S)+10*sizeof(int));
ps->n = 10;
int i = 0;
//使用柔性陣列
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
//增加陣列元素個數
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S)+20*sizeof(int));
if (ptr != NULL)
{
ps = ptr;
}
//使用和上面的一樣
//釋放
free(ps);
ps = NULL;
return 0;
}
/
🙉可能有老鐵覺得,柔性陣列的存在是多余的,沒有柔性陣列,我也可以可以創建一個結構體的陣列成員,實作動態的變化,那我們通過下面的代碼簡單的來分析一下吧,我們不使用柔性陣列,實作上面柔性陣列的功能,對比一下
代碼二
#include<stdio.h>
struct S
{
int n;
int* arr;
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL)
return 1;
ps->n = 10;
ps->arr = (int*)malloc(10 * sizeof(int));
if (ps->arr == NULL)
return 1;
//使用陣列
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
//增加
int*ptr = realloc(ps->arr, 20 * sizeof(int));
if (ptr != NULL)
{
ps->arr = ptr;
}
//使用和上面的一樣只是陣列元素改變
//釋放
free(ps->arr);
ps->arr = NULL;
free(ps);
ps = NULL;
return 0;
}

🙉通過以上代碼和圖示分析,為了實作柔性陣列的功能,我們通過上面的代碼會存在如下的問題:
上述 代碼1 和 代碼2 可以完成同樣的功能,但是 方法1 的實作有兩個好處:
第一個好處是:方便記憶體釋放包含柔性陣列的空間開辟只需要一次malloc,不包含的需要兩次malloc,兩次free釋放記憶體
第二個好處是:再由空間區域性原理可知,這樣有利于訪問速度.連續的記憶體有益于提高訪問速度,也有益于減少記憶體碎片,多次開辟堆上面的空間,導致堆空間殘留許多未被利用的記憶體塊(記憶體碎片),占用記憶體,影響程式運行,
上面所寫的內容只是一些基礎的知識點,和本篇博客內容相關的博客文章可以參考這位大佬的文章:C語言結構體里的陣列和指標
擴展閱讀:
《高質量c/c++編程》這本書里面后面的附錄里面有本篇博客一些筆試題的原題,及其他的一些題目知識點,其次這本書對于我們形成好的代碼風格,撰寫高質量的程式也有幫助,值得一讀,網盤分享鏈接鏈接:https://pan.baidu.com/s/1AOUSdyas7N1YP-XqQ2rFZQ
提取碼:6666
記🉐點👍🏻🍹持哦
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/289863.html
標籤:其他
上一篇:【Ubuntu 奇淫技巧】Ubuntu20.04 配置SSH & 遠程桌面
下一篇:python資料結構之資料型別
