1. 程式人生 > >C++記憶體洩漏檢查器

C++記憶體洩漏檢查器

專案介紹
記憶體洩漏一直是 C++ 中比較令人頭大的問題, 即便是很有經驗的 C++程式設計師有時候也難免因為疏忽而寫出導致記憶體洩漏的程式碼。除了基本的申請過的記憶體未釋放外,還存在諸如異常分支導致的記憶體洩漏等等。本專案將使用 C++ 實現一個記憶體洩漏檢查器。

專案涉及的知識點

1.new 操作符過載
2.__FILE__、__LINE__ 預定義巨集
3.標頭檔案中的靜態變數
4.std::shared_ptr 智慧指標

記憶體洩漏通常是由於疏忽而導致的申請記憶體未釋放。在現代作業系統中,一個應用程式使用的常規記憶體終止後這些未釋放的記憶體仍然會被作業系統回收,所以一個短暫執行的應用程式而導致的記憶體洩漏不會造成比較嚴重的後果。

但是,如果我們在編寫諸如伺服器一類的應用時,它會始終保持執行狀態,如果一旦存在發生記憶體洩露的邏輯,將很有可能繼續造成洩露的記憶體增加,從而嚴重降低計算機效能,甚至導致系統執行故障等。

一個長期執行並不斷佔用記憶體的程式非常簡單:

int main() {
    // 始終保持記憶體申請,但永不釋放
    while(1){
    //死迴圈
       int *a = new int;          }
    return 0;
}

要檢測一個長期執行的程式是否發生記憶體洩露通常是在應用中設定檢查點,分析不同檢查點中記憶體是否長期保持佔用而未釋放,從本質上來說,這與對一個短期執行的程式進行記憶體洩露檢查是非常類似的。所以本專案中的記憶體洩漏檢查器將實現一個用於短期記憶體洩露的檢查器。

我們不妨編寫下面的測試程式碼用於檢測我們的記憶體洩露,在這個程式碼中,我們刻意的不去釋放某個申請的記憶體,並刻意的去製造一個異常分支產生的記憶體洩露。

例如:
main.cpp

#include <iostream>

// 在這裡實現記憶體洩露檢查
#include "LeakDetector.hpp"

// 測試異常分支洩露
class Err {
public:
    Err(int n) {
        if(n == 0) throw 1000;
        data = new int[n];
    }
    ~Err() {
        delete[] data;
    }
private:
    int *data;
};

int main() {

    // 忘記釋放指標 b 申請的記憶體,從而導致記憶體洩露
    int *a = new int;
    int *b = new int;
    delete a;

    // 0 作為引數傳遞給建構函式將發生異常,從而導致異常分支的記憶體洩露
    try {
        Err* e = new Err(0);
        delete e;
    } catch (int &ex) {
        std::cout << "Exception catch: " << ex << std::endl;
    };
    return 0;

}

先註釋掉

//#include "LeakDetector.hpp"

那麼這段程式碼能夠正常的被執行,如下圖所示。但我們知道,這段程式碼中其實是發生了記憶體洩露的:
在這裡插入圖片描述

要實現記憶體洩露的檢查,我們可以從下面幾個點來思考:

記憶體洩露產生於 new 操作進行後沒有執行 delete
最先被建立的物件,其解構函式永遠是最後執行的
對應這兩點,我們可以做下面的操作:

過載 new 運算子
建立一個靜態物件,用於在原始程式退出時候才呼叫這個靜態物件的解構函式
這樣兩個步驟的好處在於:無需修改原始程式碼的情況下,就能進行記憶體檢查。這同時也是我們希望看到的。所以,我們可以在 LeakDetector.hpp 裡首先實現:

#ifndef __LEAK_DETECTOR__
#define __LEAK_DETECTOR__

void* operator new(size_t _size, char *_file, unsigned int _line);
void* operator new[](size_t _size, char *_file, unsigned int _line);
// 此處巨集的作用下一節實現 LeakDetector.cpp 時說明
#ifndef __NEW_OVERLOAD_IMPLEMENTATION__
#define new    new(__FILE__, __LINE__)
#endif

class _leak_detector
{
public:
    static unsigned int callCount;
    _leak_detector() noexcept {
        ++callCount;
    }
    ~_leak_detector() noexcept {
        if (--callCount == 0)
            LeakDetector();
    }
private:
    static unsigned int LeakDetector() noexcept;
};
static _leak_detector _exit_counter;

為什麼要設計 callCount? callCount 保證了我們的 LeakDetector 只調用了一次。考慮下面的簡化程式碼:

// main.cpp
#include <iostream>
#include "test.h"
int main() {
    return 0;
}
// test.hpp
#include <iostream>
class Test {
public:
    static unsigned int count;
    Test() {
        ++count;
        std::cout << count << ", ";
    }
};
static Test test;
// test.cpp
#include "test.hpp"
unsigned int Test::count = 0;

最後的輸出結果為 1, 2,

這是因為在標頭檔案中的靜態變數會被多次定義,每包含一次,都會在某個 .cpp 中被定義(這裡則在 main.cpp 和 test.cpp 中均有定義),但實際上他們均為同名,本質上還是同一個物件。

那麼現在的問題就只剩下了:如何實現記憶體洩露的檢測?

下面我們來逐步實現這個記憶體洩露檢查器。

既然我們已經過載了 new 操作符,那麼我們很自然就能想到通過手動管理記憶體申請和釋放,如果我們 delete 時沒有將申請的記憶體全部釋放完畢,那麼一定發生了記憶體洩露。接下來一個問題就是,使用什麼結構來實現手動管理記憶體?

不妨使用雙向連結串列來實現記憶體洩露檢查。原因在於,對於記憶體檢查器來說,並不知道實際程式碼在什麼時候會需要申請記憶體空間,所以使用線性表並不夠合理,一個動態的結構(連結串列)是非常便捷的。而我們在刪除記憶體檢查器中的物件時,需要更新整個結構,對於單向連結串列來說,也是不夠便利的。

建立LeakDetector.cpp檔案

#include <iostream>
#include <cstring>

// 在此處定義 _DEBUG_NEW_ 巨集, 
// 從而在這個實現檔案中不再繼續過載 new 運算子, 
// 從而防止編譯衝突
#define __NEW_OVERLOAD_IMPLEMENTATION__
#include "LeakDetector.hpp"

typedef struct _MemoryList {
    struct  _MemoryList *next, *prev;
    size_t     size;       // 申請記憶體的大小
    bool    isArray;    // 是否申請了陣列
    char    *file;      // 儲存所在檔案
    unsigned int line;  // 儲存所在行
} _MemoryList;
static unsigned long _memory_allocated = 0;     // 儲存未釋放的記憶體大小
static _MemoryList _root = {
    &_root, &_root, // 第一個元素的前向後向指標均指向自己
    0, false,               // 其申請的記憶體大小為 0, 且不是陣列
    NULL, 0                 // 檔案指標為空, 行號為0
};

unsigned int _leak_detector::callCount = 0;
// 從 _MemoryList 頭部開始分配記憶體
void* AllocateMemory(size_t _size, bool _array, char *_file, unsigned _line) {
    // 計算新的大小
    size_t newSize = sizeof(_MemoryList) + _size;

    // 由於 new 已經被過載,我們只能使用 malloc 來分配記憶體
    _MemoryList *newElem = (_MemoryList*)malloc(newSize);

    newElem->next = _root.next;
    newElem->prev = &_root;
    newElem->size = _size;
    newElem->isArray = _array;
    newElem->file = NULL;

    // 如果有檔案資訊,則儲存下來
    if (_file) {
        newElem->file = (char *)malloc(strlen(_file)+1);
        strcpy(newElem->file, _file);
    }
    // 儲存行號
    newElem->line = _line;

    // 更新列表
    _root.next->prev = newElem;
    _root.next = newElem;

    // 記錄到未釋放記憶體數中
    _memory_allocated += _size;

    // 返回申請的記憶體,將 newElem 強轉為 char* 來嚴格控制指標每次 +1 只移動一個 byte
    return (char*)newElem + sizeof(_MemoryList);
}

void  DeleteMemory(void* _ptr, bool _array) {
    // 返回 MemoryList 開始處
    _MemoryList *currentElem = (_MemoryList *)((char *)_ptr - sizeof(_MemoryList));

    if (currentElem->isArray != _array) return;

    // 更新列表
    currentElem->prev->next = currentElem->next;
    currentElem->next->prev = currentElem->prev;
    _memory_allocated -= currentElem->size;

    // 記得釋放存放檔案資訊時申請的記憶體
    if (currentElem->file) free(currentElem->file);
    free(currentElem);
}

