C程式記憶體洩露檢測工具——Valgrind

ARM的程式設計師敲著詩歌的夢發表於2020-04-04

緣起

C/C++程式設計師需要親力親為地管理記憶體,一不小心就會造成“記憶體洩露”。說到這兒,有的同學會問:記憶體洩露是什麼意思?

以下摘自 維基百科

在電腦科學中,記憶體洩漏指由於疏忽或錯誤造成程式未能釋放已經不再使用的記憶體。記憶體洩漏並非指記憶體在物理上的消失,而是應用程式分配某段記憶體後,由於設計錯誤,導致在釋放該段記憶體之前就失去了對該段記憶體的控制,從而造成了記憶體的浪費。

除了在編碼時小心翼翼,還有什麼辦法能避免記憶體洩露呢?這裡為大家介紹一個工具——Valgrind. 它的官網是 http://valgrind.org/

正如官網所說:

Valgrind is an instrumentation framework for building dynamic analysis tools. There are Valgrind tools that can automatically detect many memory management and threading bugs, and profile your programs in detail. You can also use Valgrind to build new tools.

Valgrind 提供了許多除錯和分析工具,這些工具中最流行的稱為 Memcheck. 它可以檢測出許多在C/C ++程式中常見的與記憶體有關的錯誤。

Valgrind 的安裝

圖個省事,我們們就不從原始碼安裝了。

sudo apt install valgrind

這是一個沒有介面的記憶體檢測工具。安裝後,輸入valgrind ls -l驗證一下該工具是否能用。(這是README裡面的方法,實際上是執行對ls -l命令的記憶體檢測。)如果你看到類似下圖的資訊,說明安裝成功。
這裡寫圖片描述

Valgrind 的使用

首先準備好原始碼,然後進行編譯。官網是這樣說的:

Compile your program with -g to include debugging information so that Memcheck’s error messages include exact line numbers. Using -O0 is also a good idea, if you can tolerate the slowdown. With -O1 line numbers in error messages can be inaccurate, although generally speaking running Memcheck on code compiled at -O1 works fairly well, and the speed improvement compared to running -O0 is quite significant. Use of -O2 and above is not recommended as Memcheck occasionally reports uninitialised-value errors which don’t really exist.

總之要加-g選項,另外優化級別建議0或1級別,即加-O0或者-O1.

編譯後,假設生成的可執行檔案是a.out,那麼請輸入

    valgrind  ./a.out

如果你的程式有命令列引數,那麼跟在後面就行。比如

    valgrind  ./a.out arg1 arg2

舉例

我的上一篇博文——堆的C語言實現——堆與堆排序(二)講了堆的初始化、構造、插入、刪除等,剛好用到了mallocfree函式,我們們就拿這個舉例。

只分配,不釋放

//測試函式
int main(void)
{
    int a[]={4,1,3,2,16,9,10,14,8,7}; //10個
    int elmt_cnt = sizeof(a) / sizeof(a[0]);

    priority_queue pq1,pq2,pq3,pq4;

    // 測試最大堆建構函式
    pq1 = build_max_heap_bottom_up_recursive(a,elmt_cnt);
    pq2 = build_max_heap_bottom_up(a,elmt_cnt); 
    pq3 = build_max_heap_up_bottom(a,elmt_cnt);
    pq4 = build_max_heap_up_bottom2(a,elmt_cnt);

    // 列印構造結果
    print_heap(pq1);
    print_heap(pq2);
    print_heap(pq3);
    print_heap(pq4);

    // 測試刪除最大鍵的函式
    int del_key = 0;

    printf("delete the max key: ");
    while(is_empty(pq3)==0) // 用pq3測試
    {
        del_key = delete_max(pq3);
        printf("%d ", del_key);
    }
    printf("\n");

   // destroy(pq1); //別忘了釋放記憶體!
   // destroy(pq2);
   // destroy(pq3);
   // destroy(pq4);   

    return 0;   
}

把後面的destroy函式都註釋掉,編譯後用 Valgrind 工具檢測,結果如下圖。
這裡寫圖片描述

Valgrind 報告顯示,在程式退出的時候,還有 240 位元組的記憶體在使用,也就是沒有釋放。
我們看看原始碼,這240位元組是怎麼來的?
不管用什麼方式構造堆,都會呼叫初始化函式:


struct heap_struct
{
    int capacity;
    int size;
    element_t *elements;
};
...
priority_queue initialize(int capacity)
{
    priority_queue heap;

    ...
    heap = malloc( sizeof(struct heap_struct));
    ...

    // +1 是因為要包含[0]
    heap->elements = malloc( (capacity + 1) * sizeof(element_t) );
    ...

    ...
    return heap;
}

我們計算一下這個初始化函式需要申請多少記憶體。結構體struct heap_struct佔用的記憶體是 16(=4+4+8,我的系統是 x64 的 Ubuntu,指標佔8位元組)。heap->elements指向的空間大小是44(=4*11)。所以共申請記憶體60(= 16+44)位元組。

main函式中構造了4個堆,所以共申請記憶體240(=60*4)位元組,這和檢測結果相符。

分配後正確釋放

加上main函式最後的destroy(pq1)等4條語句,編譯後再用 Valgrind 檢測,結果如下圖。
這裡寫圖片描述
可以看出,程式退出時,還在使用的記憶體大小是0;所有的堆空間都被釋放,沒有記憶體洩露。

【完】

參考資料
https://blog.csdn.net/gatieme/article/details/51959654

相關文章