內存泄露
什麼是內存泄露
程序中申請的資源在程序進程退出之後系統都會回收,所以只要程序退出,程序申請的內存都會被釋放。但是,如果程序在運行的過程中不停的申請內存(或資源),而且不再使用時不進行釋放,會導致系統資源耗盡。
內存泄露實例代碼
void leakfunc()
{
char*szBuf = new char[256];
memset(szBuf, 0, sizeof(char) * 256);
strcpy(szBuf, "Teststring\n");
printf(szBuf);
}
例如上述函數分配了一塊256byte的內存,但是沒有釋放,在調用到該函數時會導致內存泄露。
如何檢測內存泄露
1. 使用工具檢測
檢測內存泄露的工具不少,例如DevPartner(BounderChecker) , PurifyPlus等,使用工具檢測是最簡單也是最有效的方法。
PurifyPlus是異常強大的程序白盒測試工具,基本上所有的內存問題都可以測試,內存泄露,訪問未初始化的內存,野指針,緩衝區溢出等。
2. Windows 提供的內存泄露檢測接口
Windows提供了內存泄露的檢測接口_CrtDumpMemoryLeaks()(頭文件爲crtdbg.h)。
例如我調用上述有內存泄露的接口,代碼如下:
int _tmain(intargc, _TCHAR*argv[])
{
leakfunc();
_CrtDumpMemoryLeaks();
return0;
}
運行結束之後Output窗口會輸出如下Log:
蛋疼,是不是發現只檢測到泄露了多少內存,但是沒有統計內存泄露的位置。那下面我們就讓VS把內存泄露的位置也輸出出來。
重新定義new 和 malloc, 定義如下
#define new new(_CLIENT_BLOCK, __FILE__,__LINE__)
#define malloc _malloc_dbg(_CLIENT_BLOCK, __FILE__,__LINE__)
再運行,輸出如下:
是不是發現已經正確的顯示了行號以及文件。
使用Windows提供的接口也比較方便,但是有很多不足,例如如果是已經封裝好的庫裏面內存泄露則沒有辦法統計內存泄露的位置,除非你也將第三方的庫用自己重新定義的malloc和new重新編譯一次。
3. 自己統計內存泄露
如果有人說我是在嵌入式平臺上運行的,不像Windows一樣有工具,也沒有提供內存檢查的接口,如何檢測內存泄露呢。其實是有辦法的,但是很複雜。我做一個簡單的實現。
---------------------------------------memoryleak.h------------------------------------------
#ifndef __MEMORYLEAK_H__
#define __MEMORYLEAK_H__
void * operator new[](size_t size, const char* file, int line);
void * operator new(
size_t size,
const char * file,
int line
);
void operator delete[] (void* addr);
void operator delete(void* addr);
void* mymalloc(size_t size,
const char* file,
int line);
void myfree(void* addr);
void dumpMemoryLeak();
void initDumpMemoryLeak();
void deinitDumpMemoryLeak();
#endif
--------------------------------memoryleak.cpp------------------------------
#include "stdafx.h"
#include "memleak.h"
#include <malloc.h>
#include <string.h>
#include <assert.h>
typedef struct LEAK_NODE_tag
{
char file[260];
int line;
int size;
void* address;
LEAK_NODE_tag* next;
LEAK_NODE_tag* prev;
}LEAK_NODE_T;
static LEAK_NODE_T m_head = {0};
void * operator new[](size_t size, const char* file, int line)
{
return mymalloc(size, file, line);
}
void * operator new(
size_t size,
const char * file,
int line
)
{
return mymalloc(size, file, line);
}
void operator delete[] (void* addr)
{
return myfree(addr);
}
void operator delete(void* addr)
{
return myfree(addr);
}
void* mymalloc(size_t size,
const char* file,
int line)
{
if (size == 0)
{
return NULL;
}
void* memory = NULL;
LEAK_NODE_T* node = NULL;
do
{
void* memory = malloc(size);
if (!memory)
{
break;
}
node = (LEAK_NODE_T*) malloc(sizeof(LEAK_NODE_T));
if (!node)
{
break;
}
memset(node, 0, sizeof(LEAK_NODE_T));
if (file)
{
strcpy(node->file, file);
}
node->line = line;
node->size = size;
node->address = memory;
m_head.prev->next = node;
node->next = &m_head;
node->prev = m_head.prev;
m_head.prev = node;
return memory;
} while (0);
if (node)
{
free(node);
}
if (memory)
{
free(memory);
}
return NULL;
}
void myfree(void* addr)
{
if (!addr)
{
return;
}
LEAK_NODE_T* node = m_head.prev;
while (node != &m_head)
{
if (node->address == addr)
{
node->next->prev = node->prev;
node->prev->next = node->next;
free(node);
free(addr);
return;
}
node = node->prev;
}
assert(0); // this addr is invalid or not malloc via dump memory
free(addr);
}
void dumpMemoryLeak()
{
LEAK_NODE_T* node = m_head.next;
if (node != &m_head)
{
printf("Detected memory leaks! \n");
}
while (node != &m_head)
{
printf("%s(%d) : block at %p, %d bytes long\n", node->file, node->line, node->address, node->size);
node = node->next;
}
}
void initDumpMemoryLeak()
{
m_head.prev = &m_head;
m_head.next = &m_head;
}
void deinitDumpMemoryLeak()
{
LEAK_NODE_T* node = m_head.next;
LEAK_NODE_T* next = NULL;
while (node != &m_head)
{
next = node->next;
free(node);
node = next;
}
}
-------------------memorytest.cpp--------------------
void leakfunc();
#define new new(__FILE__, __LINE__)
#define malloc(size) mymalloc(size, __FILE__, __LINE__)
#define free(addr) myfree(addr)
void leakfunc()
{
char* szBuf = new char[256];
memset(szBuf, 0, sizeof(char) * 256);
strcpy(szBuf, "Test string\n");
printf(szBuf);
double* pdouble = (double*)malloc(sizeof(double));
int *pint = (int*)malloc(sizeof(int));
free(pdouble);
}
int _tmain(int argc, _TCHAR* argv[])
{
initDumpMemoryLeak();
leakfunc();
dumpMemoryLeak();
deinitDumpMemoryLeak();
return 0;
}
上述例子輸入如下結果:
從上圖可以看出該實現正確的統計了程序的內存泄露。
該例子只是一個非常簡單的實現,在一些更低級的平臺上,基於防止內存碎片等考慮,程序可能需要使用自己的內存池,這時可以在自己分配內存的時候添加Overhead來統計分配的信息。
常用的防止內存泄露的方案
1. 合理的編碼規範防止內存泄露
如:
a) do while{0} 以及goto語句的合理使用
例如:
Void func()
{
A* a = NULL;
B* b = NULL;
Do
{
a = new A();
if (!a)
{
break;
}
b = new B();
if (!b)
{
break;
}
…
}while(0);
if (a)
{
delete a;
}
if (b)
{
delete b;
}
}
b) 在設計類時在init函數分配資源,在析構函數釋放資源等
2. 使用引用計數
對於指針需要在很多模塊傳遞的情況,使用引用計數可以很方便的防止內存泄露。使用的原則是當調用者需要管理這個對象時調用該對象的retain, 當其不需要管理時調用release。當然,如果用戶錯誤的調用retain和release,就會引發嚴重的錯誤(內存泄露或者程序崩潰)。
3. 智能指針
4. void leakfunc()
5. {
6. char* szBuf = new char[256];
7.
8. memset(szBuf, 0, sizeof(char) * 256);
9.
10. strcpy(szBuf, "Teststring\n");
11.
12. printf(szBuf);
13.
14. double* pdouble = (double*)malloc(sizeof(double));
15.
16. auto_ptr<int> pint (new int(5));// this will not cause memory leak
17.
18. free(pdouble);
19. }
但是,智能指針不能用於數組。
非法內存訪問
在C/C++程序中,非法內存使用是永恆的話題。內存非法訪問一般包括如下形式:
1. 使用NULL指針。
例如 A* a = NULL; a->func();
2. 使用野指針。
例如 A* a = new A; delete a; a->func();
3. Double free。
例如A* a = new A; delete a;delete a;
4. 緩衝區溢出
例如 char szBuf[4] = {0}; strcpy(szBuf, “Helloworld”);
非法內存訪問的後果比較嚴重,而且調試起來非常麻煩,所以儘量在寫代碼的時候多小心,否則調試的時候要崩潰。
VS斷點支持
調試內存問題使用需要使用數據斷點,我就簡單的講一下VS支持的常見斷點類型。
- 普通斷點
這個我想大家都會用,就是在某一行按F9, 運行到該行時程序會進入調試模式。
- 條件斷點
可以對普通斷點設置條件,設置條件斷點的方式爲,在設置斷點的那一行右擊,選擇“斷點”->“條件”,然後就可以設置斷點起效的條件。
- 跟蹤點(命中條件)
對於在程序運行過程中需要輸出Log,而又不想添加輸出Log代碼的話,可以使用跟蹤點,添加跟蹤點得方法爲在已設置普通斷點的那一行右擊,選擇“斷點”->“命中條件”;或者在未設置斷點的某行右擊,選擇“斷點”->“插入跟蹤點”。
- 數據斷點(內存斷點)
內存斷點是解決非法內存訪問最有效的武器。
插入數據斷點的方法爲點擊“調試”->“新建斷點”->“新建數據斷點”,然後指定需要監視的內存的位置以及大小(只支持1byte, 2 byte, 4byte)。設置斷點後,在這塊內存的值修改後,系統會進入調試狀態。
非法內存訪問調試
1. 空指針
空指針是最好調試的,在系統訪問空指針(或者系統內存區域時),系統一般會進入調試狀態,可以直接看到當前的堆棧,所以一般很好定位。
但是在Windows程序下需要注意,Windows 通過一個類的空指針去訪問一個函數可能和你預料的有一些不一樣。例如定義如下類:
class A
{
public:
A()
{
m_value= 0;
}
voidsetValue(intvalue)
{
m_value= value;
}
voidPrintValue()
{
printf("Value is %d\n", m_value);
}
voidPrintNull()
{
printf("Just print log\n");
}
private:
intm_value;
};
然後使用如下調用方式:
A*a = NULL;
a->PrintNull(); (1) 程序運行正常
a->setValue(5); (2) 程序死機
a->PrintValue();(3) 程序死機
上面三個都是從對象的空指針調用對象的成員函數,但是(1)不會死機,而(2)(3)會死機,這是爲什麼呢?這就要涉及到編譯器怎麼實現類的成員函數,類的成員函數和普通的C函數是一樣的,只是編譯器在編譯的時候給它改了一個名字,同時加了一個參數(第一個參數)表示對象的this指針,例如PrintNull就會變位類似XXXX_PrintNULL_XX(A* a)的形式。而PrintNull函數不引用對象裏面的任何成員變量,所以不會導致非法內存訪問,因此就沒有事情。而(2)和(3)都需要訪問對象裏面的成員變量,這樣就訪問了非法的內存,所以會死機。而且大家會發現程序會死在setValue和PrintValue裏面訪問成員變量的位置。所以如果以後遇到類似的問題,基本都是因爲調用成員函數的對象指針爲空。
2. 野指針訪問
野指針問題是C/C++最難調試的問題之一,我覺得只能看個人的經驗以及人品了。我們來分析一下野指針會導致哪些災難性的後果。
(1) 程序訪問到野指針的地方宕機。這個已經是最好的結果了,至少你能保留死機現場,知道哪死機的,能調試。
(2) 通過野指針把別的正常數據改了,導致別人運行不正常。
(3) 通過野指針把別的指針改了,死在別的模塊。
分享一下野指針的調試辦法,最常用的辦法就是通過內存斷點了。如果你知道每次都是某個對象的某個成員被改,你就可以設置內存斷點來監視這段內存,當程序通過野指針來修改這段內存的值時,就會被逮個正着。如果每次的位置都是隨機的,這個我不會,只能通過分析代碼或者是Revert代碼看是哪一個Changlist出問題的。
應該在編程中避免野指針的出現,需要養成一些好的習慣,簡單來講就是:
(1) retain和release合理的使用。(需要用時retain,不需要使用了release)。
(2) delete 和 release之後將指針置空。
(3) 使用智能指針
3. double free
double free的問題在Windows上會稍微好查一些,因爲Windows在調試模式下會對內存做檢查,如果double free 會Assert。
上述的稍微好檢查一點只是相對的,例如在使用cocos2d-x的時候,經常會遇到由於引用計數沒有管理好而導致的double free。
這調用堆棧中你不知道到被double free的是哪個對象,而且也很難跟蹤這個對象在哪被釋放了。
很難被跟蹤有如下的原因:
(1) 不知道對象的類型,因爲已經被free過,所以vtbl相關信息都已經被清除了。
(2) 沒辦法在析構函數設斷點跟蹤,因爲這種類型的對象很多,根本沒有辦法跟蹤。
(3) 內存斷點也不是很好設置,因爲在大部分情況下,每次內存位置都不一樣。
但是這種辦法可以採用如下方式:
方法一,可以檢查析構時的代碼,肯定有某個對象retain和release未匹配。
方法二,可以在當前場景的析構函數開始處設置斷點,然後再設置內存斷點。雖然對象在內存中的位置可能會變化,但是在一個靜態的界面中,在CCArray中的索引變化可能性不大,所以可以在釋放之前獲取該索引對應的對象的地址,再設置內存斷點。
在開發工作中預防double free和防止野指針基本類似,但是要多注意一點:如果你對外發布的庫提供了創建對象/數據的接口,也應該提供刪除對象/數據的接口。因爲:
(1) 用戶不知道你是用什麼庫函數進行對象分配的(new, new[], malloc /Debug 版 / Release版)。
(2) 如果分配和釋放的接口匹配會導致問題。
4. 緩衝區溢出
緩衝區溢出也是C/C++程序中永恆的話題,據說80%的系統漏洞都是由緩衝區溢出引起的。
在棧和堆裏面的緩衝區溢出都會導致嚴重的後果。目前的調試方法還是使用內存斷點。
緩衝區溢出應該在編程時注意避免:
1. 不要使用strcpy, 而使用strncpy。