MFC 调试方法

Posted 风席夜夏

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MFC 调试方法相关的知识,希望对你有一定的参考价值。

MFC 提供特殊的 AfxDebugBreak 函数,以供在源代码中对断点进行硬编码:

 
 
AfxDebugBreak( );

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

 
 
_asm int 3

在其他平台上,AfxDebugBreak 仅调用 DebugBreak。

确保在创建发布版本时移除 AfxDebugBreak 语句,或使用 #ifdef _DEBUG 环绕这些语句。

In this topic

 

TRACE 宏
 
 

 

若要在调试器的“输出”窗口中显示来自程序的消息,可以使用 ATLTRACE 宏或 MFC TRACE 宏。 断言类似,跟踪宏只在程序的“Debug”版本中起作用,在“Release”版本中编译时将消失。

下面的示例显示几种 TRACE 宏的用法。 与 printf 类似,TRACE 宏可处理许多参数。

 
 
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 宏的更多信息,请参见诊断服务

In this topic

 

MFC 提供一些类和函数来检测曾经被分配但从未释放的内存。

在 MFC 中,可以使用 DEBUG_NEW 宏代替 new 运算符来帮助定位内存泄漏。 在程序的“Debug”版本中,DEBUG_NEW 将为所分配的每个对象跟踪文件名和行号。 当编译程序的“Release”版本时,DEBUG_NEW 将解析为不包含文件名和行号信息的简单 new 操作。 因此,在程序的“Release”版本中不会造成任何速度损失。

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

 
 
#define new DEBUG_NEW

当进行对象转储时,用 DEBUG_NEW 分配的每个对象均将显示被分配到的文件和行号,使你可以查明内存泄漏源。

MFC 框架的“Debug”版本自动使用 DEBUG_NEW,但代码不自动使用它。 如果希望利用 DEBUG_NEW 的好处,则必须显式使用 DEBUG_NEW或 #define new,如上所示。

In this topic

必须先启用诊断跟踪,然后才能使用内存诊断功能。

启用或禁用内存诊断

  • 调用全局函数 AfxEnableMemoryTracking 来启用或禁用诊断内存分配器。 由于默认情况下内存诊断在调试库中是打开的,所以通常会使用该函数暂时关闭内存诊断,这会提高程序执行速度并减少诊断输出。

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

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

     

    Value

    描述

    allocMemDF

    打开诊断内存分配器(默认)。

    delayFreeMemDF

    在调用 delete 或 free 时延迟释放内存,直到程序退出。 这将使你的程序分配可能的最大内存量。

    checkAlwaysMemDF

    每次分配或释放内存时均调用 AfxCheckMemory

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

     
    afxMemDF = allocMemDF | delayFreeMemDF | checkAlwaysMemDF;
    

In this topic

 

  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::DumpStatistics,该函数将有助于对其进行定位。

In this topic

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 给出程序所使用的内存总量。

In this topic

在 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

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

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

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

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

In this topic

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

 
 
{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 类型的分配(如 char、int 或 long)。 如果 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;
}

In this topic

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

Dump 函数将对象的成员变量的文本化表示形式写入转储上下文 (CDumpContext)。 转储上下文类似于 I/O 流。 可以使用追加运算符 (<<) 向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 的“Debug”版本提供名为 afxDump 的预定义 CDumpContext 对象,它将输出发送到调试器。

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

In this topic

 

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

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

  2. 重新生成没有调试信息的 MFC 库(没有 /Z7、/Zi、/ZI(调试信息格式) 选项)。 在此情况下,缺少调试信息将妨碍你在 MFC 库代码内使用大多数调试器功能,但由于 MFC 库已完全调试,所以可能不会有问题。

  3. 生成你自己的只带有选定模块的调试信息的应用程序,如下所述。

In this topic

生成带有 MFC 调试库的选定模块以后,你便可以在这些模块中使用单步执行和其他调试功能。 该过程同时利用 Visual C++ 生成文件的“Debug”模式和“Release”模式,从而使得有必要进行下面所描述的更改(也使得在需要完全发布版本时必须进行“全部重新生成”)。

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

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

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

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

    2. “配置管理器”对话框中,在网格中定位你的项目。 “配置”列中,选择“<新建...>”。

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

    4. “从此处复制设置”列表中,选择“Release”。

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

    6. 关闭“配置管理器”对话框。

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

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

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

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

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

以上是关于MFC 调试方法的主要内容,如果未能解决你的问题,请参考以下文章

用vs调试c++,想在输出窗口那里看见指定变量的值,该怎么写?

VS下,在win32中实现trace功能

MFC 调试方法

DebugView工具使用方法

DebugView工具使用方法

OT1.0 + TP3.2开启trace调试输出调试信息开启自定义菜单