MFC 调试方法

如果要调试 MFC 程序,这些调试技术可能很有用。

AfxDebugBreak

MFC 为源代码中的硬编码断点提供特殊的 AfxDebugBreak 函数:

AfxDebugBreak( );

在 Intel 平台上, AfxDebugBreak 将生成以下代码,它在源代码而不是内核代码中中断:

_asm int 3

在其他平台上, AfxDebugBreak 只是调用 DebugBreak

请务必在创建发布版本时删除 AfxDebugBreak 语句,或使用 #ifdef _DEBUG 将它们包围起来。

TRACE 宏

若要在调试器 输出窗口中显示来自程序的消息,可以使用 ATLTRACE 宏或 MFC TRACE 宏。 与 断言一样,跟踪宏仅在程序的调试版本中处于活动状态,并在发布版本中编译时消失。

以下示例演示了使用 TRACE 宏的一些方法。 同样 printfTRACE 宏可以处理多个参数。

int x = 1;
int y = 16;
float z = 32.0;
TRACE( "This is a TRACE statement\n" );

TRACE( "The value of x is %d\n", x );

TRACE( "x = %d and y = %d\n", x, y );

TRACE( "x = %d and y = %x and z = %f\n", x, y, z );

TRACE 宏可适当处理 char* 和 wchar_t* 参数。 以下示例演示如何将 TRACE 宏与不同类型的字符串参数一起使用。

TRACE( "This is a test of the TRACE macro that uses an ANSI string: %s %d\n", "The number is:", 2);

TRACE( L"This is a test of the TRACE macro that uses a UNICODE string: %s %d\n", L"The number is:", 2);

TRACE( _T("This is a test of the TRACE macro that uses a TCHAR string: %s %d\n"), _T("The number is:"), 2);

有关 TRACE 宏的详细信息,请参阅 诊断服务

检测 MFC 中的内存泄漏

MFC 提供用于检测分配但从未解除分配的内存的类和函数。

跟踪内存分配

在 MFC 中,可以使用宏 DEBUG_NEW 代替 运算符来帮助查找内存泄漏。 在程序的调试版本中, DEBUG_NEW 跟踪它分配的每个对象的文件名和行号。 编译程序的发布版本时,DEBUG_NEW会被解析为一个简单的new操作,而无需文件名和行号信息。 因此,在程序的“Release”版本中不会造成任何速度损失。

如果您不想重写整个程序以使用 DEBUG_NEW 替代 new,可以在源文件中定义此宏:

#define new DEBUG_NEW

执行 对象转储时,分配 DEBUG_NEW 的每个对象将显示已分配的文件和行号,使你能够查明内存泄漏的来源。

MFC 框架的调试版本会自动使用 DEBUG_NEW,但你的代码不使用。 如果您想要 DEBUG_NEW 的优点,则必须显式地使用 DEBUG_NEW 或如上所示 #define new

启用内存诊断

在使用内存诊断设施之前,必须启用诊断跟踪。

启用或禁用内存诊断

  • 调用全局函数 AfxEnableMemoryTracking 以启用或禁用诊断内存分配器。 由于内存诊断在调试库中默认处于打开状态,因此通常使用此函数暂时关闭它们,从而提高程序执行速度并减少诊断输出。

    使用 afxMemDF 选择特定的内存诊断功能

  • 如果希望对内存诊断功能进行更精确的控制,可以通过设置 MFC 全局变量 afxMemDF 的值来选择性地打开和关闭单个内存诊断功能。 此变量可以具有由枚举类型 afxMemDF 指定的以下值。

    价值 DESCRIPTION
    allocMemDF 打开诊断内存分配器(默认值)。
    delayFreeMemDF 在调用 deletefree 时延迟释放内存,直到程序退出。 这将导致程序分配可能的最大内存量。
    checkAlwaysMemDF 每次分配或释放内存时调用 AfxCheckMemory

    可以通过执行逻辑 OR 操作来组合使用这些值,如下所示:

    afxMemDF = allocMemDF | delayFreeMemDF | checkAlwaysMemDF;
    

