記憶體的分配與釋放,記憶體洩漏

xumenger發表於2019-05-12

初始化的重要性

和在使用一個資料之前必須要對資料進行初始化一樣,否則可能會使得資料的值不確定,那就會給程式埋下很大的隱患,在使用指標之前也必須要對指標進行”初始化“,參見下面的例程1:

#include<stdio.h>
int main(void)
{
    int *x;
    *x = 3;
    return 0;
}

這樣的程式碼可能會出現段錯誤,因為x指標不知道會指向哪一塊記憶體,使用*x=3來更改那塊記憶體的資料有可能訪問到非法記憶體導致段錯誤,當然也有可能因為沒訪問到非法記憶體而沒有產生段錯誤,但是一個健壯的程式不允許存在這樣的隱患。

再看下面的一個例程2:

#include<stdio.h>
#include<stdlib.h>
int main(void)
{
    int *x;
    x = (int *)malloc(sizeof(int));
    //上面一行程式碼相當於對指標的初始化,使得指標指向一個合法的記憶體區域
    //準確的表述應該是,使用malloc動態分配一塊sizeof(int)大小的記憶體空間,然後讓x指向這塊記憶體
    *x = 3;    
    //上面這行程式碼的方式就不會有訪問非法記憶體的可能,就不會產生段錯誤
    free(x);
    return 0;
}

指向非法記憶體的指標

通過指標釋放記憶體後別忘了將指標置為NULL,或者將這個指標指向另一個合法的記憶體地址,總之不能再有指標指向被釋放過了的非法的記憶體空間。

上面的例程2中直接呼叫free來釋放記憶體,因為這是一個很簡短的程式所以執行的效果會正如你希望的那樣,但是假如在一個比較大型的專案中,這樣的寫法就存在著隱患,更為穩定的程式應該這樣寫,見例程3:

#include<stdio.h>
#include<stdlib.h>
int main(void)
{
    int *x;
    x = (int *)malloc(sizeof(int));
    //上面一行程式碼相當於對指標的初始化,使得指標指向一個合法的記憶體區域
    *x = 3;    
    //上面這行程式碼的方式就不會有訪問非法記憶體的可能,就不會產生段錯誤
    free(x);
    x = NULL;
    return 0;
}

有人會說這不是多此一舉嗎,確實在這個例程裡面是多此一舉,因為這個程式太過簡單,free(x) 之後,程式就執行結束了,不會存在再通過x訪問被釋放了的記憶體的非法訪問記憶體的問題。

但是假如在一個比較大的程式中,在程式的某個地方使用free(x)釋放了x所指向的記憶體空間(free操作只是釋放該指標所指向的記憶體,並不會將指標置為NULL),但是忘記將x指標置為NULL,那麼x將還會指向這塊記憶體,假如後面通過if(x==NULL) 來進行判斷,顯然x不等於NULL,就可能出現非法訪問x所指向的記憶體(但是這塊記憶體之前已經被釋放過了)的情況,顯然會出現很嚴重的錯誤。

另外需要強調的一點,有可能有多個指標指向同一塊記憶體,所以這種時候如果釋放了記憶體空間的話,必須保證所有指向這塊記憶體的指標都不再指向這塊記憶體,可以是置為NULL,當然也可以使其指向其他合法的記憶體地址。

下面以一個簡單的例程4展示free之後的指標將還指向原來的記憶體