// 過載 new 運算子
void* operator new(size_t _size) {
    return AllocateMemory(_size, false, NULL, 0);
}
void* operator new[](size_t _size) {
    return AllocateMemory(_size, true, NULL, 0);
}
void* operator new(size_t _size, char *_file, unsigned int _line) {
    return AllocateMemory(_size, false, _file, _line);
}
void* operator new[](size_t _size, char *_file, unsigned int _line) {
    return AllocateMemory(_size, true, _file, _line);
}
// 過載 delete 運算子
void operator delete(void *_ptr) noexcept {
    DeleteMemory(_ptr, false);
}
void operator delete[](void *_ptr) noexcept {
    DeleteMemory(_ptr, true);
}
unsigned int _leak_detector::LeakDetector(void) noexcept {
    unsigned int count = 0;
    // 遍歷整個列表, 如果有記憶體洩露,那麼 _LeakRoot.next 總不是指向自己的
    _MemoryList *ptr = _root.next;
    while (ptr && ptr != &_root)
    {
        // 輸出存在記憶體洩露的相關資訊, 如洩露大小, 產生洩露的位置
        if(ptr->isArray)
            std::cout << "leak[] ";
        else
            std::cout << "leak   ";
        std::cout << ptr << " size " << ptr->size;
        if (ptr->file)
            std::cout << " (locate in " << ptr->file << " line " << ptr->line << ")";
        else
            std::cout << " (Cannot find position)";
        std::cout << std::endl;

        ++count;
        ptr = ptr->next;
    }

    if (count)
        std::cout << "Total " << count << " leaks, size "<< _memory_allocated << " byte." << std::endl;
    return count;
}

最後編譯我們的程式碼,輸入:

g++ main.cpp LeakDetector.cpp -std=c++11 -Wno-write-strings

在這裡插入圖片描述

我們通過過載 new 運算子,實現了記憶體洩露檢查器

事實上,這個程式碼同樣可以用於 C++11 中智慧指標迴圈引用產生的記憶體洩露,將 main.cpp 修改為:

#include <memory> // 使用智慧指標

// ...前面不做修改

// 增加兩個測試類
class A;
class B;
class A {
public:
    std::shared_ptr<B> p;
};
class B {
public:
    std::shared_ptr<A> p;
};

int main() {

    //...前面不做修改

    // 智慧指標迴圈引用導致的記憶體洩露
    auto smartA = std::make_shared<A>();
    auto smartB = std::make_shared<B>();
    smartA->p = smartB;
    smartB->p = smartA;

    return 0;

}

結果如下

在這裡插入圖片描述

不過其缺陷在於無法獲得洩露的定位,這是由於我們只把 FILELINE 巨集嵌入到了 new 操作符中,而 make_shared 並沒有進行指定。

相關推薦

C++記憶體洩漏檢查

專案介紹 記憶體洩漏一直是 C++ 中比較令人頭大的問題, 即便是很有經驗的 C++程式設計師有時候也難免因為疏忽而寫出導致記憶體洩漏的程式碼。除了基本的申請過的記憶體未釋放外,還存在諸如異常分支導致的記憶體洩漏等等。本專案將使用 C++ 實現一個記憶體洩漏檢查

一個小專案 --- C++實現記憶體洩漏檢查

先貼出程式碼: .h: // 注意, 我們的標頭檔案是要被包含進被測試的.cpp 的, 所以標頭檔案中不要出現"多餘的"程式碼及庫檔案, 以免影響被測檔案 #ifndef LEAK_DETECTOR_H_ #define LEAK_DETECTOR_H_ // 有個小技

c記憶體洩漏檢查工具---mtrace

    專案中出現記憶體洩漏是讓人很頭疼的事情,使用了vargrind效果不明顯,可能因為試用了libuv裡面有太多非同步處理,導致使用vargrind會出現段錯誤。後來發現mtrace,使用還是挺簡單的。     mtrace是gn

C/C++應用程式記憶體洩漏檢查統計方案

  一、前緒   C/C++程式給某些程式設計師的幾大印象之一就是記憶體自己管理容易洩漏容易崩,筆者曾經在一個產品中使用C語言開發維護部分模組,只要產品有記憶體洩漏和崩潰的問題,就被甩鍋“我的程式是C#開發的記憶體都是託管的,C++那邊也沒有記憶體(庇護其好友),肯定是C這邊的問題”

關於C++記憶體洩漏的一個經驗教訓

       近期寫了一段程式碼,發現有記憶體洩漏,經多次查詢都找不到源點,搞到焦頭爛額,最後經同事細心審查,競是粗心導致的隱藏性錯誤,為了在以後避免犯同樣的錯誤,有必有記錄下來。        在C++中記憶體管理

c++記憶體洩漏檢測工具(上)

原文連結: http://blog.csdn.net/beanjoy/article/details/7578372   1/  VC自帶的CRT:_CrtCheckMemory   偵錯程式和 CRT 除錯堆函式 用法 /********

C 記憶體洩漏檢測工具

所有使用動態記憶體分配(dynamic memory allocation)的程式都有機會遇上記憶體洩露(memory leakage)問題,在Linux裡有三種常用工具來檢測記憶體洩露的情況,包括: mtrace dmalloc memwatch 1. mtrace

(Android Studio 3.0)Android Profiler記憶體洩漏檢查