拍摄内存快照

  1. 创建 CMemoryState 对象并调用 CMemoryState::Checkpoint 成员函数。 这会创建第一个内存快照。

  2. 在程序执行了其内存分配和释放操作以后,创建另一个 CMemoryState 对象,并为该对象调用 Checkpoint 。 这会获取内存使用情况的第二个快照。

  3. 创建第三个 CMemoryState 对象,并调用其 CMemoryState::Difference 成员函数,同时将前两个 CMemoryState 对象作为参数提供。 如果两个内存状态之间存在差异,该 Difference 函数将返回非零值。 这表示某些内存块尚未解除分配。

    此示例显示代码的外观:

    // Declare the variables needed
    #ifdef _DEBUG
        CMemoryState oldMemState, newMemState, diffMemState;
        oldMemState.Checkpoint();
    #endif
    
        // Do your memory allocations and deallocations.
        CString s("This is a frame variable");
        // The next object is a heap object.
        CPerson* p = new CPerson( "Smith", "Alan", "581-0215" );
    
    #ifdef _DEBUG
        newMemState.Checkpoint();
        if( diffMemState.Difference( oldMemState, newMemState ) )
        {
            TRACE( "Memory leaked!\n" );
        }
    #endif
    

    请注意,内存检查语句由 #ifdef _DEBUG/#endif 块括起来,以便仅在程序的调试版本中编译这些语句。

    知道存在内存泄漏后,可以使用另一个成员函数 CMemoryState::D umpStatistics 来帮助你找到它。

查看内存统计信息

CMemoryState::Difference 函数比较两个内存状态对象,并检测在开始状态和结束状态之间是否有未从堆中释放的对象。 创建内存快照并使用它们 CMemoryState::Difference进行比较后,可以调用 CMemoryState::DumpStatistics 来获取有关尚未解除分配的对象的信息。

请看下面的示例:

if( diffMemState.Difference( oldMemState, newMemState ) )
{
    TRACE( "Memory leaked!\n" );
    diffMemState.DumpStatistics();
}

从该示例得出的转储示例如下所示:

0 bytes in 0 Free Blocks
22 bytes in 1 Object Blocks
45 bytes in 4 Non-Object Blocks
Largest number used: 67 bytes
Total allocations: 67 bytes

可用块是 afxMemDF 设置为 delayFreeMemDF时延迟释放的块。

第二行中显示的普通对象块仍在堆中保持分配状态。

非对象块包括使用new分配的数组和结构。 在此例中,堆中分配了四个非对象块,但均未释放。

Largest number used 提供程序随时使用的最大内存。

Total allocations 提供程序使用的内存总量。

采用对象转储

在 MFC 程序中,可以使用 CMemoryState::DumpAllObjectsSince 来转储堆上尚未释放的所有对象的描述。 DumpAllObjectsSince 转储从最后一个 CMemoryState::Checkpoint。 如果未发生 Checkpoint 调用,则 DumpAllObjectsSince 将转储当前在内存中的所有对象和非对象。

注释

必须先 启用诊断跟踪,然后才能使用 MFC 对象转储。

注释

MFC 会在程序退出时自动转储所有泄漏的对象,因此你无需创建代码来转储对象。

以下代码通过比较两个内存状态来测试内存泄漏,并在检测到泄漏时导出所有对象。

if( diffMemState.Difference( oldMemState, newMemState ) )
{
    TRACE( "Memory leaked!\n" );
    diffMemState.DumpAllObjectsSince();
}

转储的内容如下所示:

Dumping objects ->

{5} strcore.cpp(80) : non-object block at $00A7521A, 9 bytes long
{4} strcore.cpp(80) : non-object block at $00A751F8, 5 bytes long
{3} strcore.cpp(80) : non-object block at $00A751D6, 6 bytes long
{2} a CPerson at $51A4

Last Name: Smith
First Name: Alan
Phone #: 581-0215

{1} strcore.cpp(80) : non-object block at $00A7516E, 25 bytes long

大多数行开头的大括号中的数字指定对象分配的顺序。 最近分配的对象具有最高编号,并显示在转储的顶部。

若要从对象转储获取最大信息量,可以重写 Dump 派生的任何对象的 CObject成员函数,以自定义对象转储。

可以通过将全局变量 _afxBreakAlloc 设置为大括号中显示的数字来设置特定内存分配的断点。 如果重新运行程序,调试器将在执行该分配时中断执行。 然后,可以查看调用堆栈,了解程序如何到达该点。

C 运行时库具有类似的函数 _CrtSetBreakAlloc,可用于 C 运行时分配。

解释内存转储

查看此对象转储的更详细信息:

{5} strcore.cpp(80) : non-object block at $00A7521A, 9 bytes long
{4} strcore.cpp(80) : non-object block at $00A751F8, 5 bytes long
{3} strcore.cpp(80) : non-object block at $00A751D6, 6 bytes long
{2} a CPerson at $51A4

Last Name: Smith
First Name: Alan
Phone #: 581-0215

{1} strcore.cpp(80) : non-object block at $00A7516E, 25 bytes long

生成该转储的程序只有两个显式分配,一个在框架上,另一个在堆上:

// Do your memory allocations and deallocations.
CString s("This is a frame variable");
// The next object is a heap object.
CPerson* p = new CPerson( "Smith", "Alan", "581-0215" );

构造函数CPerson接受三个指向char的参数,这些参数用于初始化CString成员变量。 在内存转储中,可以看到对象 CPerson 以及三个非对象块(3、4 和 5)。 这些保存了 CString 成员变量的字符,并且在调用 CPerson 对象的析构函数时不会被销毁。

块号 2 是 CPerson 对象本身。 $51A4表示块的地址,后面是对象的内容,这些内容是在DumpAllObjectsSince调用时由CPerson::Dump输出的。

你可以猜测块编号1与CString帧变量相关联,因为它的序列号和大小与帧CString变量中的字符数相匹配。 框架上分配的变量在框架超出范围后自动释放。

框架变量

通常,不应担心与帧变量关联的堆对象,因为它们会在帧变量超出范围时自动解除分配。 为了避免内存诊断转储中出现混乱,应将调用定位到 Checkpoint 位置,使其超出帧变量的范围。 例如,将范围括号放在前面的分配代码周围,如下所示:

oldMemState.Checkpoint();
{
    // Do your memory allocations and deallocations ...
    CString s("This is a frame variable");
    // The next object is a heap object.
    CPerson* p = new CPerson( "Smith", "Alan", "581-0215" );
}
newMemState.Checkpoint();

在作用域括号设置好的情况下,此示例的内存转储如下所示:

Dumping objects ->

{5} strcore.cpp(80) : non-object block at $00A7521A, 9 bytes long
{4} strcore.cpp(80) : non-object block at $00A751F8, 5 bytes long
{3} strcore.cpp(80) : non-object block at $00A751D6, 6 bytes long
{2} a CPerson at $51A4

Last Name: Smith
First Name: Alan
Phone #: 581-0215

非对象分配

请注意,某些分配是对象(例如 CPerson),有些是非对象分配。 “非对象分配”指的是未派生自CObject的对象的分配或基元 C 类型(例如charintlong)的分配。 如果 CObject 派生类分配额外的空间(例如用于内部缓冲区),这些对象将显示对象和非对象分配。

防止内存泄漏

请注意,在上面的代码中,与帧变量关联的 CString 内存块已自动解除分配,并且不会显示为内存泄漏。 与范围规则关联的自动释放负责处理与框架变量关联的大多数内存泄漏。

但是,对于在堆上分配的对象,必须显式删除该对象以防止内存泄漏。 若要清理上一示例中的最后一个内存泄漏,请删除 CPerson 在堆上分配的对象,如下所示:

{
    // Do your memory allocations and deallocations.
    CString s("This is a frame variable");
    // The next object is a heap object.
    CPerson* p = new CPerson( "Smith", "Alan", "581-0215" );
    delete p;
}

自定义对象转储

当从 CObject派生类时,在使用 Dump DumpAllObjectsSince 将对象转储到 “输出”窗口 时,可以重写成员函数以提供附加信息。

Dump 函数将对象的成员变量的文本表示形式写入转储上下文(CDumpContext)。 转储上下文类似于 I/O 流。 可以使用 append 运算符 (<<) 将数据发送到 CDumpContext

重写 Dump 函数时,应先调用 Dump 的基类版本以转储基类对象的内容。 然后,输出派生类的每个成员变量的文本说明和值。

函数的 Dump 声明如下所示:

class CPerson : public CObject
{
public:
#ifdef _DEBUG
    virtual void Dump( CDumpContext& dc ) const;
#endif

    CString m_firstName;
    CString m_lastName;
    // And so on...
};

由于对象转储功能仅在调试程序时有意义,因此函数 Dump 的声明用 #ifdef _DEBUG/#endif 块括起来。

在下面的示例中,该 Dump 函数首先为其基类调用 Dump 该函数。 然后,它将每个成员变量的简短说明以及成员的值写入诊断流。

#ifdef _DEBUG
void CPerson::Dump( CDumpContext& dc ) const
{
    // Call the base class function first.
    CObject::Dump( dc );

    // Now do the stuff for our specific class.
    dc << "last name: " << m_lastName << "\n"
        << "first name: " << m_firstName << "\n";
}
#endif

必须提供一个 CDumpContext 参数来指定转储输出的存放位置。 MFC 的调试版本提供一个名为CDumpContext的预定义对象afxDump,其将输出发送到调试器。

CPerson* pMyPerson = new CPerson;
// Set some fields of the CPerson object.
//...
// Now dump the contents.
#ifdef _DEBUG
pMyPerson->Dump( afxDump );
#endif

