//第十一章调试技术 // 如何在VisualC++2008调试器的控制下运行程序 // 如何每次一条语句地单步调试程序 // 如何监控或修改程序中变量的值 // 如何监控程序中表达式的值 // 调用堆栈 // 断言的概念,如何使用断言来检查代码 // 如果给程序添加调试代码 // 如何检测本地C++程序中的内存泄漏 // 在C++/CLI程序中如何使用执行跟踪功能,如何生成调试输出 //11.1.1 程序故障 //语法错误: 这些是因形式错误语句而引起的错误,比如漏写了语句最后的分号,或者在应该使用逗号的地方使用冒号,我们不必过多担心话法错误,编译器能够了识别所有语法错误,通常还会给相当有益的关于错误的提示信息,所以此类错误还是很容易改正的 //语义错误:在这些错误中,代码在语法上正确有,但却不能做我们本来想做的事情,编译器无法知道我们想要在程序中达到什么目的,因此不可能检测到语义错误,不过,我们往往可以得到某种表明程序有错误的迹像,因为程序将导常终止,VisualC++2007中的调试功能就是为了帮助我们发现语义错误,语义错误可能非常微妙且难以发现 //比如程序偶尔才产生错误的结果或者偶尔才崩溃,在并发的执行路径未被妥善管理的多线程程序中,可以发生最难发现的此类错误 //11.1.2 觉故障 //1 数据被破坏: 没有初始化变量 超出了整数类型的范围 无效的指针 数组索引表达工中存在错误 循环条件错误 // 动态分配的数组的大小错误 没有实现类的复制构造函数,赋值运算符或析构函数 //2 未经处理的异常 //无效的指针或引用 //缺少catch代代块 //3 程序挂起或崩溃 //没有初始化变量 //无穷循环 //无效指针 //两次释放同一自由存储器中的内存 //没有实现类的析构函数,或者析构函数中存在错误 //没有正确的处理未预期的用户输入 //4 流输入数据不正确 使用了提取运算符和getline()函数执行读取操作 //5 结果不正确 //打字错误: -= 代替==, 或者i代替了j等 //没有初始化变量 //超出了整数类型范围 //无效指针 //switch语句中遗漏了break; //无效指针出现的常见方式如下: //声明指针时没有初始化 //当删除已分配的空间时,没有将指向自由存储器内存的指针设定为空指针 //从函数中返回了局部变量的地址 //没有为分配自由存储器内存的类实现复制构造函数和赋值运算符 //11.2 基本的调试操作 //来自第四章的实例 //11.2.1 设置断点 //我们不仅可以在某个存储单元而非语句开始处设定断点,而且可以在特定布尔表达式结果为true时设置定断点, //11.2.2 设置跟踪点 //跟踪点是一种特殊的断点,具有相关联的自定义动作,创建跟踪点的方法是右击希望设置跟踪点的代码行,然后从弹出菜单中选择 //11.2.3 启动调试模式 //1 检查变量值 //定义某个希望检查的变量被称为设置该变量的监视 //自动窗口有五个选项卡,各选择卡显示的住处如下所述 //应该是自动窗口 //Autos选项显示当前误句及相关的前一条语句(换句话说,就是Editor窗格中箭头指向的语句以及该语句之前的那条语句)正在使用的自动变量 //(局部变量)Locals选项卡显示当前函数中局部变量的值,通常,新变量随着程序的执行依次进入其作用域,然后在我们退出定义它们的代码块时再离开其作用域, //在本例中,该窗口始终显示numbre1, numbre2,和pnumber的值,因为我们只有一个由单一代码块组成的函数main() //(线程)Threads 选项卡允许我们检查并控制高级应用程序中的线程 //(模块)Modules选项卡列出当前执行的代码模块的细节,如果应用程序崩溃,通过比较崩溃发生时的地址与该选项卡上Address列的地址范围,我们就可以确定崩溃发生在哪个模块中 //2 在编辑窗口中查看变量 //11.2.4 修改变量的值 /*#include <iostream> using namespace std; int main() { int count = 30000; for(int i=0; i<count; i++){ int s = i*i; cout<<"s:"<<s<<endl; } long* pnumber = NULL; long number1 = 55, number2 = 99; pnumber = &number1; *pnumber += 11; //66 cout<<endl; cout<<"number1="<<number1<<endl; cout<<"&number1 = "<<hex<<pnumber; pnumber = &number2; //99 number1 = *pnumber * 10; //990 cout<<endl; cout<<"number1 = "<<dec<<number1 <<" pnumber = "<<hex<<pnumber <<" *pnumber = "<<dec<<*pnumber; cout<<endl; system("pause"); return 0; } */ //11.3 添加调试代码 //11.3.1 使用断言 //标准库文件<cassert>包含着assert()函数的声明,该函数在特殊的预处理器符号NDEBUG未定义时,可用来检查程序内逻辑条件, //void assert(int expression) //#define NDEBUG //#include <cassert> //如果给assert()传递的实参表达式不是0(即true),则该函数什么也不做,如果该表达式为0(换句话说是false),并且定义了NDEBUG符号 //,则该函数将输出一条诊断消息,显示出错的表达式,源文件名,源文件中发生错误的行号等信息,在显示诊断消息之后,assert()函数将调用 //abort()来终止程序, //10.3.2 添加自己的调试代码 //除了NDEBUG符号以外,我们可以使用另一个预处理器符号_DEBUG,这是一种更好,更积极的控制机制,该符号在Visual C++自动在程序的调试版本中 //定义的,但没有在发布版本定义,借助于_DEBUG符号的测试,我们只需将仅希望在调试试时编译和执行的代码放入预处理器指令对#ifdef / #endif之间即可 //#ifdef _DEBUG //code 内容 //#endif //-DEBUG //考虑一个简单的例子,假设我们使用了两个自己和符号来控制调试代码:管理"正常"调试代码的MYDEBUG和VOLUMEDEBUG //它控制着产生大量输出的代码,而我们偶尔才需要大量的输出,我们可以使用这两个符号仅当_DEBUG已经定义时才被定义 /*#ifdef _DEBUG //判断是否存在 #define MYDEBUG //定义 #define VOLUMEDEBUG //定义 #endif*/ //11.4.2 单步执行到出错位置 //11.5 测试扩展的类 //Name类添加重载运算符定义的时候,使用在<cstring>头文件中声明的比较函数, //11.6 调试动态内存 //11.6.1 检查自由存储器的函数 //本小节检查自由存储器涉及哪些操作,以及如何检测内存泄漏问题 //在ctrdbg.h文件中声明的函数是使用在_CrtMemState类型的结构中存储的内存状态检查自由存储器的,该结构相当简单 /*typedef struct _CrtMemState { struct _CrtMemBlockHeader* pBlockHeader; unsigned long lCounts[_MAX_BLOCKS]; unsigned long lSizes[_MAX_BLOCKS]; unsigned long lHighWaterCount; unsigned long lTotalCount; } _CrtMemState; //最常用的五个函数 //1 记录任何时刻自由存储器的状态 //2 确定两种自由存储器状态之间的差别 //3 输出状态信息 //4 输出与自由存储器中的对像有关的信息 //5 检测内存泄漏 void _CrtMemCheckpoint(_CrtMemState* state); //函数在_CrtMemState结构中存储自由存储器的当前状态,给该函数传递的实参是指向某个_CrtMemState结构的指针,状态信息将被记录在该结构中 int _CrtMemDifference(_CrtMemState* stateDiff, const _CrtMemState* oldState, const _CrtMemState* newState); //该函数比较第三个实参指定的状态与第二个实参指定的先前状态,两者的差别被存储在第一个实参指定的_CrtMemState结构中,如果两种状态不同,该函数将返回一个非零值(true),否则返回0(false) void _CrtMemDumpStatistics(const _CrtMemState* state); //该函数将与实参指定的自由存储器状态有关的信息转储到输出流中,实参指向的状态结构可能是使用_CrtMemCheckpoint()函数记录的状态 //也可以是_CrtMemDifference()函数产生的两种状态之间的差别 void _CreMemDumpAllObjectsSince(const _CrtMemState* state); //该函数转储自实参指定的自由存储器状态以来,在自由存储器中分配的对像的信息,指定的状态是程序中先前通过调用_CrtMemCheckpoint()函数而记录的,如果我们给该函数传递空指针,该函数将转储自从程序开始执行以来,与所有已分配的对像有关的信息 int _CrtDumpMemoryLeaks(); //我本示例中所需要的函数,因为该函数检查内存汇漏情况,并转储与任何检测到的遗漏有关的信息,我们在任何时候都可以调用该函数,但有一种非常有用的机制可以使该函数在程序结束时被自动调用。如果我们启用这种机制,那么对程序执行过程中有无发生内存汇漏的检测将自动进行 //11.6.2 控制自由存储器的调试操作 //我们通过设置int类型的标志_crtDbgFlag,来控制自由存储器的调试操作,该标志包括5个独立的控制位 _CRTDBG_ALLOC_MEM_DF //该位为1时,将启用调试地址分配功能,使我们可以跟踪自由存储器的状态 _CRTDBG_DELAY_FREE_MEM_DF //该位为1时,将阻止delete运算符被分配的内存,这样我们就可以确定在低内存条件下将发生什么事情 _CRTDBG_CHECK_ALWAYS_DF //该位为1时,将使得每次使用new和delete运算符时都自动调用_CrtCheckMemory()函数,该函数将验证自由存储器的完整性 //比如说检查有没有因存储超出数组范围的数值而被重写的内存块。如果发现有任何缺陷,该函数都将输出一份报告 //该函数会降低执行速度,但可以迅速帮我们找到错误 _CRTDBG_CHECK_CRT_DF //该位为1时,调试操作将跟踪在内部由运行库使用的内存 _CRTDBG_LEAK_CHECK_DF //该位为1时,将在程序退出时通过自动调用_CrtDumpMemoryLeaks()函数执行内存泄漏检查,仅当我们在程序未能释放所有先前分配的内存时,我们才能得到自该函数的输出 //默认情况下_CRTDBG_ALLOC_MEM_DF位为1,其他所有位都是0。 int flag = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG); flag |= _CRTDBG_LEAK_CHECK_DF; flag &= ~_CRTDBG_CHECK_CRT_DF; _CrtSetDbgFlag(flag); //如果我们只需要在程序退出时执行内存泄漏检测,则可以使用下面这条语句 _CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF|_CRTDBG_ALLOC_MEM_DF); */ //11.6.3 自由存储器的调试输出 //自由存储器调试函数的输出目的地不是标准的输出流,默认情况下该目的地是调试消息窗口 //_CrtSetReportMode()设置定输出的一般目的地 //_CtrSetReportFile() 指定一个流目的地 //_CrtSetReportMode() 声明 //int _CrtSetReportMode(int reportType, int reportMode); //我们使用下列标识符之一指定报告类型 //_CRT_WARN 各种警告消息,检测到内存泄漏时的输出就属于警告消息 //_CRT_ERROR 报告不可恢复问题的灾难性错误消息 //_CRT_ASSERT 来自断言的输出(不是前面讨论过的assert()函数的输出) //我们使用下列标识符的组事指定报告模式: //_CRTDBG_MODE_DEBUG 这是默认模式,输出将被发送到某个调试字符串,在调试器的控制下运行程序时,我们将在调试窗口中看到该字符串 //_CRTDBG_MODE_FILE 输出被定向到某个输出流 //_CRTDBG_MODE_WNDW 输了在一个消息框中给出 //_CRTDBG_REPORT_MODE 如果传递该实参,则_CtrSetReportMode()函数仅仅返回当前的报告模式 //CrtSetReportMOde(_CRT_WARN, _CRTDBG_MODE_FILE); //_CrtSetReportFile()函数的声明如下 //_HFILE _CrtSetReportFile(int reportType, _HFILE reportFile); //此处的第二个实参可以是指向某个_HFILE类型文件流的指针,也可以是下列标识符之一 //_CRTDBG_FILE_STDERR 输出被定向到标准错误流stderr //_CRTDBG_FILE_STDOUT 输出被定向到标准输出流stdout //_CRTDBG_REPORT_FILE 如果指定该实参,则_CrtSetReportFile()函数仅仅返回当前的目的地 //_CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDOUT); //11.7 调试C++/CLI程序 //C++/CLI编程更加简单,在为CLR编写的程序中,任何像指针被破坏或内丰泄漏这样的错综复杂的问题都不会出现,因此相对本地C++而言,一下子就大大减少了调试问题 //11.7.1 使用调试和跟踪类 //System::Diagnostics命名空间中的Debug和Trace类用于为调试目的跟踪程序的执行 //Debug和Trace类提供的功能是相同的,二者之间的区别是Trace函数将被编译到发布版本中,而Debug函数不会 //1 生成输出 //我们可以使用向输出目的地写入消的Debug::WriteLine()和Debug::Write()函数产生输出,这两个函数之间的区别是WriteLine()函数在输出消息之后将再输出一个换行字符,而Write()函数则不然 //Debug::Write(String^ message) 将message写入输出目的地 //Debug::Write(String^ message, String^ category) //将category和message依次写入输出目的地,类名用来组织输出 //Debug::Write(Object^ value); 将value->ToString()返回的字符串写入输出目的地 //Debug::Write(Object^ value, String^ category); //WriteIf()和WriteLineIf()函数是Write()和WriteLine()函数的有条件版本 //Debug::WriteIf(bool condition, String^ message) 如果condition为true,则将message写入输出目的地,否则不产生输出 //Debug::WriteIf(bool condition, String^ message, String^ category) //如果condition为true,则将category和message依次写入输出目的地,否则不产生输出 //Debug::WriteIf(bool condition, Object^ value) 如果condition为true,否将value->ToString()返回的字符串写入输出目的地;否则不产生输出 //Debug::WriteIf(bool condition, Object^ value, String^ category) 如果condition为true,则将category和value->ToString()返回的字符串依次写入输出目的地,否则不产生输出 //Debug::Print()函数来产生输出 //Print(String^ message) 该函数将后跟换行字符的message写入输出目的地 //Print(String^ format, ...array<Object^>^ args) 该函数的工作方式与Console::WriteLine()函数的格式化输出相同,格式字符串决定着如何在输出中显示后跟的实参 //2 设定输出目的地 TextWriterTraceListener^ listener = gcnew TextWriteTraceListener(Console::Out); //创建TextWriteTraceListener对像将输出定向到Console类中静态属性Out返回标准输出流(Console类的In和Error属性分别返回标准输入流和标准错误流),Debug类的Listeners属性返回用于调试输出的侦听器集合, Debug::Listeners->Add(listener); //侦听器对像添加到该集合中,我们可以添加其他将输出定向到其他地方(可能是某个文件)的侦听器 //3 缩排输出 Debug::Indent() //为了使当前的缩排级别减一,我们应当调用静态的Unindent()函数 Debug::Unindent(); //当前的缩排级别记录在Debug类的静态属性IndexLevel中,因此我们可能通过该属性获得或设置当前的缩排级别 Debug::IndentLevel = 2*Debug::IndetLevel; Console::WriteLine(L"Current indent unit = {0}", Debug::IndentSize); Debug::IndentSize = 2; //第一条语句仅仅输出单位缩排量,第二条语句将其修改为新值,之后对Indent()的调用将根据新值--即两个空格,增加当前的缩排量 //4 控制输出 //跟踪开关给我们提供了打开和关闭任何调试或跟踪输出的方法 //BooleanSwitch引用类对像给我们提供了根据该开关的状态,打开或关闭输出段的方法 //TraceSwitch引用类对像给我们提供了一种更复杂的控制机制,因为每个对像都有四个对应于四种植输出语句控制级别的属性 public ref class MyClass { private: static BooleanSwitch^ errors = gcnew BooleanSwitch(L"Error Switch", L"Controls error output"); public: void DoIt() { if(errors->Enabled){ Debug::WriteLine(L"Error in Doit()"); } } } //TraceSwitch对像的Level属性属于枚举类类型TraceLevel,我们可以将控制跟踪输出的该属性设定为有11-6中所示的任意一个值 TraceLevel::Off 不产生跟踪输出 TraceLevel::Info 输出通知,警告和出错消息 TraceLevel::Warning 输出警告和出错消息 TraceLevel::Error 输出出错消息 TraceLevel::Verbose 输出所有消息 //我们可以通过测试TraceSwitch对四种bool类型属性之一的状态,来确定某条特定的消息是否应该由跟踪和调试代码发布 TraceVerbose 当输出所有消息时,返回true TraceInfo 当输出通知消息时,返回true TraceWarning 当输出警告消息时,返回true TraceError 当输出出错消息时,返回true //5 断言 //Debug和Trace类都有静态的Assert()函数,可提供与本地C++的assert()函数类似的功能 //三个重载的Assert()函数版本 Debug::Assert(bool condition) //当condition为falsh时,将出现一个显示此时调用堆栈的对话框 Debug::Assert(bool condition, String^ message)与上面相同,但还将对话框中调用堆栈信息的前面显示message Debug::Assert(bool condition, String^ message, String^ details) 与前面的版本相同,但还将在对话中显示details //小结 //我们可以使用<cassert>头文件中声明的assert()库函数,检查本地C++程序中应该始终为true的逻辑条件 //在本地C++程序的调试版本中,预处理器符号_NDEBUG是自动定义的,该符号不会在发布版本中被定义 //我们可以添加自己的调试代码,方法是将其放入测试有无定义_NDEBUG符号的#ifdef #endif指令对之间,这样,添加的调试代码将只包括在程序的调试版本中 //crtdbg.h头文件提供了调试自由存储器操作的函数的声明 //通过适当设置_crtDbgFlag,我们可以启用程序的自动检测内存泄漏功能 //为了定向来自自由存储器调试函数的输出消息,应当调用_CrtSetReportMode()和_CrtSetReportFile()函数 //在C++/CLI程序中使用断点和跟踪点的调试操作与本地C++程序中完全相同 //System::Diagnostics命名空间中定义的Debug和Trace类提供了在CLR程序中跟踪执行并生成调试输出的函数 //Debug和Trace类的静态Assert()函数提供了CLR程序中的断言功能