#include<stdio.h>
int main(){
    int *x;
    x = (int*)malloc(sizeof(int)); //為x動態分配一塊記憶體,記憶體大小為int型大小
    *x = 3; //將3儲存到分配的記憶體中去
    printf("%d
 ", x); //以整數形式輸出指標,也就是對應的記憶體的地址
    printf("%d
", *x); //輸出記憶體中儲存的數值
    
    free(x);
    if(x != NULL){
        printf("%d
", x); //將記憶體釋放後再輸出指標的值
        printf("%d
", *x); //看看釋放了記憶體之後還能不能通過指標訪問這塊記憶體
    }
    
    x=NULL;
    printf("%d
", x); //將指標置為NULL之後再輸出指標的值
    printf("%d
", *x); //看看將指標置為NULL之後,再用*x會有什麼效果
    
    return 0;
}

使用gcc編譯的時候(我暫時將原始檔命名為test1.c,使用gcc test1.c -o test1來編譯),會有關於不正確使用指標的警告資訊,但是並不是語法錯誤,所以還是可以編譯通過,但是這就存在潛藏的大問題了。

最後執行輸出的結果是:

29540368
3
29540368
0
0
Segmentation fault (core dumped)

所以可以清晰的看出,通過指標釋放了動態分配的記憶體之後,指標還是指向原來的地址,還可以訪問原來的地址(不過原來的地址中的值可能變了),而最後將指標置為NULL之後,顯然指標不再指向原來的地址,而且如果這時候再想通過指標訪問對應的記憶體,就會報段錯誤。

這個程式演示了幾種不規範的使用指標的方法:

- 使用free釋放了記憶體之後沒有將指標置為NULL或者將指標再次指向另一個合法的記憶體而導致的再次訪問到已經被釋放了的記憶體的情況。Free之後而沒有將指標置為NULL或者再次指向合法的地址,然後根據`if(pointer != NULL)`來進行判斷指標是否合法其實是沒有意義的。
- 指標置為NULL之後錯誤的通過*運算子取指標所指向記憶體的資料而導致的段錯誤。

另外,關於這個小的測試程式,有一個關於printf使用的問題,我已經在問答網站上問了,請參見:http://segmentfault.com/q/101…

補充:使用Delphi開發語言進行開發的時候,Delphi的類、物件名也需要注意釋放的問題。直接以一個例程5講解

    var
        objectA : ClassA;  {宣告一個ClassA型別的變數}
    begin
        objectA := ClassA.Create;  {為objectA建立實體,要注意的是Delphi的物件的變數名其實就是一個指標}
                                  {所以這裡會建立一個實體,objectA其實是指標,會指向這個實體}
                                  
        {使用物件進行一些操作}
        
        objectA.Free;    {最後釋放變數}
        objectA:= nil;  {別忘了將變數置為nil,因為Delphi中的類的物件名就是指標}
    end;

說明:Delphi的物件導向程式設計中,一個物件名就相當於一個指標!

可能出現記憶體洩漏的幾種情況

情況1

多次malloc但是沒有對應的釋放次

這樣一個C語言的例子

#include<stdio.h>
int main(){
    int *x;
    int i;
    for(i=0; i<=100; i++)
        x = (int *)malloc(size(int));
    free(x);
    x = NULL;
    return 0;
}

在這個例子中,多次用malloc分配記憶體,然後每次將新分配的記憶體的地址賦值給x,但是x只能指向一個地址,所以最後x只能指向最後一次分配的記憶體的地址,所以也就只能釋放最後一次分配的記憶體,而前99次分配的記憶體因為丟失了地址,所以沒有辦法釋放,只能造成記憶體洩漏。

類似的dephi的例子可能是這樣的:

var
    i: Integer;
    objectA: ClassA;
begin
    for i:=0 to 100 do
    begin
        objectA:= ClassA.Create;
    end;
    
    {...}
    objectA.Free;
    objectA:= nil;
end;

這個delphi的程式,每次ClassA.Create就在記憶體中建立一個實體,然後將objectA指向這個實體的記憶體,但是和上面的C程式類似,objectA只能指向一個地址,所以只能指向最後一次建立的物件實體的地址,所以前面99個建立的物件因為丟失了地址,所以沒有辦法釋放,所以也就造成了記憶體洩露。

當然也有時候可以這樣使用:就是在某種情況下,delphi的某個執行緒類是可以執行結束的(不是無限迴圈執行的),並且將FreeOnTerminate設為True(表示執行緒執行結束之後會自動釋放執行緒的相關資源),這時候可以以這樣的方式建立多個不使用指標儲存其記憶體實體地址的執行緒類。但是這樣的情況也實在是少,就算是可以採取這樣的策略,我還是建議選擇用個指標儲存其地址以保證更為穩妥。總而言之,Delphi物件導向的一個好的程式設計規範是:建立類的物件實體,要有一個物件名(指標)儲存這個實體的地址。

情況2

動態分配的記憶體還沒有釋放就直接將指向該記憶體的指標置為NULL

這樣的一個C語言的例子

#include<stdio.h>
int main(){
    int *x;
    x = (int *)malloc(sizeof(int));
    //...
    x = NULL;
    //free(x);
    return 0;
}

這樣的程式分配了記憶體空間,然後x指向這塊記憶體(注意不要這樣表述:使用malloc為x指標分配了空間,這樣的表達方式是錯誤的,而應該是malloc分配了記憶體空間,然後x指標指向這塊記憶體空間)。但是卻將x置為NULL,這時候原來分配的記憶體空間的地址就丟失了,就沒有辦法對其釋放,所以就會導致記憶體洩漏。

另外關於free的引數是空指標的問題(注意也不要表達成:free釋放指標,應該是:釋放指標所指向的記憶體空間),我在問答網站上提問了,請參見:http://segmentfault.com/q/101…

類似的Delphi的程式可能是這樣的:

var
    ObjectA: ClassA;
begin
    ObjectA:= ClassA.Create;
    {...}
    ObjectA:= nil;
    //ObjectA.Free;    
end;

思考和總結

你可能會說,這種錯誤太明顯了,我怎麼可能會犯呢?

但是,當一個專案特別大的時候,有上萬行,甚至數十萬行的程式碼的時候,你還能保證不因為你的不細心或者各種意料之外的偶然而不出現這種情況嗎?



相關文章