减小 MFC 调试版本的大小

大型 MFC 应用程序的调试信息可能会占用大量磁盘空间。 可以使用下列过程之一来减小大小:

  1. 使用 /Z7、/Zi、/ZI (调试信息格式) 选项(而不是 /Z7)重新生成 MFC 库。 这些选项生成单个程序数据库 (PDB) 文件,其中包含整个库的调试信息,从而减少冗余并节省空间。

  2. 在没有调试信息的情况下重新生成 MFC 库(无 /Z7、/Zi、/ZI(调试信息格式) 选项。 在这种情况下,缺少调试信息将阻止你在 MFC 库代码中使用大多数调试器设施,但由于 MFC 库已经进行了彻底调试,因此这不是问题。

  3. 仅按如下所述,使用所选模块的调试信息生成自己的应用程序。

使用所选模块的调试信息生成 MFC 应用

通过使用 MFC 调试库生成所选模块,可以使用这些模块中的逐步调试和其他调试功能。 此过程同时使用项目的“调试”和“发布”配置,因此需要执行以下步骤中所述的更改(并在需要完整发布版本时进行“全部重新生成”)。

  1. 在“解决方案资源管理器”中,选择 项目。

  2. “视图 ”菜单中,选择 “属性页”。

  3. 首先,将创建新的项目配置。

    1. <“项目> 属性页 ”对话框中,单击 “配置管理器 ”按钮。

    2. Configuration Manager 对话框中,在网格中找到项目。 在 “配置 ”列中,选择“ <新建...”>

    3. “新建项目配置”对话框中,在 “项目配置名称 ”框中键入新配置的名称,例如“部分调试”。

    4. 复制设置来源列表中,选择发布

    5. 单击“ 确定 ”关闭“ 新建项目配置 ”对话框。

    6. 关闭 “Configuration Manager ”对话框。

  4. 现在,你将为整个项目设置选项。

    1. 在“ 属性页 ”对话框中的 “配置属性” 文件夹下,选择 “常规 ”类别。

    2. 在项目设置网格中,展开 “项目默认值 ”(如有必要)。

    3. “项目默认值”下,找到 “使用 MFC”。 当前设置显示在网格的右列中。 单击当前设置并将其更改为 在静态库中使用 MFC

    4. 在“ 属性页 ”对话框的左窗格中,打开 C/C++ 文件夹,然后选择 预处理器。 在属性网格中,找到 预处理器定义 ,并将“NDEBUG”替换为“_DEBUG”。

    5. “属性页 ”对话框的左窗格中,打开 链接器 文件夹并选择 “输入 类别”。 在属性网格中,查找 其他依赖项。 在 “其他依赖项 ”设置中,键入“NAFXCWD”。LIB“和”LIBCMT”。

    6. 单击 “确定 ”以保存新的生成选项并关闭“ 属性页 ”对话框。

  5. 在“ 生成 ”菜单中,选择“ 重新生成”。 这会从模块中删除所有调试信息,但不会影响 MFC 库。

  6. 现在,必须将调试信息添加回应用程序中的选定模块。 请记住,只能在使用调试信息编译的模块中设置断点和执行其他调试器函数。 对于要在其中包含调试信息的每个项目文件,请执行以下步骤:

    1. 在解决方案资源管理器中,打开位于项目下的 源文件 文件夹。

    2. 选择要为其设置调试信息的文件。

    3. “视图 ”菜单中,选择 “属性页”。

    4. 在“ 属性页 ”对话框中的 “配置设置” 文件夹下,打开 C/C++ 文件夹,然后选择“ 常规 ”类别。

    5. 在属性网格中,找到 “调试信息格式”。

    6. 单击 “调试信息格式 ”设置并选择所需的选项(通常 为 /ZI)以获取调试信息。

    7. 如果使用应用程序向导生成的应用程序或具有预编译标头,则必须在编译其他模块之前关闭预编译标头或重新编译它们。 否则,将收到警告 C4650 和错误消息 C2855。 可以通过更改“项目>属性”对话框中的“创建/使用预编译标头”设置<配置属性文件夹、C/C++子文件夹、预编译标头类别)来关闭预编译标头。

  7. 在“ 生成 ”菜单中,选择“ 生成 ”以重新生成过期的项目文件。

    作为本主题中所述技术的替代方法,可以使用外部生成文件为每个文件定义单个选项。 在这种情况下,若要与 MFC 调试库链接,必须为每个模块定义 _DEBUG 标志。 如果要使用 MFC 发布库,则必须定义 NDEBUG。 有关编写外部 Makefile 的详细信息,请参阅 NMAKE 参考