目錄
一、為什么存在動態內存分配
二、動態內存函數的介紹
2.1malloc
2.2free
2.3calloc
2.4realloc
三、常見的動態內存錯誤
3.1對NULL指針的解引用操作
3.2對動態開辟空間的越界訪問
3.3對非動態開辟內存使用free釋放
3.4使用free釋放一塊動態開辟內存的一部分
3.5對同一塊動態內存多次釋放
3.6動態開辟內存忘記釋放(內存泄漏)
四、經典筆試題
🌴題目一:?
?🌴題目二:?
?🌴題目三:?
??🌴題目四:?
五、C/C++程序的內存開辟
六、柔性數組
6.1柔性數組的特點
?6.2柔性數組的使用
6.3柔性數組的優勢
結語:
一、為什么存在動態內存分配
截止目前我們已經掌握的開辟空間的方式有以下兩種:
int main()
{//在棧空間上開辟四個字節,存放一個值int a = 10;//在棧空間上開辟10個字節的連續空間,存放一組數int arr[] = { 1,2,3,4,5,6,7,8,9,10 };return 0;
}
但是上述開辟空間的方式有兩個特點:
- 空間開辟的大小是固定的。
- 數組在聲明的時候,必須指定數組的長度,它所需要的內存在編譯時分配。
但是對于空間的需求,不僅僅是上述的情況。有時候我們需要的空間大小在程序運行的時候才能知道,那數組在編譯時開辟空間的方式就不能滿足了。這個時候C語言給了我們程序員一種權利:能夠動態申請和管理內存空間,即動態內存的開辟。有了這種權力,我們就能很好的解決上述所面臨的問題了。
二、動態內存函數的介紹
?🌴函數頭文件都是:
#include <stdlib.h>
2.1malloc
"malloc"函數用于動態分配指定大小的內存空間,并返回一個指向該內存空間的指針。
🌴函數原型:?
void* malloc (size_t size);
- 其中,size參數表示需要分配的內存空間的大小,單位為字節。
- 函數返回一個指向分配的內存空間的指針,如果分配失敗則返回 NULL。
?🌴函數用法示例:
#include <stdlib.h>
#include <stdio.h>int main()
{//申請一塊空間,用來存放10個整型int* ptr = (int*)malloc(10 * sizeof(int));//如果返回NULL,則代表空間申請失敗,并打印失敗原因if (ptr == NULL){perror("malloc");return 1;}int i = 0;//因為這個函數返回一個指向分配好的內存塊開頭的指針,//所以ptr這個指針變量相當于這塊內存空間的首地址for (i = 0; i < 10; i++){*(ptr + i) = i;}for (i = 0; i < 10; i++){printf("%d ", ptr[i]);}return 0;
}
🌴打印結果:
?🌴注意事項:?
- 如果開辟成功,則返回一個指向開辟好空間的指針。
- 如果開辟失敗,則返回一個NULL指針,因此malloc的返回值一定要做檢查。
- 返回值的類型是 void*?,所以malloc函數并不知道開辟空間的類型,具體在使用的時候由使用者自己來決定。
- 如果參數 size 為0,malloc的行為是標準是未定義的,取決于編譯器。
malloc函數申請的空間有兩種釋放方式:
- free釋放 ---?主動回收。
- 程序退出后,malloc申請的空間會被操作系統回收 --- 被動回收。
注:正常情況下,誰申請的空間,誰去釋放,萬一自己不釋放,也要交代給別人記得釋放。
2.2free
動態分配內存允許程序在運行時分配和釋放內存,這對于處理大量數據或者需要靈活管理內存的程序非常重要。但是,如果不及時釋放已經分配的內存,就會導致內存泄漏,最終可能導致程序崩潰。在C語言中,我們可以使用free()函數釋放已經分配的內存。
?🌴函數原型:?
void free (void* ptr);
?free函數非常簡單,它只需要傳入一個指向已經分配的內存塊的指針,就可以將該內存塊釋放回操作系統。
??🌴函數用法示例:
#include <stdlib.h>
#include <stdio.h>int main()
{//申請一塊空間,用來存放10個整型int* ptr = (int*)malloc(10 * sizeof(int));//如果返回NULL,則代表空間申請失敗,并打印失敗原因if (ptr == NULL){perror("malloc");return 1;}*ptr = 10;printf("ptr指向的值為:%d\n", *ptr);free(ptr);ptr = NULL;return 0;
}
🍄解析:
- ?在上述代碼中,首先使用malloc函數分配一個整型變量的內存空間,并將其賦值給指針變量ptr。然后,將整型變量的值設置為10,并輸出該值。最后,使用free函數釋放了該內存空間。
- 需要我們清楚的一點是,malloc的作用是申請一塊空間供當前程序使用,而free函數的作用是把申請的這塊空間還給操作系統。然而,free雖然將空間還給了操作系統,但指針變量ptr卻還記得申請的這塊空間的地址,這時候它就變成了一個野指針,這是非常危險的,所以我們要在free釋放完空間后將ptr置為空指針NULL。
🌴注意事項:?
- 如果參數 ptr 指向的空間不是動態開辟的,那free函數的行為是未定義的。
- 如果參數 ptr 是NULL指針,則函數什么事都不做。
2.3calloc
"calloc"函數和"malloc"函數相似,都是用來分配內存的。不同之處在于"calloc"函數在分配內存時會將內存中的每個字節初始化為0。
?🌴函數原型:
void* calloc (size_t num, size_t size);
- "calloc"函數接收兩個參數,分別是要分配的元素個數和每個元素的大小。
- 它會分配總共 num*size 個字節的內存,并將這些內存空間初始化為0。?
???🌴函數用法示例:
#include <stdio.h>
#include <stdlib.h>int main()
{int* ptr = (int*)calloc(10, sizeof(int));if (ptr == NULL){perror("calloc");return 1;}//打印int i = 0;for (i = 0; i < 10; i++){printf("%d ", *(ptr + i));}//釋放free(ptr);ptr = NULL;return 0;
}
🍄解析:
?
在上述代碼中,首先使用calloc函數分配了10個整型變量的內存空間 ,并將其賦值給指針變量ptr。由于calloc函數會將分配的內存空間初始化為0,因此在使用循環輸出ptr指向的內存空間時,會發現所有的值都是0。最后,使用free函數釋放了該內存空間。所以如果我們對申請的內存空間的內容要求初始化,那么可以很方便的使用calloc函數來完成任務。
2.4realloc
有時會我們發現過去申請的空間太小了,有時候我們又會覺得申請的空間過大了,那為了合理的使用內存,我們一定會對內存的大小做靈活的調整,那 realloc 函數就可以做到對動態開辟內存大小的調整,即realloc函數用于重新分配之前通過malloc、calloc或realloc函數分配的內存塊的大小。
??🌴函數原型:
void* realloc (void* ptr, size_t size);
- ptr:之前分配的內存塊的指針。
- size:新的內存塊的大小。
- 返回值為調整之后的內存起始位置。
- realloc函數會嘗試將之前分配的內存塊的大小改變為size字節,并返回指向新內存塊的指針。如果無法滿足新的大小要求,realloc函數可能會在不同的位置重新分配內存塊,并將原內存塊的內容復制到新的內存塊中。
????🌴函數用法示例:
#include <stdio.h>
#include <stdlib.h>int main()
{//分配10個整型的內存塊int* ptr = (int*)calloc(10, sizeof(int));if (ptr == NULL){perror("calloc");return 1;}//初始化內存塊int i = 0;for (i = 0; i < 10; i++){ptr[i] = i;}//打印for (i = 0; i < 10; i++){printf("%d ", ptr[i]);}//空間不夠,希望調整空間為20個整型的空間int* p = (int*)realloc(ptr, 20 * sizeof(int));if (p != NULL){ptr = p;}//釋放free(ptr);ptr = NULL;return 0;
}
?🌴注意事項:??
1.realloc函數在調整內存空間時存在兩種情況:
🍄情況1:原有空間之后有足夠大的空間:
?
?🍄情況2:原有空間之后沒有足夠大的空間:
2.如果ptr為NULL,則realloc的行為類似于free,即它將分配一個新的內存塊。?
int main()
{int* p = (int*)realloc(NULL, 10 * sizeof(int));if (p == NULL){perror("realloc");return 1;}free(p);p = NULL;return 0;
}
三、常見的動態內存錯誤
3.1對NULL指針的解引用操作
int main()
{int* p = (int*)malloc(40);//不做返回值判斷,就可能使用NULL指針解引用,就會報錯*p = 20;return 0;
}
🍂這時候鼠標點上去編譯器就會提示你有錯誤:?
🌴正確寫法:
int main()
{int* p = (int*)malloc(40);if (p == NULL){perror("malloc");}//不做返回值判斷,就可能使用NULL指針解引用,就會報錯*p = 20;free(p);p = NULL;return 0;
}
3.2對動態開辟空間的越界訪問
int main()
{int* ptr = (int*)calloc(10, sizeof(int));if (ptr == NULL){perror("calloc");return 1;}//初始化內存塊int i = 0;for (i = 0; i <= 10; i++){ptr[i] = i;//當i是10的時候越界訪問}for (i = 0; i <= 10; i++){printf("%d ", ptr[i]);//當i是10的時候越界訪問}free(ptr);ptr = NULL;return 0;
}
3.3對非動態開辟內存使用free釋放
int main()
{int a = 10;int* p = &a;free(p);p = NULL;return 0;
}
上述代碼中的變量a是我們在棧上面開辟的空間,而free只能釋放malloc、calloc、realloc開辟的空間,它們都是在堆區上面開辟空間。
3.4使用free釋放一塊動態開辟內存的一部分
int main()
{int* ptr = (int*)calloc(10, sizeof(int));if (ptr == NULL){perror("calloc");return 1;}//初始化內存塊int i = 0;for (i = 0; i < 5; i++){*ptr = i;ptr++;}// 0 1 2 3 4 0 0 0 0 0free(ptr);ptr = NULL;return 0;
}
在上述代碼中,我們初始化內存塊的時候,剛開始*ptr指向所開辟的這塊空間的第一個元素,將它初始化為0,然后ptr++,指向第二個元素,初始化為1,直到循環結束,所開辟的這塊空間前5個元素就會被初始化為0 1 2 3 4,而因為它是calloc開辟的空間,所以剩下的5個元素就會被初始化為0 0 0 0 0。而現在當這個循環結束的時候ptr++,它會指向第6個元素,緊接著直接釋放內存,這個時候程序就會出現問題。所以我們要釋放也是全部釋放。
?
3.5對同一塊動態內存多次釋放
int main()
{int* p = (int*)malloc(40);if (p == NULL){//...return 1;}free(p);//...free(p);return 0;
}
解決辦法是第一次釋放完空間后將p置為NULL指針,因為當free函數的參數為NULL指針是它什么事也不做。
3.6動態開辟內存忘記釋放(內存泄漏)
void test()
{int* p = (int*)malloc(100);if (NULL != p){*p = 20;}
}
int main()
{test();while (1);
}
忘記釋放不在使用的動態開辟的空間會造成內存泄漏,切記:動態開辟的空間一定要釋放,并且正確釋放。
四、經典筆試題
🌴題目一:?
#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);
}//請問運行Test 函數會有什么樣的結果?
int main()
{Test();return 0;
}
🍂解析:
上述這段代碼運行起來后最終會崩潰。首先程序運行起來后先進入main函數,然后調用Test函數,進入Test函數后,創建指針變量str并將它置為NULL指針。緊接著再調用GetMemory函數,將實參str傳給形參p,并將str的值也一并傳給了形參,然后malloc開辟100個字節的空間并將它的起始地址返回給p,即p指向此時開辟的這100個字節的空間。當它返回被調用的GetMemory函數時,因為它是形參,一旦函數執行完畢,形參的值和狀態將不再保留,所以p所占用的內存空間將會被釋放,但malloc開辟的這塊空間還存在,此時因為p不見了,所以沒人能找到這塊空間。函數繼續往下執行,要將"hello world"拷貝到str指向的空間,但因為str是空指針,這兒必然會對空指針進行解引用操作,所以會出現程序崩潰的情況。而且程序運行結束時,malloc開辟的空間也沒有釋放,所以還會發生內存泄漏的情況。
🌻改正后的代碼:?
#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;
}
因為形參是實參的一份臨時拷貝,所以改變形參并不會影響到實參。在上面改進后的代碼中,我們將str的地址傳給形參,并用二級指針變量來接收,然后解引用p,找到str讓他指向malloc函數開辟的空間,就可以將"hello world"拷貝過去了。
?🌴題目二:?
char* GetMemory(void)
{char p[] = "hello world";return p;
}void Test(void)
{char* str = NULL;str = GetMemory();printf(str);
}
//請問運行Test 函數會有什么樣的結果?
int main()
{Test();return 0;
}
🍂解析:
在上述代碼中,我們還是先進入主函數,然后調用Test函數,進入Test函數內部首先創建str指針變量并將它置為NULL指針,然后調用GetMemory函數并將它的返回值賦給str,進入GetMemory函數內部,創建一個數組,里邊存放的是"hello world",這兒我們應該清楚這個p數組它是在棧上邊創建的,進入這個函數創建,出這個函數銷毀。然后返回p(此時p是數組名,代表的是數組首元素的地址),str接收返回的p,此時,str里邊存放的就是p的地址值,但是,雖然將地址值帶回來了,創建的那塊空間卻不屬于我了(因為出了函數后,創建的空間就會還給操作系統,它的使用權限就不屬于我了)。所以,當我們再去使用str指針的時候,它就會變成野指針,這種問題屬于返回棧空間地址的問題。??
要想解決這個問題也很簡單,第一種方法可以在局部變量char p[ ]前面加關鍵字static修飾,這樣它就變成了靜態局部變量,它會一直存在于內存中,直到程序結束。第二種方法是用malloc函數來開辟空間,只要程序不退出或主動回收,這塊空間就一直存在。
?🌴題目三:?
void GetMemory(char** p, int num)
{*p = (char*)malloc(num);
}void Test(void)
{char* str = NULL;GetMemory(&str, 100);strcpy(str, "hello");printf(str);
}
//請問運行Test 函數會有什么樣的結果?
int main()
{Test();return 0;
}
?🍂解析:
上面這段代碼雖然能打印出結果來,但它在使用完malloc開辟的空間后沒有將它釋放,所以說存在內存泄漏的問題。
??🌴題目四:?
void Test(void)
{char* str = (char*)malloc(100);strcpy(str, "hello");free(str);if (str != NULL){strcpy(str, "world");printf(str);}
}//請問運行Test 函數會有什么樣的結果?
int main()
{Test();return 0;
}
?🍂解析:
上面這段程序將"hello"拷貝到malloc函數開辟的空間后,直接將這塊空間釋放掉了,但str還是存著這塊空間的地址,所以在if語句中判斷時,它依然為真,那就進入函數內部,將"world"拷貝到str指向的這塊空間的地址,此時的str已經變成了野指針,對野指針進行操作,就是非法訪問內存。
其實這段代碼考察的點在free釋放完空間后有沒有將str置為NULL指針。
五、C/C++程序的內存開辟

