??xml version="1.0" encoding="utf-8" standalone="yes"?>
一、控制存储方式:
static被引入以告知~译器,变量存储在E序的静态存储区而非栈上I间?
1、引出原因:函数内部定义的变量,在程序执行到它的定义处时Q编译器为它在栈上分配空_大家知道Q函数在栈上分配的空间在此函数执行结束时会释放掉Q这样就产生了一个问? 如果惛_函数中此变量的g存至下一ơ调用时Q如何实玎ͼ
最Ҏ惛_的方法是定义一个全局的变量,但定义ؓ一个全局变量有许多缺点,最明显的缺Ҏ破坏了此变量的访问范_使得在此函数中定义的变量Q不仅仅受此函数控制Q?
2?解决ҎQ因此c++ 中引入了staticQ用它来修饰变量Q它能够指示~译器将此变量在E序的静态存储区分配I间保存Q这样即实现了目的,又得此变量的存取范围不变?
二、控制可见性与q接cd :
staticq有一个作用,它会把变量的可见范围限制在编译单元中Q它成Z个内部连接,q时Q它的反义词?#8221;extern”.
static作用分析ȝQstaticL使得变量或对象的存储形式变成静态存储,q接方式变成内部q接Q对于局部变量(已经是内部连接了Q,它仅改变其存储方式;对于全局变量Q已l是静态存储了Q,它仅改变其连接类型?
cM的static成员Q?
一、出现原因及作用Q?
1、需要在一个类的各个对象间交互Q即需要一个数据对象ؓ整个c而非某个对象服务?
2、同时又力求不破坏类的封装?卌求此成员隐藏在类的内部,对外不可见?
cȝstatic成员满了上q的要求Q因为它h如下特征Q有独立的存储区Q属于整个类?
二、注意:
1、对于静态的数据成员Q连接器会保证它拥有一个单一的外部定义。静态数据成员按定义出现的先后顺序依ơ初始化Q注意静态成员嵌套时Q要保证所嵌套的成员已l初始化了。消除时的顺序是初始化的反顺序?
2、类的静态成员函数是属于整个c而非cȝ对象Q所以它没有this指针Q这导致了它仅能访问类的静态数据和静态成员函数?
const 是c++中常用的cd修饰W,但我在工作中发现Q许多h使用它仅仅是惛_然尔Q这?有时也会用对Q但在某些微妙的场合Q可没那么q运了,I其实质原由Q大多因为没有搞清本源。故在本中我将对constq行辨析。溯其本源,I其实质Q希望能对大家理解const有所帮助Q根据思维的承接关p,分ؓ如下几个部分q行阐述?
c++中ؓ什么会引入const
c++的提当初是Z什么样的目的引入(或者说保留Qconst关键字呢Q,q是一个有又有益的话题,对理解const很有帮助?
1Q?大家知道Qc++有一个类型严格的~译pȝQ这使得c++E序的错误在~译阶段卛_发现许多Q从而得出错率大ؓ减少Q因此,也成Zc++与c相比Q有着H出优点的一个方面?
2Q?c中很常见的预处理指o #define variablename variablevalue 可以很方便地q行值替代,q种值替代至在三个斚w优点H出Q?
一是避免了意义模糊的数字出玎ͼ使得E序语义畅清晰Q如下例Q?
#define user_num_max 107 q样避免了直接使用107带来的困惑?
二是可以很方便地q行参数的调整与修改Q如上例Q当人数?07变ؓ201Ӟq改动此处即可,
三是提高了程序的执行效率Q由于用了预编译器q行值替代,q不需要ؓq些帔R分配存储I间Q所以执行的效率较高?
鉴于以上的优点,q种预定义指令的使用在程序中随处可见?
3Q?说到q里Q大家可能会qh上述?炏V?点与const有什么关pd?,好,h着向下看:
预处理语句虽然有以上的许多优点,但它有个比较致命的缺点,卻I预处理语句仅仅只是简单值替代,~Zcd的检机制。这样预处理语句׃能n受c++严格cd查的好处Q从而可能成为引发一pd错误的隐患?
4Q好了,W一阶段l论出来了:
l论Q?const 推出的初始目的,正是Z取代预编译指令,消除它的~点Q同时承它的优炏V?
现在它的形式变成了:
const datatype variablename = variablevalue ;
Z么const能很好地取代预定义语句?
const 到底有什么大通,使它可以振臂一挥取代预定义语句呢?
1Q?首先Q以const 修饰的常量|h不可变性,q是它能取代预定义语句的基础?
2Q?W二Q很明显Q它也同样可以避免意义模p的数字出现Q同样可以很方便地进行参数的调整和修攏V?
3Q?W三Qc++的编译器通常不ؓ普通const帔R分配存储I间Q而是它们保存在W号表中Q这使得它成Z个编译期间的帔RQ没有了存储与读内存的操作,使得它的效率也很高,同时Q这也是它取代预定义语句的重要基。这里,我要提一下,Z么说q一Ҏ也是它能取代预定义语句的基础Q这是因为,~译器不会去d储的内容Q如果编译器为const分配了存储空_它就不能够成Z个编译期间的帔R了?
4Q?最后,const定义也像一个普通的变量定义一P它会q译器对它q行cd的检,消除了预定义语句的隐患?
const 使用情况分类详析
1.const 用于指针的两U情况分析:
int const *a; file://a可变Q?a不可?
int *const a; file://a不可变,*a可变
分析Qconst 是一个左l合的类型修饰符Q它与其左侧的类型修饰符和ؓ一个类型修饰符Q所以,int const 限定 *a,不限定a。int *const 限定a,不限?a?
2.const 限定函数的传递值参敎ͼ
void fun(const int var);
分析Q上q写法限定参数在函数体中不可被改变。由g递的特点可知Qvar在函C中的改变不会影响到函数外部。所以,此限定与函数的用者无养I仅与函数的编写者有兟?
l论Q最好在函数的内部进行限定,对外部调用者屏蔽,以免引v困惑。如可改写如下:
void fun(int var){
const int & varalias = var;
varalias ....
.....
}
3.const 限定函数的值型q回|
const int fun1();
const myclass fun2();
分析:上述写法限定函数的返回g可被更新Q当函数q回内部的类型时Q如fun1Q,已经是一个数|当然不可被赋值更斎ͼ所以,此时const无意义,最好去掉,以免困惑。当函数q回自定义的cdӞ如fun2Q,q个cd仍然包含可以被赋值的变量成员Q所以,此时有意义?
4. 传递与q回地址Q?此种情况最为常见,由地址变量的特点可知,适当使用constQ意义昭然?
5. const 限定cȝ成员函数Q?
class classname {
public:
int fun() const;
.....
}
注意Q采用此Uconst 后置的Ş式是一U规定,亦ؓ了不引vh。在此函数的声明中和定义中均要用const,因ؓconst已经成ؓcd信息的一部分?
获得能力Q可以操作常量对象?
失去能力Q不能修改类的数据成员,不能在函C调用其他不是const的函数?
在本文中Qconst斚w的知识我讲的不多Q因为我不想把它变成一本c++的教U书。我只是惌l地阐述它的实质和用? 我会量说的很详l,因ؓ我希望在一U很L随意的气氛中说出自己的某些想法,毕竟Q编E也是轻松,快乐人生的一部分。有时候,你会惊叹q其中的世界原来是如此的_?
在前面谈了const后,现在再来谈一下inlineq个关键字,之所以把q个问题攑֜q个位置,是因为inlineq个关键字的引入原因和const十分怼Q下面分为如下几个部分进行阐q?
c++中引入inline关键字的原因:
inline 关键字用来定义一个类的内联函敎ͼ引入它的主要原因是用它替代c中表辑ּ形式的宏定义?
表达式Ş式的宏定义一例:
#define expressionname(var1,var2) (var1+var2)*(var1-var2)
Z么要取代q种形式呢,且听我道来:
1Q?首先谈一下在c中用这UŞ式宏定义的原因,c语言是一个效率很高的语言Q这U宏定义在Ş式及使用上像一个函敎ͼ但它使用预处理器实现Q没有了参数压栈Q代码生成等一pd的操?因此Q效率很高,q是它在c中被使用的一个主要原因?
2Q?q种宏定义在形式上类g一个函敎ͼ但在使用它时Q仅仅只是做预处理器W号表中的简单替换,因此它不能进行参数有效性的,也就不能享受c++~译器严格类型检查的好处Q另外它的返回g不能被强制{换ؓ可{换的合适的cdQ这P它的使用存在着一pd的隐患和局限性?
3Q?在c++中引入了cdcȝ讉K控制Q这P如果一个操作或者说一个表辑ּ涉及到类的保护成员或U有成员Q你׃可能使用q种宏定义来实现(因ؓ无法this指针攑֜合适的位置)?
4Q?inline 推出的目的,也正是ؓ了取代这U表辑ּ形式的宏定义Q它消除了它的缺点,同时又很好地l承了它的优炏V?
Z么inline能很好地取代表达式Ş式的预定义呢Q?
对应于上面的1-3点,阐述如下Q?
1Q?inline 定义的类的内联函敎ͼ函数的代码被攑օW号表中Q在使用时直接进行替换,Q像宏一样展开Q,没有了调用的开销Q效率也很高?
2Q?很明显,cȝ内联函数也是一个真正的函数Q编译器在调用一个内联函数时Q会首先查它的参数的cdQ保证调用正。然后进行一pd的相x查,像对待M一个真正的函数一栗这样就消除了它的隐患和局限性?
3Q?inline 可以作ؓ某个cȝ成员函数Q当然就可以在其中用所在类的保护成员及U有成员?
在何时用inline函数Q?
首先Q你可以使用inline函数完全取代表达式Ş式的宏定义?
另外要注意,内联函数一般只会用在函数内定w常简单的时候,q是因ؓQ内联函数的代码会在M调用它的地方展开Q如果函数太复杂Q代码膨胀带来的恶果很可能会大于效率的提高带来的益处?内联函数最重要的用地Ҏ用于cȝ存取函数?
如何使用cȝinline函数Q?
单提一下inline 的用吧Q?
1.在类中定义这U函敎ͼ
class classname{
.....
....
getwidth(){return m_lpicwidth;}; // 如果在类中直接定义,可以不用inline修饰
....
....
}
2.在类中声明,在类外定?
class classname{
.....
....
getwidth(); // 如果在类中直接定义,可以不用inline修饰
....
....
}
inline getwidth(){
return m_lpicwidth;
}
在本文中Q谈了一U特D的函数Q类的inline函数Q它的源起和特点在某U说法上与const很类|可以与const搭配h看?/p>
void MyFunction(int nSize) { char* p= new char[nSize]; if( !GetStringFrom( p, nSize ) ){ MessageBox(“Error”); return; } …//using the string pointed by p; delete p; } |
当函数GetStringFrom()q回零的时候,指针p指向的内存就不会被释放。这是一U常见的发生内存泄漏的情形。程序在入口处分配内存,在出口处释放内存Q但是c函数可以在Q何地斚w出,所以一旦有某个出口处没有释攑ֺ该释攄内存Q就会发生内存泄漏?
q义的说Q内存泄漏不仅仅包含堆内存的泄漏Q还包含pȝ资源的泄?resource leak)Q比如核心态HANDLEQGDI ObjectQSOCKETQInterface{,从根本上说这些由操作pȝ分配的对象也消耗内存,如果q些对象发生泄漏最l也会导致内存的泄漏。而且Q某些对象消耗的是核心态内存,q些对象严重泄漏时会D整个操作pȝ不稳定。所以相比之下,pȝ资源的泄漏比堆内存的泄漏更ؓ严重?
GDI Object的泄漏是一U常见的资源泄漏Q?
void CMyView::OnPaint( CDC* pDC ) { CBitmap bmp; CBitmap* pOldBmp; bmp.LoadBitmap(IDB_MYBMP); pOldBmp = pDC->SelectObject( &bmp ); … if( Something() ){ return; } pDC->SelectObject( pOldBmp ); return; } |
当函数Something()q回非零的时候,E序在退出前没有把pOldBmp选回pDC中,q会DpOldBmp指向的HBITMAP对象发生泄漏。这个程序如果长旉的运行,可能会导致整个系l花屏。这U问题在Win9x下比较容易暴露出来,因ؓWin9x的GDI堆比Win2k或NT的要很多?
内存泄漏的发生方式:
以发生的方式来分c,内存泄漏可以分ؓ4c:
1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行刎ͼ每次被执行的时候都会导致一块内存泄漏。比如例二,如果Something()函数一直返回TrueQ那么pOldBmp指向的HBITMAP对象L发生泄漏?
2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作q程下才会发生。比如例二,如果Something()函数只有在特定环境下才返回TrueQ那么pOldBmp指向的HBITMAP对象q不L发生泄漏。常发性和偶发性是相对的。对于特定的环境Q偶发性的也许变成了常发性的。所以测试环境和试ҎҎ内存泄漏至关重要?
3. 一ơ性内存泄漏。发生内存泄漏的代码只会被执行一ơ,或者由于算法上的缺PDM有一块仅且一块内存发生泄漏。比如,在类的构造函C分配内存Q在析构函数中却没有释放该内存,但是因ؓq个cL一个SingletonQ所以内存泄漏只会发生一ơ。另一个例子:
char* g_lpszFileName = NULL; void SetFileName( const char* lpcszFileName ) { if( g_lpszFileName ){ free( g_lpszFileName ); } g_lpszFileName = strdup( lpcszFileName ); } |
如果E序在结束的时候没有释放g_lpszFileName指向的字W串Q那么,即多次调用SetFileName()QM有一块内存,而且仅有一块内存发生泄漏?
4. 隐式内存泄漏。程序在q行q程中不停的分配内存Q但是直到结束的时候才释放内存。严格的说这里ƈ没有发生内存泄漏Q因为最l程序释放了所有申L内存。但是对于一个服务器E序Q需要运行几天,几周甚至几个月,不及旉攑ֆ存也可能D最l耗尽pȝ的所有内存。所以,我们U这cd存泄漏ؓ隐式内存泄漏。D一个例子:
class Connection { public: Connection( SOCKET s); ~Connection(); … private: SOCKET _socket; … }; class ConnectionManager { public: ConnectionManager(){ } ~ConnectionManager(){ list<Connection>::iterator it; for( it = _connlist.begin(); it != _connlist.end(); ++it ){ delete Q?itQ? } _connlist.clear(); } void OnClientConnected( SOCKET s ){ Connection* p = new Connection(s); _connlist.push_back(p); } void OnClientDisconnected( Connection* pconn ){ _connlist.remove( pconn ); delete pconn; } private: list<Connection*> _connlist; }; |
假设在Client从Server端断开后,Serverq没有呼叫OnClientDisconnected()函数Q那么代表那ơ连接的Connection对象׃会被及时的删除(在ServerE序退出的时候,所有Connection对象会在ConnectionManager的析构函数里被删除)。当不断的有q接建立、断开旉式内存泄漏就发生了?
从用户用程序的角度来看Q内存泄漏本w不会生什么危宻I作ؓ一般的用户Q根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆U,q会最l消耗尽pȝ所有的内存。从q个角度来说Q一ơ性内存泄漏ƈ没有什么危宻I因ؓ它不会堆U,而隐式内存泄漏危x则非常大,因ؓ较之于常发性和偶发性内存泄漏它更难被检到?/p>
内存泄漏:
内存泄漏的关键是要能截获住对分配内存和释放内存的函数的调用。截获住q两个函敎ͼ我们p跟踪每一块内存的生命周期Q比如,每当成功的分配一块内存后Q就把它的指针加入一个全局的list中;每当释放一块内存,再把它的指针从list中删除。这P当程序结束的时候,list中剩余的指针是指向那些没有被释攄内存。这里只是简单的描述了检内存泄漏的基本原理Q详l的法可以参见Steve Maguire?lt;<Writing Solid Code>>?
如果要检堆内存的泄漏,那么需要截获住malloc/realloc/free和new/delete可以了Q其实new/delete最l也是用malloc/free的,所以只要截获前面一l即可)。对于其他的泄漏Q可以采用类似的ҎQ截获住相应的分配和释放函数。比如,要检BSTR的泄漏,需要截获SysAllocString/SysFreeStringQ要HMENU的泄漏,需要截获CreateMenu/ DestroyMenu。(有的资源的分配函数有多个Q释攑և数只有一个,比如QSysAllocStringLen也可以用来分配BSTRQ这时就需要截获多个分配函敎ͼ
在Windowsq_下,内存泄漏的工具常用的一般有三种QMS C-Runtime Library内徏的检功能;外挂式的工P诸如QPurifyQBoundsChecker{;利用Windows NT自带的Performance Monitor。这三种工具各有优缺点,MS C-Runtime Library虽然功能上较之外挂式的工兯弱,但是它是免费的;Performance Monitor虽然无法标示出发生问题的代码Q但是它能检出隐式的内存泄漏的存在Q这是其他两cdh能ؓ力的地方?
以下我们详细讨论q三U检工P
VC下内存泄漏的方?
用MFC开发的应用E序Q在DEBUG版模式下~译后,都会自动加入内存泄漏的检代码。在E序l束后,如果发生了内存泄漏,在DebugH口中会昄出所有发生泄漏的内存块的信息Q以下两行显CZ一块被泄漏的内存块的信息:
E:\TestMemLeak\TestDlg.cpp(70) : {59} normal block at 0x00881710, 200 bytes long.
Data: <abcdefghijklmnop> 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70
W一行显C内存块由TestDlg.cpp文gQ第70行代码分配,地址?x00881710Q大ؓ200字节Q{59}是指调用内存分配函数的Request OrderQ关于它的详l信息可以参见MSDN中_CrtSetBreakAlloc()的帮助。第二行昄该内存块?6个字节的内容Q尖括号内是以ASCII方式昄Q接着的是?6q制方式昄?
一般大安误以些内存泄漏的功能是由MFC提供的,其实不然。MFC只是装和利用了MS C-Runtime Library的Debug Function。非MFCE序也可以利用MS C-Runtime Library的Debug Function加入内存泄漏的检功能。MS C-Runtime Library在实现malloc/freeQstrdup{函数时已经内徏了内存泄漏的功能?
注意观察一下由MFC Application Wizard生成的项目,在每一个cpp文g的头部都有这样一D宏定义Q?
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
有了q样的定义,在编译DEBUG版时Q出现在q个cpp文g中的所有new都被替换成DEBUG_NEW了。那么DEBUG_NEW是什么呢QDEBUG_NEW也是一个宏Q以下摘自afx.hQ?632?
#define DEBUG_NEW new(THIS_FILE, __LINE__)
所以如果有q样一行代码:
char* p = new char[200];
l过宏替换就变成了:
char* p = new( THIS_FILE, __LINE__)char[200];
ҎC++的标准,对于以上的new的用方法,~译器会Lq样定义的operator newQ?
void* operator new(size_t, LPCSTR, int)
我们在afxmem.cpp 63行找C一个这Loperator new 的实?/p>
void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine) { return ::operator new(nSize, _NORMAL_BLOCK, lpszFileName, nLine); } void* __cdecl operator new(size_t nSize, int nType, LPCSTR lpszFileName, int nLine) { … pResult = _malloc_dbg(nSize, nType, lpszFileName, nLine); if (pResult != NULL) return pResult; … } |
W二个operator new函数比较长,Z单期_我只摘录了部分。很昄最后的内存分配q是通过_malloc_dbg函数实现的,q个函数属于MS C-Runtime Library 的Debug Function。这个函C但要求传入内存的大小Q另外还有文件名和行号两个参数。文件名和行号就是用来记录此ơ分配是由哪一D代码造成的。如果这块内存在E序l束之前没有被释放,那么q些信息׃输出到DebugH口里?
q里Z提一下THIS_FILEQ__FILE和__LINE__。__FILE__和__LINE__都是~译器定义的宏。当到__FILE__Ӟ~译器会把__FILE__替换成一个字W串Q这个字W串是当前在编译的文g的\径名。当到__LINE__Ӟ~译器会把__LINE__替换成一个数字,q个数字是当前q行代码的行受在DEBUG_NEW的定义中没有直接使用__FILE__Q而是用了THIS_FILEQ其目的是ؓ了减目标文件的大小。假讑֜某个cpp文g中有100处用了newQ如果直接用__FILE__Q那~译器会产生100个常量字W串Q这100个字W串都是q个cpp文g的\径名Q显然十分冗余。如果用THIS_FILEQ编译器只会产生一个常量字W串Q那100处new的调用用的都是指向帔R字符串的指针?
再次观察一下由MFC Application Wizard生成的项目,我们会发现在cpp文g中只对new做了映射Q如果你在程序中直接使用malloc函数分配内存Q调用malloc的文件名和行h不会被记录下来的。如果这块内存发生了泄漏QMS C-Runtime Library仍然能检到Q但是当输出q块内存块的信息Q不会包含分配它的的文g名和行号?
要在非MFCE序中打开内存泄漏的检功能非常容易,你只要在E序的入口处加入以下几行代码Q?
int tmpFlag = _CrtSetDbgFlag( _CRTDBG_REPORT_FLAG ); tmpFlag |= _CRTDBG_LEAK_CHECK_DF; _CrtSetDbgFlag( tmpFlag ); |
q样Q在E序l束的时候,也就是winmainQmain或dllmain函数q回之后Q如果还有内存块没有释放Q它们的信息会被打印到DebugH口里?
如果你试着创徏了一个非MFC应用E序Q而且在程序的入口处加入了以上代码Qƈ且故意在E序中不释放某些内存块,你会在DebugH口里看C下的信息Q?
{47} normal block at 0x00C91C90, 200 bytes long.
Data: < > 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
内存泄漏的确到了,但是和上面MFCE序的例子相比,~少了文件名和行受对于一个比较大的程序,没有q些信息Q解决问题将变得十分困难?
Z能够知道泄漏的内存块是在哪里分配的,你需要实现类似MFC的映功能,把newQmaolloc{函数映到_malloc_dbg函数上。这里我不再赘述Q你可以参考MFC的源代码?
׃Debug Function实现在MS C-RuntimeLibrary中,所以它只能到堆内存的泄漏Q而且只限于mallocQrealloc或strdup{分配的内存Q而那些系l资源,比如HANDLEQGDI ObjectQ或是不通过C-Runtime Library分配的内存,比如VARIANTQBSTR的泄漏,它是无法到的,q是q种法的一个重大的局限性。另外,Z能记录内存块是在哪里分配的,源代码必ȝ应的配合Q这在调试一些老的E序非常ȝQ毕竟修Ҏ代码不是一件省心的事,q是q种法的另一个局限性?
对于开发一个大型的E序QMS C-Runtime Library提供的检功能是q远不够的。接下来我们q看外挂式的检工兗我用的比较多的是BoundsCheckerQ一则因为它的功能比较全面,更重要的是它的稳定性。这cd具如果不E_Q反而会忙里Mؕ。到底是鼎鼎大名的NuMegaQ我用下来基本上没有什么大问题?/p>
使用BoundsChecker内存泄漏:
BoundsChecker采用一U被UCؓCode Injection的技术,来截获对分配内存和释攑ֆ存的函数的调用。简单地_当你的程序开始运行时QBoundsChecker的DLL被自动蝲入进E的地址I间Q这可以通过system-level的Hook实现Q,然后它会修改q程中对内存分配和释攄函数调用Q让q些调用首先转入它的代码Q然后再执行原来的代码。BoundsChecker在做q些动作的时Q无M改被调试E序的源代码或工E配|文Ӟq得用它非常的简ѝ直接?
q里我们以malloc函数ZQ截获其他的函数Ҏ与此cM?
需要被截获的函数可能在DLL中,也可能在E序的代码里。比如,如果静态连lC-Runtime LibraryQ那么malloc函数的代码会被连l到E序里。ؓ了截获住对这cd数的调用QBoundsChecker会动态修改这些函数的指o?
以下两段汇编代码Q一D|有BoundsChecker介入Q另一D则有BoundsChecker的介入:
126: _CRTIMP void * __cdecl malloc ( 127: size_t nSize 128: ) 129: { 00403C10 push ebp 00403C11 mov ebp,esp 130: return _nh_malloc_dbg(nSize, _newmode, _NORMAL_BLOCK, NULL, 0); 00403C13 push 0 00403C15 push 0 00403C17 push 1 00403C19 mov eax,[__newmode (0042376c)] 00403C1E push eax 00403C1F mov ecx,dword ptr [nSize] 00403C22 push ecx 00403C23 call _nh_malloc_dbg (00403c80) 00403C28 add esp,14h 131: } |
以下q一D代码有BoundsChecker介入Q?/p>
126: _CRTIMP void * __cdecl malloc ( 127: size_t nSize 128: ) 129: { 00403C10 jmp 01F41EC8 00403C15 push 0 00403C17 push 1 00403C19 mov eax,[__newmode (0042376c)] 00403C1E push eax 00403C1F mov ecx,dword ptr [nSize] 00403C22 push ecx 00403C23 call _nh_malloc_dbg (00403c80) 00403C28 add esp,14h 131: } |
当BoundsChecker介入后,函数malloc的前三条汇编指o被替换成一条jmp指oQ原来的三条指o被搬到地址01F41EC8处了。当E序q入malloc后先jmp?1F41EC8Q执行原来的三条指oQ然后就是BoundsChecker的天下了。大致上它会先记录函数的q回地址Q函数的q回地址在stack上,所以很Ҏ修改Q,然后把返回地址指向属于BoundsChecker的代码,接着跛_malloc函数原来的指令,也就是在00403c15的地斏V当malloc函数l束的时候,׃q回地址被修改,它会q回到BoundsChecker的代码中Q此时BoundsChecker会记录由malloc分配的内存的指针Q然后再跌{到到原来的返回地址厅R?
如果内存分配/释放函数在DLL中,BoundsChecker则采用另一U方法来截获对这些函数的调用。BoundsChecker通过修改E序的DLL Import Table让table中的函数地址指向自己的地址Q以辑ֈ截获的目的。关于如何拦截Windows的系l函敎ͼ《程序员》杂?002q?期,《API钩子揭密Q下Q》,对修改导入地址表做了概要的描述。我׃再赘q?
截获住这些分配和释放函数QBoundsCheckerp记录被分配的内存或资源的生命周期。接下来的问题是如何与源代码相关Q也是说当BoundsChecker到内存泄漏Q它如何报告q块内存块是哪段代码分配的。答案是调试信息QDebug InformationQ。当我们~译一个Debug版的E序Ӟ~译器会把源代码和二q制代码之间的对应关p记录下来,攑ֈ一个单独的文g?.pdb)或者直接连l进目标E序中。有了这些信息,调试器才能完成断点设|,单步执行Q查看变量等功能。BoundsChecker支持多种调试信息格式Q它通过直接d调试信息p得到分配某块内存的源代码在哪个文Ӟ哪一行上。用Code Injection和Debug InformationQBoundsChecker不但能记录呼叫分配函数的源代码的位置Q而且q能记录分配时的Call StackQ以及Call Stack上的函数的源代码位置。这在用像MFCq样的类库时非常有用Q以下我用一个例子来说明Q?
void ShowXItemMenu() { … CMenu menu; menu.CreatePopupMenu(); //add menu items. menu.TrackPropupMenu(); … } void ShowYItemMenu( ) { … CMenu menu; menu.CreatePopupMenu(); //add menu items. menu.TrackPropupMenu(); menu.Detach();//this will cause HMENU leak … } BOOL CMenu::CreatePopupMenu() { … hMenu = CreatePopupMenu(); … } |
当调用ShowYItemMenu()Ӟ我们故意造成HMENU的泄漏。但是,对于BoundsChecker来说被泄漏的HMENU是在class CMenu::CreatePopupMenu()中分配的。假讄你的E序有许多地方用了CMenu的CreatePopupMenu()函数Q如果只是告诉你泄漏是由CMenu::CreatePopupMenu()造成的,你依然无法确认问题的根结到底在哪里,在ShowXItemMenu()中还是在ShowYItemMenu()中,或者还有其它的地方也用了CreatePopupMenu()Q有了Call Stack的信息,问题容易了。BoundsChecker会如下报告泄漏的HMENU的信息:
Function CMenu::CreatePopupMenu ShowYItemMenu |
q里省略了其他的函数调用
如此Q我们很Ҏ扑ֈ发生问题的函数是ShowYItemMenu()。当使用MFC之类的类库编E时Q大部分的API调用都被装在类库的class里,有了Call Stack信息Q我们就可以非常Ҏ的追t到真正发生泄漏的代码?
记录Call Stack信息会ɽE序的运行变得非常慢Q因此默认情况下BoundsChecker不会记录Call Stack信息。可以按照以下的步骤打开记录Call Stack信息的选项开养I
1. 打开菜单QBoundsChecker|Setting…
2. 在Error Detection中Q在Error Detection Scheme的List中选择Custom
3. 在Category的Combox中选择Pointer and leak error check
4. 钩上Report Call Stack复选框
5. 点击Ok
ZCode InjectionQBoundsCheckerq提供了API Parameter的校验功能,memory over run{功能。这些功能对于程序的开发都非常有益。由于这些内容不属于本文的主题,所以不在此详述了?
管BoundsChecker的功能如此强大,但是面对隐式内存泄漏仍然昑־苍白无力。所以接下来我们看看如何用Performance Monitor内存泄漏?
使用Performance Monitor内存泄?/strong>
NT的内核在设计q程中已l加入了pȝ监视功能Q比如CPU的用率Q内存的使用情况QI/O操作的频J度{都作ؓ一个个CounterQ应用程序可以通过dq些Counter了解整个pȝ的或者某个进E的q行状况。Performance Monitor是q样一个应用程序?
Z内存泄漏,我们一般可以监视Process对象的Handle CountQVirutal Bytes 和Working Set三个Counter。Handle Count记录了进E当前打开的HANDLE的个敎ͼ监视q个Counter有助于我们发现程序是否有Handle泄漏QVirtual Bytes记录了该q程当前在虚地址I间上用的虚拟内存的大,NT的内存分配采用了两步走的ҎQ首先,在虚地址I间上保留一D늩_q时操作pȝq没有分配物理内存,只是保留了一D地址。然后,再提交这D늩_q时操作pȝ才会分配物理内存。所以,Virtual Bytes一般d于程序的Working Set。监视Virutal Bytes可以帮助我们发现一些系l底层的问题; Working Set记录了操作系lؓq程已提交的内存的总量Q这个值和E序甌的内存总量存在密切的关p,如果E序存在内存的泄漏这个g持箋增加Q但是Virtual Bytes却是跌式增加的?
监视q些Counter可以让我们了解进E用内存的情况Q如果发生了泄漏Q即使是隐式内存泄漏Q这些Counter的g会持l增加。但是,我们知道有问题却不知道哪里有问题Q所以一般用Performance Monitor来验证是否有内存泄漏Q而用BoundsChecker来找到和解决问题?
当Performance Monitor昄有内存泄漏,而BoundsChecker却无法检到Q这时有两种可能Q第一U,发生了偶发性内存泄漏。这时你要确保用Performance Monitor和用BoundsCheckerӞE序的运行环境和操作Ҏ是一致的。第二种Q发生了隐式的内存泄漏。这时你要重新审查程序的设计Q然后仔l研IPerformance Monitor记录的Counter的值的变化图,分析其中的变化和E序q行逻辑的关p,扑ֈ一些可能的原因。这是一个痛苦的q程Q充满了假设、猜惟뀁验证、失败,但这也是一个积累经验的l好Z?/p>
]]>