程振林,方金云,唐志敏
(中国科学院计算技术研究所,北京 100080)
摘 要:基于C++的软件的缺陷和错误大部分都和内存相关,预防、发现、消除代码中和内存相关的缺陷,成为程序员编写、调试、维护代码时的重要任务。该文基于“面向网络海量空间信息的大型GIS”课题的工程实践,提出和总结了如何使用C++语言机制、开发环境和相关质量保证工具来预防、发现各种编译期、运行期内存缺陷的方法和工具。
关键词:C++;内存错误;内存泄漏;质量保证
Techniques and Tools of Defending Memory-related Defects in Software Coded inC++
CHENG Zhenlin, FANG Jinyun, TANG Zhimin
(Institute of Computing Technology, Chinese Academy of Sciences, Beijing100080)
【Abstract】Most of the defects and errors in the softwarecoded in C++ are memory-related. Based on the practice in the "network,large volumespatial information oriented GIS" project, this paperpresentsthe techniques and tools to find and fix the memory problems during thecoding,debugging and production release phase with the support of the C++language mechanism, development environment and related quality-assurancetools.
【Key words】C++; Memory errors; Memory leak; Quality assurance
C++语言是桌面系统,尤其是系统软件、大型应用软件的主流开发语言。C++语言以其灵活性著称,同时也更复杂。利用C++编写健壮的代码,更具有挑战性。C++允许动态内存管理, 同时也容易导致更多和内存相关的问题。一般而言, 除了系统设计上的缺陷, 基于C++的软件的缺陷和错误大部分都和内存缺陷(主要包括内存访问错误和内存泄漏两类)相关。 所以,消除代码中的内存相关缺陷,成为程序员编写、调试、维护代码中的任务,也是保证软件质量的关键。
本文的工作基于“863”计划项目“面向网络海量空间信息的大型GIS”课题。该系统是基于C++/MFC编写,开发环境是Visual Studio .net 2003。本文基于此项目的工程实践,总结了如何使用C++语言机制、开发环境和相关质量保证工具来预防、发现各种编译期、运行期和内存相关的缺陷的方法和工具。
1 遵循C++相关的编码规范和惯用法,预防缺陷
编码规范是语言相关的规则,是经过实践总结出来的经验。良好的编程标准将有效地帮助开发人员避免开发有潜在危险的代码。一般来说,为了减少内存缺陷,应该遵循下列编码规则[1]:
(1)基类或者带有虚函数的类应该将其析构函数声明为虚函数。
(2)在构造函数中防止内存泄漏,在析构函数中不要抛出异常。
(3)使用对应形式的new和delete。即:用delete来释放new申请的内存,delete[]释放new[]申请的内存。
(4)指针在使用前必须初始化,指向动态内存的指针在释放后应立即置为空。
(5)如果类构造函数中分配了资源,那么需要显式提供拷贝构造函数和赋值操作符,并且在析构函数中释放资源。
值得重视的是C++中的惯用法RAII。RAII核心思想是利用对象来管理资源,在对象的构造函数中获取资源,在其析构函数中释放资源[2]。为了保证动态申请的内存能在即使出现异常的情况下仍能释放,比较理想的方法是使用局部变量来管理动态内存的所有权(ownership),就是所谓的智能指针。STL中的auto_ptr就是为解决资源所有权问题设计的,但是缺少对引用数和数组的支持并且不能用在STL容器中。Boost库[3]提供的智能指针相对成熟,实用价值高。其中,shared_ptr线程安全并且可以用在STL容器中。具体示例参考文献[3]。
1.1 编码规范检查工具 CodeWizard
CodeWizard能够对源程序直接进行自动扫描、分析和检查。一旦发现违例,产生信息告知与哪条规则不符并作出解释。以CodeWizard 4.3 为例,其中内置了超过500条编码标准。CodeWizard可以选择对于当前的工程执行哪些编码标准。CodeWizard可以和VC++紧密集成,安装完毕以后,VC++中有CodeWizard工具条。
1.2 代码检查工具 PC-Lint
PC-Lint可检查编译器不易发现的错误。PC-Lint可对100多个C库函数进行检查,可以发现标准C/C++代码中的1 000多个常见错误。要把PC-lint和VisualStudio集成在一起,需要自己配置。Jon Zyzyck提供了一个报告生成器,可以帮助完成这个工作。可在http://www.ddj.com下载。文献[4]说明了如何在VC++环境中集成PC-Lint。
2 利用语言机制、开发环境和相关工具以预防和发现内存缺陷
发现问题是解决问题的前提。相对于修复内存缺陷,发现内存缺陷并准确定位导致缺陷的代码更为费时费力。及早准确地发现内存缺陷,对于提高开发效率非常重要。
2.1 利用断言及早暴露内存缺陷
断言是布尔调试语句,用来检测在程序运行的时候某一条件的值是否总为真。断言经常用来确认函数的输入、输出,检查对象的当前状态是否合法等。 在以下的场景使用断言可以帮助发现和内存非法访问相关的错误:
(1)验证指针是否可读/写。在函数的入口处,经常需要验证指针所指向的内容区域是否可读/写。 通常采用assert(p!= NULL)的检测形式。 但是,指针的值不为空并不代表指针指向了合法可读/写内存。Win32 API提供了函数IsBadReadPtr、IsBadWritePtr、IsBadStringPtr、IsBadCodePtr用来检测指针指向的内存区域是否可读/写。C运行时库提供了_CrtIs ValidPointer、_CrtIsValidHeapPointer等函数,MFC库提供了AfxIsValidAddress、AfxIsValidString函数来完成类似功能。
(2)对基于MFC的程序,ASSERT_VALID宏通过调用重载的AssertValid函数来确定指向CObject派生类对象的指针是否有效。ASSERT_VALID宏主要调用了AfxIsValidAddress函数和CObject派生类对象的AssertValid函数(参考MFC源代码afx.h、objcore.cpp)。
2.2 利用C运行时刻库检查内存泄漏
VC++的C运行库(CRT)提供了广泛的功能,帮助用户检测内存泄漏。CRT提供了_CrtMemCheckPoint、_CrtDumpMemoryLeaks、_CrtSetDbgFlag等函数来帮助调试内存泄漏。
对于非MFC的工程, 要开启有效的内存泄漏报告功能, 需要进行如下设置:
(1)在StdAfx.h的头部添加如下代码并开启编译器/Yu 选项:
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#define DEBUG_NEW new(_NORMAL_BLOCK,THIS_FILE, __LINE__)
(2)确保在每个.cpp文件的头部包含以下内容:
#include "stdafx.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
(3)在程序的开始处开启报告内存泄漏的开关:
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF|_CRTDBG_LEAK_CHECK_DF);
对于MFC工程, MFC已经做了相关的工作, 只需要确认在每个.cpp文件的头部包含上述第(2)点的内容。
在某些情况下,需要知道发生内存泄漏的内存块中的内容,但是标准的内存转储只是内存块头部的十六进制形式。为了得到更多的有用信息,需要以用户块类型(_CLIENT_ BLOCK)申请内存,并利用_CrtSetDumpClient建立用户块型内存的转储函数。具体的说,对于不是从CObject继承的类,需要:
(1)为每个类/结构指定一个用户块子类型(参考crtdbg.h)。
(2)在申请内存时,采用重载的new形式:void* __cdecl operator new(size_tnSize, int nType, LPCSTR lpszFileName, int nLine)(参考MFC源代码 afxmem.cpp),其中nType就是用户块的子类型。
(3)创建一个用户块内存转储函数,专门对每种需要转储的子类型进行处理(需要包含dbgint.h)。
(4)利用_CrtSetDumpClient对用户块内存转储函数进行注册(参考MFC源代码dumpinit.cpp)。
对于从CObject继承来的类,MFC 已经按照上述方法做了基础工作(参考MFC源代码 afxmem.cpp、dumpinit.cpp)。要有效转储从CObject继承的对象,需要:(1)对每个从CObject继承的类重载虚函数Dump。(2)在程序的初始化部分 加入代码 afxDump.SetDepth(1)来开启深度转储。
2.3 利用Purify和Insure++查找运行时内存缺陷
Rational Purify和Parasoft Insure++ 是用于运行时错误检查的工具。Purify主要检测:数组内存越界读/写,使用未初始化的内存,对已释放的内存进行读/写,内存泄漏等。Insure++利用其专利技术(源码插装和运行时指针跟踪)能够发现大量的内存操作错误,报告错误的源代码行和执行轨迹。根据笔者的测试(基于98个有各种内存错误的C++程序,涵盖了典型情形),Insure++ 6.1都能准确检测。
3 利用VC++环境的调试和诊断功能,检查和发现常见内存缺陷
理解常见的内存缺陷问题以及在VC++环境下的症状,能辅助我们减少问题的发生和及时修改问题。
从错误的表现形式上看, 和堆栈有关的错误主要分为两大类:堆栈溢出和函数返回信息被破坏。
(1)堆栈溢出(overflow)
此类错误主要有两种情形:
1)过大的局部变量。缺省情况下Windows为每个线程保留1M堆栈空间。在菜单Project->Properties->ConfigurationProperties -> Linker->System中可以看到Stack Reserve Size选项可以调整保留的堆栈空间大小。
2)递归调用层数过深。在调试过程中,调用堆栈(call stack)窗口中可以发现函数递归调用的模式。
(2)函数返回信息被破坏
此类错误主要有两种情形:
1)对局部变量的写操作超出了范围(上溢)。在调试过程中,函数堆栈被破坏掉的明显标志是无法显示调用堆栈,并且错误发生在被调用函数即将返回的位置。
2)在调用函数和被调用函数之间如果出现了函数参数的不匹配或者调用规范的不一致。
为了检查此类错误,应该在代码编译时打开/GS、/RTCs开关(在菜单Project->Properties->ConfigurationProperties-> C/C++->Code Generation下设置)。
另外一类错误是动态内存错误。典型的情况如下:
(1)内存写越界。在调试版本中,如果是写上溢,就会收到“Damage:after block...”的跟踪消息,如果是写下溢出就会收到“Damage: before block...”的跟踪消息。
(2)删除不合法指针。在调试版本中,删除未初始化的指针或者非堆指针时,会收到_CrtIsValidHeapPointer断言错误。
(3)多次释放。在调试版本中,如果多次删除同一指针, 会收到_BLOCK_TYPE_IS_VALID断言错误。要防止此类错误,应在delete某个指向动态内存的指针后立即将其置为空。
4 利用Windows结构化异常处理机制处理发布版本软件的内存崩溃
在程序的发布阶段,应尽量减少程序错误尤其是内存崩溃。如果崩溃了,应该“优雅”地退出,尽量收集程序崩溃时的运行信息以帮助程序供应商后续的调试。要捕捉内存非法访问并获知非法访问的指令地址、寄存器内容等信息,需要用到Windows的结构化异常处理(Structured Exception Handling,SEH)机制[6]。MiniDumpWriteDump是dbghelp.dll提供的一个 API函数(参考MSDN),用于转储用户模式程序的一些信息(比如堆栈情况等)并存为一个文件(比如.dmp文件),此文件可以被微软的调试器(VC++或者WinDBG)利用进行事后调试。使用此函数需要dbghelp.h、dbghelp.lib和dbghelp.dll(这些文件可以在Windows Platform SDK中找到)。
要事后根据.dmp文件调试代码,需要为发布版本软件产生debug symbols (pdb)文件(打开编译器/DEBUG选项)。在拿到.dmp文件以后,用VC++打开.dmp文件,然后调试执行(按F5键)。这样,崩溃现场就会重现。文献[5]基于上述的方法实现了崩溃报告系统。
5 结论
实践证明,在上述方法和工具支持下的减少软件内存缺陷的方法和工具,可以有效防止和查找代码中的内存错误和内存泄漏,并且能和开发人员日常编码无缝结合,执行起来非常高效。上述方法配合单元测试、代码评审、每日构建、Bug追踪等措施,形成了一个高效的质量保证流程,在我们的大型平台软件开发过程中起到了重要作用。
参考文献
1 Sutter H, Alexandrescu A. C++ Coding Standards: 101 Rules, Guidelines, andBest Practices[M]. Addison-Wesley Professional, 2004-10.
2 Stroustrup B. The Design and Evolution of C++[M]. Addison-WesleyProfessional, 1994-03.
3 Karlsson B. Beyond the C++ Standard Library: An Introduction to Boost[M].Addison-Wesley Professional, 2005-08.
4 Zyzyck J. A Report Generator for PC-Lint[J]. Dr. Dobb's Journal, 2003, 28(2):52.
5 Dietrich H. XCrash Report: Exception Handling and Crash Reporting[Z].2003-10. http://www.codeproject.com/debug/ XCrash ReportPt4.asp.
6 Richter J M. Programming Applications for Microsoft Windows[M]. MicrosoftPress, 1999-09.
C/C++ 编程语言的最强大功能之一便是其动态分配和释放内存,但是中国有句古话:“最大的长处也可能成为最大的弱点”,那么 C/C++ 应用程序正好印证了这句话。在 C/C++ 应用程序开发过程中,动态分配的内存处理不当是最常见的问题。其中,最难捉摸也最难检测的错误之一就是内存泄漏,即未能正确释放以前分配的内存的错误。偶尔发生的少量内存泄漏可能不会引起我们的注意,但泄漏大量内存的程序或泄漏日益增多的程序可能会表现出各种各样的征兆:从性能不良(并且逐渐降低)到内存完全耗尽。更糟的是,泄漏的程序可能会用掉太多内存,导致另外一个程序垮掉,而使用户无从查找问题的真正根源。此外,即使无害的内存泄漏也可能殃及池鱼。
幸运的是,VisualStudio 调试器和 C 运行时 (CRT) 库为我们提供了检测和识别内存泄漏的有效方法。下面请和我一起分享收获——如何使用 CRT 调试功能来检测内存泄漏?
一、如何启用内存泄漏检测机制
VC++ IDE 的默认状态是没有启用内存泄漏检测机制的,也就是说即使某段代码有内存泄漏,调试会话的 Output 窗口的 Debug 页不会输出有关内存泄漏信息。你必须设定两个最基本的机关来启用内存泄漏检测机制。
一是使用调试堆函数:
#define _CRTDBG_MAP_ALLOC
#include<stdlib.h>
#include<crtdbg.h>
注意:#include 语句的顺序。如果更改此顺序,所使用的函数可能无法正确工作。
通过包含crtdbg.h 头文件,可以将 malloc和 free 函数映射到其“调试”版本 _malloc_dbg 和 _free_dbg,这些函数会跟踪内存分配和释放。此映射只在调试(Debug)版本(也就是要定义_DEBUG)中有效。发行版本(Release)使用普通的 malloc 和 free 函数。#define 语句将 CRT 堆函数的基础版本映射到对应的“调试”版本。该语句不是必须的,但如果没有该语句,那么有关内存泄漏的信息会不全。
二是在需要检测内存泄漏的地方添加下面这条语句来输出内存泄漏信息:
_CrtDumpMemoryLeaks();
当在调试器下运行程序时,_CrtDumpMemoryLeaks将在 Output 窗口的 Debug 页中显示内存泄漏信息。比如: Detected memoryleaks!
Dumping objects ->
C:\Temp\memleak\memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 byteslong.
Data: <AB> 41 42
c:\program files\microsoft visual studio\vc98\include\crtdbg.h(552) : {44}normal
block at 0x00441BD0, 33 bytes long.
Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD
c:\program files\microsoft visual studio\vc98\include\crtdbg.h(552) : {43}normal
block at 0x00441C20, 40 bytes long.
Data: < C > 08 02 43 00 16 00 00 00 00 00 00 00 00 00 00 00
Object dump complete.
如果不使用#define _CRTDBG_MAP_ALLOC 语句,内存泄漏的输出是这样的:
Detected memory leaks!
Dumping objects ->
{45} normal block at 0x00441BA0, 2 bytes long.
Data: <AB> 41 42
{44} normal block at 0x00441BD0, 33 bytes long.
Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD
{43} normal block at 0x00441C20, 40 bytes long.
Data: < C > C0 01 43 00 16 00 00 00 00 00 00 00 00 00 00 00
Object dump complete.
根据这段输出信息,你无法知道在哪个源程序文件里发生了内存泄漏。下面我们来研究一下输出信息的格式。第一行和第二行没有什么可说的,从第三行开始:
xx}:花括弧内的数字是内存分配序号,本文例子中是 {45},{44},{43};
block:内存块的类型,常用的有三种:normal(普通)、client(客户端)或 CRT(运行时);本文例子中是:normal block;
用十六进制格式表示的内存位置,如:at 0x00441BA0 等;
以字节为单位表示的内存块的大小,如:32 bytes long;
前 16 字节的内容(也是用十六进制格式表示),如:Data: 41 42 等;
仔细观察不难发现,如果定义了 _CRTDBG_MAP_ALLOC ,那么在内存分配序号前面还会显示在其中分配泄漏内存的文件名,以及文件名后括号中的数字表示发生泄漏的代码行号,比如:
C:\Temp\memleak\memleak.cpp(15)
双击 Output 窗口中此文件名所在的输出行,便可跳到源程序文件分配该内存的代码行(也可以选中该行,然后按 F4,效果一样) ,这样一来我们就很容易定位内存泄漏是在哪里发生的了,因此,_CRTDBG_MAP_ALLOC 的作用显而易见。
使用 _CrtSetDbgFlag
如果程序只有一个出口,那么调用 _CrtDumpMemoryLeaks 的位置是很容易选择的。但是,如果程序可能会在多个地方退出该怎么办呢?在每一个可能的出口处调用 _CrtDumpMemoryLeaks 肯定是不可取的,那么这时可以在程序开始处包含下面的调用:_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );这条语句无论程序在什么地方退出都会自动调用_CrtDumpMemoryLeaks。注意:这里必须同时设置两个位域标志:_CRTDBG_ALLOC_MEM_DF 和 _CRTDBG_LEAK_CHECK_DF。
设置 CRT 报告模式
默认情况下,_CrtDumpMemoryLeaks将内存泄漏信息 dump 到 Output 窗口的 Debug 页, 如果你想将这个输出定向到别的地方,可以使用 _CrtSetReportMode 进行重置。如果你使用某个库,它可能将输出定向到另一位置。此时,只要使用以下语句将输出位置设回 Output 窗口即可:
_CrtSetReportMode( _CRT_ERROR,_CRTDBG_MODE_DEBUG );
有关使用_CrtSetReportMode 的详细信息,请参考 MSDN 库关于_CrtSetReportMode 的描述。
二、解释内存块类型
前面已经说过,内存泄漏报告中把每一块泄漏的内存分为 normal(普通块)、client(客户端块)和 CRT 块。事实上,需要留心和注意的也就是 normal 和 client,即普通块和客户端块。
1.normalblock(普通块):这是由你的程序分配的内存。
2.clientblock(客户块):这是一种特殊类型的内存块,专门用于 MFC 程序中需要析构函数的对象。MFCnew 操作符视具体情况既可以为所创建的对象建立普通块,也可以为之建立客户块。
3.CRT block(CRT 块):是由 C RunTime Library 供自己使用而分配的内存块。由 CRT 库自己来管理这些内存的分配与释放,我们一般不会在内存泄漏报告中发现 CRT 内存泄漏,除非程序发生了严重的错误(例如 CRT 库崩溃)。
除了上述的类型外,还有下面这两种类型的内存块,它们不会出现在内存泄漏报告中:
1.free block(空闲块):已经被释放(free)的内存块。
2.Ignoreblock(忽略块):这是程序员显式声明过不要在内存泄漏报告中出现的内存块
三、如何在内存分配序号处设置断点
在内存泄漏报告中,的文件名和行号可告诉分配泄漏的内存的代码位置,但仅仅依赖这些信息来了解完整的泄漏原因是不够的。因为一个程序在运行时,一段分配内存的代码可能会被调用很多次,只要有一次调用后没有释放内存就会导致内存泄漏。为了确定是哪些内存没有被释放,不仅要知道泄漏的内存是在哪里分配的,还要知道泄漏产生的条件。这时内存分配序号就显得特别有用——这个序号就是文件名和行号之后的花括弧里的那个数字。
例如,在本文例子代码的输出信息中,“45”是内存分配序号,意思是泄漏的内存是你程序中分配的第四十五个内存块:
Detected memory leaks!
Dumping objects ->
C:\Temp\memleak\memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 byteslong.
Data: <AB> 41 42
......
Object dump complete.
CRT 库对程序运行期间分配的所有内存块进行计数,包括由 CRT 库自己分配的内存和其它库(如 MFC)分配的内存。因此,分配序号为 N 的对象即为程序中分配的第 N 个对象,但不一定是代码分配的第 N 个对象。(大多数情况下并非如此。)这样的话,你便可以利用分配序号在分配内存的位置设置一个断点。方法是在程序起始附近设置一个位置断点。当程序在该点中断时,可以从 QuickWatch(快速监视)对话框或 Watch(监视)窗口设置一个内存分配断点:
例如,在 Watch 窗口中,在 Name 栏键入下面的表达式:
_crtBreakAlloc
如果要使用 CRT 库的多线程 DLL 版本(/MD 选项),那么必须包含上下文操作符,像这样:
{,,msvcrtd.dll}_crtBreakAlloc
现在按下回车键,调试器将计算该值并把结果放入 Value 栏。如果没有在内存分配点设置任何断点,该值将为 –1。
用你想要在其位置中断的内存分配的分配序号替换 Value 栏中的值。例如输入 45。这样就会在分配序号为 45的地方中断。
在所感兴趣的内存分配处设置断点后,可以继续调试。这时,运行程序时一定要小心,要保证内存块分配的顺序不会改变。当程序在指定的内存分配处中断时,可以查看 Call Stack(调用堆栈)窗口和其它调试器信息以确定分配内存时的情况。如果必要,可以从该点继续执行程序,以查看对象发生了什么情况,或许可以确定未正确释放对象的原因。
尽管通常在调试器中设置内存分配断点更方便,但如果愿意,也可在代码中设置这些断点。为了在代码中设置一个内存分配断点,可以增加这样一行(对于第四十五个内存分配):
_crtBreakAlloc = 45;
你还可以使用有相同效果的_CrtSetBreakAlloc 函数:
_CrtSetBreakAlloc(45);
四、如何比较内存状态
定位内存泄漏的另一个方法就是在关键点获取应用程序内存状态的快照。CRT 库提供了一个结构类型_CrtMemState。你可以用它来存储内存状态的快照:
_CrtMemState s1, s2, s3;
若要获取给定点的内存状态快照,可以向 _CrtMemCheckpoint 函数传递一个 _CrtMemState 结构。该函数用当前内存状态的快照填充此结构:
_CrtMemCheckpoint( &s1 );
通过向_CrtMemDumpStatistics 函数传递 _CrtMemState 结构,可以在任意地方 dump 该结构的内容:
_CrtMemDumpStatistics( &s1 );
该函数输出如下格式的dump 内存分配信息:
0 bytes in 0 Free Blocks.
75 bytes in 3 Normal Blocks.
5037 bytes in 41 CRT Blocks.
0 bytes in 0 Ignore Blocks.
0 bytes in 0 Client Blocks.
Largest number used: 5308 bytes.
Total allocations: 7559 bytes.
若要确定某段代码中是否发生了内存泄漏,可以通过获取该段代码之前和之后的内存状态快照,然后使用 _CrtMemDifference 比较这两个状态:
_CrtMemCheckpoint( &s1 );// 获取第一个内存状态快照
// 在这里进行内存分配
_CrtMemCheckpoint( &s2 );// 获取第二个内存状态快照
// 比较两个内存快照的差异
if ( _CrtMemDifference( &s3, &s1, &s2) )
_CrtMemDumpStatistics( &s3 );// dump 差异结果
顾名思义,_CrtMemDifference比较两个内存状态(前两个参数),生成这两个状态之间差异的结果(第三个参数)。在程序的开始和结尾放置 _CrtMemCheckpoint 调用,并使用 _CrtMemDifference 比较结果,是检查内存泄漏的另一种方法。如果检测到泄漏,则可以使用 _CrtMemCheckpoint 调用通过二进制搜索技术来分割程序和定位泄漏。
五、结论
尽管 VC ++ 具有一套专门调试 MFC 应用程序的机制,但本文上述讨论的内存分配很简单,没有涉及到 MFC 对象,所以这些内容同样也适用于 MFC 程序。在 MSDN 库中可以找到很多有关 VC++ 调试方面的资料,如果你能善用 MSDN 库,相信用不了多少时间你就有可能成为调试高手。