前提概要 記憶體洩漏是常見又重要的問題,針對這個問題谷歌在Android Studio 3.0中推出了Android Profiler。筆者此篇文章主要記錄一下Android Profiler在記憶體洩漏方面的使用。 Android Profiler Android

C++ 記憶體洩漏檢測1:微軟自帶的記憶體洩漏檢測方法

在程式總的包含標頭檔案中新增以下程式碼, #ifdef _DEBUG #define DEBUG_CLIENTBLOCK new( _CLIENT_BLOCK, __FILE__, __LINE__) #else #define DEBUG_CLIENTBLOCK

C++ 記憶體洩漏檢測方法

程式中通常包含著靜態儲存區和棧記憶體。靜態儲存區也就是靜態記憶體,是用來儲存區域性static物件、類static資料成員以及定義在任何函式之外的變數(全域性變數)。棧記憶體用來儲存定義在函式內的非static物件。分配在靜態或棧記憶體中的物件由編譯器自動建立

CC++記憶體問題檢查利器——Purify

C/C++記憶體問題檢查利器——Purify 一、           引言   我們都知道軟體的測試(在以產品為主的軟體公司中叫做QA—Quality Assessm

如何檢測及預防C++記憶體洩漏

“該死系統存在記憶體洩漏問題”,專案中由於各方面因素,總是有人抱怨存在記憶體洩漏,系統長時間執行之後,可用記憶體越來越少,甚至導致了某些服務失敗。記憶體洩漏是最難發現的常見錯誤之一,因為除非用完記憶體或呼叫malloc失敗,否則都不會導致任何問題。實際上,使用C/C++這類

C++記憶體洩露檢查工具

Linux下編寫C或者C++程式,有很多工具,但是主要編譯器仍然是gcc和g++。最近用到STL中的List程式設計,為了檢測寫的程式碼是否會發現記憶體洩漏,瞭解了一下相關的知識。 所有使用動態記憶體分配(dynamic memory allocation)的程式都有機

vld記憶體洩漏檢查工具不能顯示記憶體洩漏檔名與行號

       最近用vld工具在VS2015下除錯記憶體洩漏,發現輸出視窗有提示記憶體洩漏,但是並沒有顯示檔名和行號, 網上的解決方法提示檢查dbghelp.dll是否載入正確,以及中文路徑等,對我都不適用。幾經周折發現是 vs裡連結選項的設定問題,debug下聯結器-&g

記一次由於智慧指標shared_ptr迴圈引用而產生的C++記憶體洩漏

自從 C++ 11 以來,boost 的智慧指標就被加入了 C++ 新標準之中。其中,廣為人知的 shared_ptr 被用的最多,以引用計數的方式來管理指標指向資源的生命週期。看起來有了智慧指標後,C++ 程式再也不用擔心記憶體洩漏了,就可以像 Java 一樣

Android 記憶體洩漏檢查工具LeakCanary原始碼淺析

使用 監控 Activity 洩露 我們經常把 Activity 當作為 Context 物件使用,在不同場合由各種物件引用 Activity。所以,Activity 洩漏是一個重要的需要檢查的記憶體洩漏之一。 public class Exa

c++記憶體洩漏記憶體碎片的問題

1.記憶體洩漏的定義    一般我們常說的記憶體洩漏是指堆記憶體的洩漏。堆記憶體是指程式從堆中分配的,大小任意的(記憶體塊的大小可以在程式執行期決定),使用完後必須顯示釋放的記憶體。應用程式一般使用malloc,realloc,new等函式從堆中分配到一塊記憶體,使用

c/c++記憶體洩漏檢測工具

1、  可以得到記憶體洩漏點的呼叫堆疊,如果可以的話,還可以得到其所在檔案及行號; 2、  可以得到洩露記憶體的完整資料; 3、  可以設定記憶體洩露報告的級別; 4、  它是一個已經打包的lib,使用時無須編譯它的原始碼。而對於使用者自己的程式碼,也只需要做很小的改動; 5、

[Objective-C]記憶體洩漏是新手必然要經歷的痛,NSMutableArray的正確使用

Objective-C程式開發中的記憶體洩漏問題是新手非常頭痛的事情,可能是用C#這類自動垃圾釋放的語言太習慣了,用xcode中的profile工具查了一下我寫的小程式,記憶體洩漏了一大堆,經過一陣子排查,在NSMutableArray中新增物件後不正確維護物件的引用計數是一個主要原因。 在NSMutable

Linux C/C++ 記憶體洩漏檢測工具Valgrind

下面是一段有問題的C程式程式碼test.c #i nclude <stdlib.h> void f(void) { int* x = malloc(10 * sizeof(int)); x[10] = 0; //問題1: 陣列下標越界 } //問