参考blog
http://www.cnblogs.com/findumars/p/5180490.html
1. C++内存管理
1.1 内存管理
1.1.1分配方式简介
C++中,内存分为5个区 堆, 栈, 自由存储区, 全局/静态存储区 和 常量存储区。
栈(堆栈段)在执行函数时,函数内部的局部变量在栈上创建,函数执行结束后这些存储单元被自动释放(cpu内部有ESS ESP等寄存器和各种push, pop等支持栈操作的指令集)
自由存储区,由new分配的内存块用以创建对象 他的释放编译器不会去管,应该由应用程序控制。一般一个new就要对应一个delete。在程序结束后,操作系统也会自动回收。
堆,有malloc/free来管理的内存区。和自由存储区在概念上非常相似。(在windows中调用HeapAlloc)
(补充,因为C++标准的定义被没有强制要求在堆上来创建对象。比如c++有placement new重载操作符。是可以在固定特定的内存空间中创建的。依赖于具体的编译器实现。
因为堆和自由存储区他们只是逻辑概念定义上的不同,具体的底层实现是可能相同也可能不同。为了加以区分new 要用delete来释放, malloc用free来释放)
全局/静态存储区(数据段),也就是全区变量和静态变量分配到同一块内存中。
常量存储区 存储常量的区域,并且是不可以修改的。
1.1.2 明确区分堆与栈
void f() { int *p = new int[5]; } 编译后反编译查看 void f() { 00AB3BE0 push ebp ; push some register to stack 00AB3BE1 mov ebp,esp 00AB3BE3 sub esp,0D8h ; stack space d8h 00AB3BE9 push ebx 00AB3BEA push esi 00AB3BEB push edi 00AB3BEC lea edi,[ebp-0D8h] 00AB3BF2 mov ecx,36h 00AB3BF7 mov eax,0CCCCCCCCh 00AB3BFC rep stos dword ptr es:[edi] ;copy the value from eax to es:[edi] repeat times is store in ecx (36h) int *p = new int[5]; 00AB3BFE push 14h 00AB3C00 call operator new (0AB1203h) ; call the new operator 00AB3C05 add esp,4 ;move the stack point for a double word unit 00AB3C08 mov dword ptr [ebp-0D4h],eax 00AB3C0E mov eax,dword ptr [ebp-0D4h] 00AB3C14 mov dword ptr [p],eax ;store the allocate memory address to pointer P } 00AB3C17 pop edi ;recover the register and destroy the stack 00AB3C18 pop esi 00AB3C19 pop ebx 00AB3C1A add esp,0D8h 00AB3C20 cmp ebp,esp 00AB3C22 call __RTC_CheckEsp (0AB1140h) 00AB3C27 mov esp,ebp 00AB3C29 pop ebp 00AB3C2A ret1 备份入口地址的esp值。和相关寄存器
2 显示的使用d8h的长度作为子程序的栈保留空间。
3 new 返回以后将返回值保持在临时栈空间内
4 ret以后会销毁临时栈空间(恢复esp的初始值)
5 new操作符申请的堆空间(自由存储区)并未释放
这里应该调用delete[] p去释放而不是delete。
1.1.3 堆和栈究竟有哪些区别
1、管理方式不同; 堆栈是由编译器自动管理,无需手动控制(本质上来说是编译器和C++的运行库共同管理。)
而堆的需要由程序自己手动管理,否则会产生memory leak 2、空间大小不同; 一般在32位系统下,堆的理论可以达到4GB空间(但是实际由于操作系统的限制可能达不到)但是他的空间几乎没有限制。
栈的话一般都有一定的空间大小根据编译器的不同可能不同,比如cl编译器默认是1MB。可以自己修改编译选项。 3、能否产生碎片不同; 频繁的new delete会产生大量的碎片,是程序效率降低。
栈本质是一种先进后出队列,也就是线性存储单元。并不会造成碎片化的问题。 4、生长方向不同; 对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;
对于栈来讲,它的生长方向是向下的,顺着内存地址小的方向增长。 5、分配方式不同;
堆是动态分配的。
栈有静态分配和动态分配两种。编译时由编译器完成的。
6、分配效率不同;
堆是操作系统和运行库提供的数据结构(通常是一种链表,并且支持合并分割 参考K&R C)并且要进行搜索分割合并等操作,效率比栈低。
栈是CPU提供的机器级别的数据结构(寄存器ESS ESP EBP,指令pushxxx popxxx call ret int等都支持栈操作)效率高
1.1.4 常见的内存错误以及对策
内存错误经常发生在程序运行时。编译器不能自动发现这些错误,而且错误大多没有明显的症状,时隐时现,增加了改错的难度。
* 内存分配未成功,缺使用了它。
编程新手常犯的错误,因为他们没有意识到内存分配会不成功。常用解决方法是,在使用内存之前检查指针是否为NULL,如果指针P是函数的参数,在函数入口处用Assert(p!=NULL) 来进行检查。
如果用malloc 或new来申请内存也需要用 if(p) ... 来预防错误。
* 内存分配虽然成功,但是尚未初始化
这种错误有两个概念,一个是没有初始化的概念,二是误以为内存缺省值全为零(尤其是栈局部变量结构体初始化)。所以应该给变量对象做一些初始化工作。赋0值也不要省略。
*内存分配成功并且已经初始化,但是操作越过了内存的边界
例如在使用数组时候经常会发生下标多1或者少1的操作。特别是在for语句中,循环次数很容易搞错,导致数组操作越界。
*忘记释放了内存,造成内存泄漏
含有这种操作的函数没被调用一次就丢失一块内存,刚开始系统内存充足,你看不到错误,终有一次程序突然死掉,系统提示内存耗尽。所以要使用delete/new
free/malloc来释放内存
*释放了内存却继续使用他
有3种情况
(1)程序的对象关系调用过于复杂,实在难以搞清楚某个对象是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理混乱的局面。
(2)函数的return语句写错了,将执行栈内存的指针和引用返回给外部。(该内存区在函数体结束时自动销毁)
(3)使用free 或delete后没有设置指针为NULL,导致野指针。
【规则1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。 【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。 【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。 【规则4】动态内存的申请与释放必须配对,防止内存泄漏。 【规则5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
1.1.5 指针参数是如何传递的?
void GetMemory(char *p, int num) { p = (char *)malloc(sizeof(char) * num); } void Test(void) { char *str = NULL; GetMemory(str, 100); // str 仍然为 NULL strcpy(str, "hello"); // 运行错误 } 因为函数体内部会创建一个p指针的副本。而该副本会指向申请的内存。而原来的p指针所指向的地址丝毫没有得到任何的修改。
汇编实现
p = (char *)malloc(sizeof(char) * num); 00FE13DE mov esi,esp 00FE13E0 mov eax,dword ptr [num] 00FE13E3 push eax 00FE13E4 call dword ptr ds:[0FE9114h] 00FE13EA add esp,4 00FE13ED cmp esi,esp 00FE13EF call __RTC_CheckEsp (0FE114Ah) 00FE13F4 mov dword ptr [p],eax
需要申明一个指向指针的指针。
void GetMemory2(char **p, int num) { *p = (char *)malloc(sizeof(char) * num); } void Test2(void) { char *str = NULL; GetMemory2(&str, 100); // 注意参数是 &str,而不是str strcpy(str, "hello"); cout<< str << endl; free(str); } 这样在函数体内会创建一个二级指针的副本,并且该二级指针所指向的是原指针的地址。
汇编实现
*p = (char *)malloc(sizeof(char) * num); 00BE3BFE mov esi,esp 00BE3C00 mov eax,dword ptr [num] 00BE3C03 push eax 00BE3C04 call dword ptr ds:[0BE912Ch] 00BE3C0A add esp,4 00BE3C0D cmp esi,esp 00BE3C0F call __RTC_CheckEsp (0BE1140h) 00BE3C14 mov ecx,dword ptr [p] 00BE3C17 mov dword ptr [ecx],eax }
由于指针指针这个概念不太容易理解,可以使用函数返回值来传递动态内存。
char *GetMemory3(int num) { char *p = (char *)malloc(sizeof(char) * num); return p; } void Test3(void) { char *str = NULL; str = GetMemory3(100); strcpy(str, "hello"); cout<< str << endl; free(str); } 1.1.6 杜绝野指针(1)指针变量没有初始化为NULL 会指向栈上的垃圾数据,被误以为是内存地址。对其进行操作导致访问非法内存的错误。
(2)指针P被free或者delete以后没有被设置为NULL,让人误以为是一个合法的操作。
几个有用的宏
#define FREEP(p) do { if (p) { g_free((void *)p); (p)=NULL; } } while (0) #define DELETEP(p) do { if (p) { delete(p); (p)=NULL; } } while (0) #define DELETEPV(pa) do { if (pa) { delete [] (pa); (pa)=NULL; } } while (0)(3)指针超越了变量的作用域范围。
class A { public: void Func(void){ cout << “Func of class A” << endl; } }; void Test(void) { A *p; { A a; p = &a; // 注意 a 的生命期 } p->Func(); // p是“野指针” }1.1.7有了malloc/free 为什么还需要new 和delete呢?
因为在C++语言中,多了对象的概念。对象除了需要分配内存空间还需要进行一些初始化(构造函数)在释放以后需要调用(析构函数)
而malloc 和free是系统库函数,本身只负责分配和释放内存。不会自动调用构造和析构函数。
当然可以在malloc以后显示调用构造函数完成对象的初始化。
1.1.8 内存耗尽怎么办?
如果申请内存没有找到足够大的内存块,malloc和new将返回NULL,宣告内存申请失败。有三种方案解决“内存耗尽”问题。
1)判断指针是否为NULL,如果是则马上return终止本函数的运行,并且可以设定相应的错误返回代码。
void Func(void) { A *a = new A; if(a == NULL) { return; } … } 2)通常可能是系统有大量内存泄漏导致系统无法再分配内存了,如果继续让程序执行可能会造成用户数据的错误和丢失。因此应该考虑立即结束应用程序并输出响应的日志。调用exit(1) (或者ExitProcess 在windows系统上)
3)可以给new和malloc设置异常处理函数。 例如在VC++中可以设置_set_new_hander为new设置自己的异常处理。
另外有个很重要的现象,在32位的系统上,几乎如论怎样new 和malloc都不会导致内存耗尽。程序会无休止的运行下去,因为32位操作系统支持“虚拟内存”。内存用完了硬盘空间顶着。
一个无限吃内存的例子。
Subprocess.exe
int _tmain(int argc, TCHAR* argv[], TCHAR * env[]) { float *p = NULL; while (TRUE) { p = new float[1000000]; std::cout << "eat memory" << std::endl; if (p == NULL) exit(1); } return 0; }
1.2 C++中健壮的指针和资源管理
除非是对系统api和第三方库的调用和释放要遵循相应的内存申请和内存释放方法。(比如Windows系统的GlobalAlloc, CreateXXX系列函数创建系统内核对象, CreateXXX创建GDI对象。)
在C++语义范围内的尽量用new 和delete来申请和释放内存(特例:在嵌入式系统中。由于系统性能的受限通常不使用new和delete或者会对其进行重载)
1.2.1 第一条规则(RAII)
RAII[1] (Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。
比如以下例子
class CritSect { friend class Lock; public: CritSect () { InitializeCriticalSection (&_critSection); } ~CritSect () { DeleteCriticalSection (&_critSection); } private: void Acquire () { EnterCriticalSection (&_critSection); } void Release () { LeaveCriticalSection (&_critSection); } private: CRITICAL_SECTION _critSection; }; 在创建CritSect会自动初始化临界区对象并对其锁定。在CritSect对出其作用域被销毁时,其析构函数会对临界区对象解锁。因为C++没有java上的Finally关键字,很多时候函数有多分支退出语句时,要在多出写释放(解锁)语句。在逻辑复杂时,特别不方便很容易遗漏。
而且弱由于函数内部的异常导致函数退出时也能正常销毁(释放,解锁)对象。
1.2.2 Smart Pointers
c++标准库有一个模板叫 shared_ptr(还有一个auto_ptr但是最新的c++标准已经不再推荐使用此模板) 可以避免手写Delete代码。
他的基本实现原理是讲指针包在shared_ptr模板内,并在内部实现一个引用计数器。
任何函数的退出会使其引用计数器-1.
任何使用此对象赋值会导致其引用计数器+1
在该对象的析构函数内判断,若引用计数器为0.则会释放其内部保存的指针。也就是自动调用delete。 如果在编写C++代码的时候为了防止内存错误,如果对性能要求不是太高。而且要对指针进行大量的传递等操作。推荐使用智能指针。
(但是在使用递归嵌套和循环嵌套带break等的语法时候要小心,注意理清引用计数器的管理)
std::shared_ptr<Object *> p(new Object());1.2.3 Resource Transfer
这点也可以通过上面的Shared_ptr来实现,还有一个unique_ptr可以支持数组对象。
std::unique_ptr<int[]> p(new int[10]);//ok std::shared_ptr<int[]> p(new int[10]);//error, does not compile
2 内存泄漏
2.1什么是内存泄漏?
一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显示释放的内存。应用程序一般使用malloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
2.1.1内存泄漏的发生方式?
1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。 比如以下代码
void fun() { //.. char *str = new char[255]; //.. // some if statement will return the function return; return; } 2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。比如某些函数内部调用了new/malloc但是由于函数内部逻辑太复杂有多个Return,又或者该函数在堆上创建的对象会作为参数返回,传递给别的函数使用。过度复杂的多重指针传递最后导致你自己也看不懂自己的代码某些资源究竟释放了没有。 3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,但是因为这个类是一个Singleton,所以内存泄漏只会发生一次。
2.1.2如何对付内存泄漏?
写出那些不会导致任何内存泄漏的代码。很明显,当你的代码中到处充满了new 操作、delete操作和指针运算的话,你将会在某个地方搞晕了头,导致内存泄漏,指针引用错误,以及诸如此类的问题。这和你如何小心地对待内存分配工作其实完全没有关系:代码的复杂性最终总是会超过你能够付出的时间和努力。于是随后产生了一些成功的技巧,它们依赖于将内存分配(allocations)与重新分配(deallocation)工作隐藏在易于管理的类型之后。标准容器(standard containers)是一个优秀的例子。它们不是通过你而是自己为元素管理内存,从而避免了产生糟糕的结果。想象一下,没有string和vector的帮助,写出这个:
#include<vector> #include<string> #include<iostream> #include<algorithm> using namespace std; int main() // small program messing around with strings { cout << "enter some whitespace-separated words:"n"; vector<string> v; string s; while (cin>>s) v.push_back(s); sort(v.begin(),v.end()); string cat; typedef vector<string>::const_iterator Iter; for (Iter p = v.begin(); p!=v.end(); ++p) cat += *p+"+"; cout << cat << ’"n’; } 如果你的程序还没有包含将显式内存管理减少到最小限度的库,那么要让你程序完成和正确运行的话,最快的途径也许就是先建立一个这样的库。 模板和标准库实现了容器、资源句柄以及诸如此类的东西,更早的使用甚至在多年以前。异常的使用使之更加完善。
如果你实在不能将内存分配/重新分配的操作隐藏到你需要的对象中时,你可以使用资源句柄(resource handle),以将内存泄漏的可能性降至最低。
比如shared_ptr 等
2.2 内存泄漏的检测
基本原理:
检测内存泄漏的关键是能截获对内存分配和释放内存的函数的调用。例如每成功分配一个内存就把它的指针加入全局list中。每释放一个内存就把他从全局list中删除。
这样在程序结束的时候,list中剩余的指针指向那些没有被释放的内存。
Windows下检测内存泄漏的工具一般有3种,MS C-Runtime Library内建的检测功能;外挂式检测工具Purify, BoundChecker; 利用Windows NT自带的Task Manager等。
MS CRT 功能较弱而且不一定准确。(我发现很多静态变量中的内存释放会被误报 而且如果和boost和STL库一起使用会因为标准库和boost库中存在placement new 的重载用法而导致编译错误。) 但是如果使用纯C的方式还是比较准确的。
另外两款是商业软件,笔者也没有用过。
Task Manager 虽然无法表示问题的代码但是他能检测出隐式内存泄漏的存在。
以下将讨论这三种检测方法:
1 CRT Dump Memory
建立一个头文件
#pragma once #ifndef _UT_DETECT_MEMLEAK_H #define _UT_DETECT_MEMLEAK_H #if defined(_NEW_) //Checked that whether the ::operator new placement has been used. #define DebugCodeCRT(m) #else #ifdef _DEBUG #define new DEBUG_CLIENTBLOCK #define DebugCodeCRT(m) {m;} #endif #ifdef _DEBUG #define DEBUG_CLIENTBLOCK new( _CLIENT_BLOCK, __FILE__, __LINE__) #else #define DEBUG_CLIENTBLOCK #endif #define _CRTDBG_MAP_ALLOC #include <crtdbg.h> #ifdef _DEBUG #define new DEBUG_CLIENTBLOCK #endif #endif #endif _UT_DETECT_MEMLEAK_H 创建一个退出函数在主线程退出以后执行memoryDump void Exit() { int i = _CrtDumpMemoryLeaks(); } 注册OnExit函数 BOOL APIENTRY DllMain(HMODULE hModule, DWORD fdwReason, LPVOID lpReserved ) { switch (fdwReason) { case DLL_PROCESS_ATTACH: //Create the testlog instance DebugCode( UT_TestLog::GetInstance()->printlnlogA("Init the ATSWord control"); atexit(Exit); ); 在程序执行退出以后会输出dump 日志 Detected memory leaks! Dumping objects -> ..\..\src\af\xap\xp\xap_ModuleManager.cpp(69) : {38192} normal block at 0x0824AB40, 24 bytes long. Data: <` > 60 AA 9E 0F 00 00 00 00 00 00 00 00 00 00 00 00 d:\projects\ehrpro\02.sourcecode\atsword\src\af\util\xp\ut_stringbuf.h(345) : {37815} normal block at 0x0829E418, 9 bytes long. Data: <8.2677in > 38 2E 32 36 37 37 69 6E 00 ..\..\src\af\util\xp\ut_mbtowc.cpp(153) : {34416} normal block at 0x082A1538, 4 bytes long. Data: <(P > 28 50 CB 07使用Performance Monitor(Task Manager)
检测进程的 Handle Count, GDI Object, Virtual Bytes , Working Set 如果发现这些资源持续的增加。可以做最小化测试。
给项目搭建每个模块的最小化模块测试。通过扫描每个模块来定位内存泄漏发生的模块,从而进入相关的代码进行调试。