我知道的只是 「 肉隨便加 」和 「 要加多少加多少 」 這些詞。 ———— 路飛
階段2目標:
此階段開始大量刷題,多多參加程式設計類競賽,在實戰中鍛鍊程式設計思維和本領,並且要在不斷複習夯實初階的基礎上,刻意地進行程式設計思維的訓練。學無止境!為了精程序式設計,可以去學習一切為他服務的課程!
目錄
1.為什麼存在動態記憶體分配
2.動態記憶體函數的介紹
3.常見的動態記憶體錯誤
4.幾個經典的筆試題
5.柔性陣列
我們已經掌握的記憶體開闢方式有:
int val = 20;//在棧空間上開闢四個位元組
char arr[10] = {0};//在棧空間上開闢10個位元組的連續空間
但是,上述開闢空間的方式有兩個特點:
但是對於空間的需求,不僅僅是上述的情況。有時候我們需要的空間大小在程式執行的時候才能知道,那陣列的編譯時開闢空間的方式就不能滿足了。 這時候就只能用動態記憶體開闢了。
我們所知道,我們在堆區實現動態記憶體分配,利用malloc 、 calloc 、 realloc 、 free函數去進行相應的動態分配操作。如圖:
C語言提供了一個動態記憶體開闢的函數:
void* malloc( size_t size );
這個函數向記憶體申請一塊連續可用的空間,並返回指向這塊空間的指標。
解釋:
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;
}
C語言提供了另外一個函數free,專門是用來做動態記憶體的釋放和回收的,函數原型如下:
void free( void *memblock );
free函數用來釋放動態開闢的記憶體。
解釋:
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這個指標程序在某個時刻突然莫名其妙地崩潰。
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函數既申請記憶體空間,並且返回起始地址,又去初始化每個位元組為0。
總結:
在應用中,想要初始化就應用calloc,不想初始化就用malloc即可。
(追加增容)
realloc函數的出現讓動態記憶體管理更加靈活。
有時會我們發現過去申請的空間太小了,有時候我們又會覺得申請的空間過大了,那為了合理的時候記憶體,我們一定會對記憶體的大小做靈活的調整。那 realloc 函數就可以做到對動態開闢記憶體大小的調整。 函數原型如下:
void* realloc ( void* ptr, size_t size );
情況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;
}
錯誤寫法:
int* p = (int*)malloc(20);
*p = 20;//直接這樣寫,是有風險的!!
free(p);
改寫:
int* p = (int*)malloc(20);
if (p == NULL)
{
return -1;
}
*p = 20;//直接這樣寫,是有風險的!!
free(p);
錯誤寫法:
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;
}
錯誤程式碼:
int main()
{
int a = 10;
int* p = &a;
free(p);
p = NULL;
return 0;
}
該錯誤在於,變數a在記憶體的棧區,而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的脾氣 )。
錯誤程式碼:
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);是沒錯的,只不過是沒有用處的寫法罷了!
錯誤程式碼:
//動態開闢的記憶體忘記釋放
//在堆區申請的空間,有2種方式可以回收
//1.主動free
//2.程式結束時,作業系統會自動回收
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
return -1;
}
//使用
//...
//忘記釋放了
return 0;
}
錯誤原因:
我們在堆區開闢的記憶體,一直未釋放,就會一直存在與堆區,造成記憶體被佔用,對應到後端程式就是,執行變卡;這樣一塊記憶體,我們不能夠對他進行使用,也沒有去釋放掉,就造成了 「 記憶體漏失 」。
忘記釋放不再使用的動態開闢的空間會造成記憶體漏失。
#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的地址,進而完成字串的列印。
#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函數的棧幀(其實他已經被銷燬了)。
#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屬於非法存取
C/C++程式記憶體分配的幾個區域:
有了這幅圖,我們就可以更好的理解在《C語言初識》中講的static關鍵字修飾區域性變數的例子了:
實際上普通的區域性變數是在棧區分配空間的,棧區的特點是在上面建立的變數出了作用域就銷燬。但是被static修飾的變數存放在資料段(靜態區),資料段的特點是在上面建立的變數,直到程式結束才銷燬所以生命週期變長。
也許你從來沒有聽說過柔性陣列(flexible array)這個概念,但是它確實是存在的。 C99 中,結構中的最後一個元素允許是未知大小的陣列,這就叫做『柔性陣列』成員。
例如:
typedef struct st_type
{
int i;
int a[0];//柔性陣列成員
}type_a;
有些編譯器會報錯無法編譯可以改成:
typedef struct st_type
{
int i;
int a[];//柔性陣列成員
};
解釋:
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( )函數進行記憶體的動態分配,並且分配的記憶體應該大於結構的大小,以適應柔性陣列的預期大小。