?
🌴C/C++程序內存分配的幾個區域:
- ?棧區(stack):在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置于處理器的指令集中,效率很高,但是分配的內存容量有限。 棧區主要存放運行函數而分配的局部變量、函數參數、返回數據、返回地址等。
- 堆區(heap):一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 ,分配方式類似于鏈表。
- 數據段(靜態區):(static)存放全局變量、靜態數據。程序結束后由系統釋放。
- 代碼段:存放函數體(類成員函數和全局函數)的二進制代碼。
有了這幅圖,我們就可以更好的理解static關鍵字修飾局部變量的例子了。實際上普通的局部變量是在棧區分配空間的,棧區的特點是在上面創建的變量出了作用域就銷毀。但是被static修飾的變量存放在數據段(靜態區),數據段的特點是在上面創建的變量,直到程序結束才銷毀所以生命周期變長。
六、柔性數組
C99 中,結構中的最后一個元素允許是未知大小的數組,這就叫做『柔性數組』成員。
🎈例如:?
struct s
{char c;int i;int arr[0];//未知大小的數組--柔性數組成員
};
有些編譯器會報錯無法編譯可以改成:?
struct s
{char c;int i;int arr[];//未知大小的數組--柔性數組成員
};
6.1柔性數組的特點
1、結構中的柔性數組成員前面必須至少一個其他成員。
2、sizeof 返回的這種結構大小不包括柔性數組的內存。
struct s
{char c;//1int i;//4int arr[0];//未知大小的數組--柔性數組成員
};int main()
{printf("%d\n", sizeof(struct s));return 0;
}
?
3、包含柔性數組成員的結構用malloc ()函數進行內存的動態分配,并且分配的內存應該大于結構的大小,以適應柔性數組的預期大小。
struct s
{char c;int i;int arr[0];//未知大小的數組--柔性數組成員
};int main()
{struct s* pc = (struct s*)malloc(sizeof(struct s) + 20);if (pc == NULL){perror("malloc");return 1;}//釋放free(pc);pc = NULL;return 0;
}
?6.2柔性數組的使用
//代碼1
struct s
{char c;int i;int arr[];
};int main()
{struct s* pc = (struct s*)malloc(sizeof(struct s) + 20);if (pc == NULL){peror("malloc");return 1;}pc->c = 'w';pc->i = 100;for (int i = 0; i < 5; i++){pc->arr[i] = i;}for (int i = 0; i < 5; i++){printf("%d ", pc->arr[i]);}//空間不夠了,增容struct s* ptr = (struct s*)realloc(pc, sizeof(struct s) + 40);if (ptr != NULL){pc = ptr;}else{perror("realloc");return 1;}//增容成功后,繼續使用// ......//釋放free(pc);pc = NULL;return 0;
}
6.3柔性數組的優勢
🌵上述的代碼也可寫成下面這樣:?
//代碼2
struct s
{char c;int i;int* data;
};int main()
{struct s* pc = (struct s*)malloc(sizeof(struct s) + 20);if (pc == NULL){peror("malloc1");return 1;}pc->c = 'w';pc->i = 100;pc->data = (int*)malloc(20);if (pc->data == NULL){perror("malloc2");return 1;}for (int i = 0; i < 5; i++){pc->data[i] = i;}for (int i = 0; i < 5; i++){printf("%d ", pc->data[i]);}//空間不夠了,增容int* ptr = (int*)realloc(pc->data, 40);if (ptr != NULL){pc->data = ptr;return 1;}//釋放free(pc->data);pc->data = NULL;free(pc);pc = NULL;return 0;
}
上述代碼1 和 代碼2 可以完成同樣的功能,但是 方法1 的實現有兩個好處:
🍂第一個好處是:方便內存釋放
如果我們的代碼是在一個給別人用的函數中,你在里面做了二次內存分配,并把整個結構體返回給用戶。用戶調用free可以釋放結構體,但是用戶并不知道這個結構體內的成員也需要free,所以你不能指望用戶來發現這個事。所以,如果我們把結構體的內存以及其成員要的內存一次性分配好了,并返回給用戶一個結構體指針,用戶做一次free就可以把所有的內存也給釋放掉。
🍂第二個好處是:有利于訪問速度
連續的內存有益于提高訪問速度,也有益于減少內存碎片。
結語:
動態內存分配是一項強大而靈活的功能,它允許程序再運行時動態的分配和釋放內存。然而,與之相伴而來的是一些潛在的問題和風險。在使用動態內存時,我們需要特別注意內存泄漏、野指針、內存碎片等問題,以及合理的管理內存的生命周期。?