## 输出调试信息 我们讲解了 assert 断言函数的使用,本节我们来学习在调试器的调试窗口上输出我们自己的调试信息,在这里,我们将用到一个 Windows 操作系统提供的函数 —— OutputDebugString,这个函数非常常用,他可以向调试输出窗口输出信息(无需设置断点,执行就会输出调试信息),并且一般只在绑定了调试器的情况下才会生效,否则会被 Windows 直接忽略。接下来我们了解一下这个函数的使用方法。 首先,这个函数在 windows.h 中被定义,所以我们需要包含 windows.h 这个头文件才能使用 OutputDebugString 函数。这个函数的使用方式非常的简单,它只有简单的一个参数——我们要输出的调试信息。但是有一点值得注意:准确来说 OutputDebugString 并不是一个函数,他是一个宏。在高版本的 Visual Studio 中,因为编译的时候 Visual Studio 默认定义了 UNICODE 宏,所以我们查找 OutputDebugString 的定义会看到如下代码: ```c #ifdef UNICODE #define OutputDubugString OutputDebugStringW #else #define OutputDebugString OutputDebugStringA #endif //一般是_MBCS宏 #ifdef _MBCS #define OutputDubugString OutputDebugStringW #else #define OutputDebugString OutputDebugStringA #endif ``` 我们可以从代码高亮上看到,OutputDebugString 实际上等价于 OutputDebugStringW,这就意味着我们必须传入宽字符串(事实上只要定义了 UNICODE ,调用所有 Windows 提供的函数都需要使用宽字符),或者使用 TEXT 或 _T 宏,并且这是最好的方法,这个宏会自动识别编译器是否处于默认宽字符的状态并对传入字符串做些处理,使用这个宏可以加强代码对不同编译器不同编译参数的兼容性。下面我们就来看一段示例代码: ```c #include #include int main() { OutputDebugString(TEXT("汪跃东的个人网站:")); OutputDebugString(TEXT("https://wangyuedong.com")); //也可以:OutputDebugStringA("大家好才是真的好。"); //也可以:OutputDebugStringW(L"大家好才是真的好。"); //使用自动字符宏 TEXT 或者 _T 可以自动判断是否使用宽字符 system("pause"); return 0; } ``` F5调试一下,这个函数与 printf 等函数一样,需要我们自己插入换行符,但在 Windows 下,我们一般使用 \r\n 作为完整的换行符。 直接使用这个调试信息输出函数有个弊端,那就是它不能直接输出含参数的字符串。但是我们可以通过 sprintf / wsprintf 等缓冲区写入函数来间接实现输出含参数字符串的功能。下面是一段示例代码: ```c #include #include int main(){ //注意!这段代码我们指定使用ANSI字符! char szBuffer[200]; int number = 100; sprintf_s(szBuffer, "变量 number 的值是 %d \r\n", number); //写入缓冲区,注意不要溢出 OutputDebugStringA(szBuffer); sprintf_s(szBuffer, "变量 number 的地址是 %x \r\n", &number); OutputDebugStringA(szBuffer); //我门指定使用 ANSI 版本的 OutputDebugString 函数 return 0; } ``` 我们看到了输出的结果,怎么样,大家是不是觉得这样调用这个函数很麻烦?为解决此问题,这里C语言中文网为大家提供了一个更好的解决方案,我们可以自己写一个前端函数,然后保存到头文件中(编译生成 dll 也可以,有兴趣的同学可以试试)。为了方便,我们已经编写好了这么一套函数。代码如下: ```c #include #include #ifndef _DEBUG_INFO_HPP_ #define _DEBUG_INFO_HPP_ #ifdef _MBCS #define DebugInfo DebugInfoW #else #define DebugInfo DebugInfoA #endif // 函数: DebugInfoA(char*, int, ...) // // 目的: 以窄字符的形式输出调试信息 // // char* str - 格式化 ANSI 字符串 // ... - 任意不定长参数 // void DebugInfoA(char* str, ...) { char szBuffer[500]; //注意不要让缓冲区溢出! va_list Argv; va_start(Argv, str); _vsnprintf_s(szBuffer, 500, str, Argv); va_end(Argv); OutputDebugStringA(szBuffer); } // 函数: DebugInfoW(char*, int, ...) // // 目的: 以宽字符的形式输出调试信息 // // char* str - 格式化 UNICODE 字符串 // ... - 任意不定长参数 // void DebugInfoW(wchar_t* str, ...) { wchar_t szBuffer[1000]; va_list Argv; va_start(Argv, str); _vsnwprintf_s(szBuffer, 500, str, Argv); va_end(Argv); OutputDebugStringW(szBuffer); } #endif ``` 上面的这段代码会自动识别编译器是否默认使用了宽字符并且使用对应版本的输出函数,其中注释为 Visual Studio 的智能提示信息,我们把上面的代码保存到 debuginfo.hpp 并添加到当前工程中,就可以直接通过如下代码调用: ```c #include #include #ifndef _DEBUG_INFO_HPP_ #define _DEBUG_INFO_HPP_ #ifdef _MBCS #define DebugInfo DebugInfoW #else #define DebugInfo DebugInfoA #endif // 函数: DebugInfoA(char*, int, ...) // // 目的: 以窄字符的形式输出调试信息 // // char* str - 格式化 ANSI 字符串 // ... - 任意不定长参数 // void DebugInfoA(char* str, ...) { char szBuffer[500]; //注意不要让缓冲区溢出! va_list Argv; va_start(Argv, str); _vsnprintf_s(szBuffer, 500, 500, str, Argv); va_end(Argv); OutputDebugStringA(szBuffer); } // 函数: DebugInfoW(char*, int, ...) // // 目的: 以宽字符的形式输出调试信息 // // char* str - 格式化 UNICODE 字符串 // ... - 任意不定长参数 // void DebugInfoW(wchar_t* str, ...) { wchar_t szBuffer[1000]; va_list Argv; va_start(Argv, str); _vsnwprintf_s(szBuffer, 500, 500, str, Argv); va_end(Argv); OutputDebugStringW(szBuffer); } #endif ``` 我们可以看到,它正确地输出了 “用户输入的数是 666” ,这就说明我们的代码执行成功了,上面的代码大家可以随意修改我们上面提供的代码以符合自己的实际需要。但是请注意不要让缓冲区溢出,否则会造成不可估计的后果。 我们除了在调试器中可以看到调试字符串的输出,我们还可以借助 Sysinternals 软件公司研发的一个相当高级的工具 —— DebugView 调试信息捕捉工具,这个工具可以在随时随地捕捉 OutputDebugString 调试字符串的输出(包括发布模式构建的程序),可以说这是个神器,大家可以在微软 MSDN 库上搜索下载。接下来我们运行 DebugView 调试信息捕捉工具。 ## Debug环境和Release环境 首先我们再了解一下Visual Studio 中,Release构建模式和Debug 构建模式的区别。Release构建模式下构建的程序为发行版,而Debug构建模式下构建的程序为调试版。在 Visual Studio 中调试模式还会定义两个宏 _DEBUG 和 DEBUG,后文我们将介绍它们的一些妙用。在 Visual Studio 中,如果我们要更改编译参数的话,可以点击菜单 -> 项目(P) -> <项目名>属性(P),我们在弹出的页面左侧选择配置属性即可对编译参数进行修改。 接下来,我们来了解一下调试标记。不知道大家有没有遇到这样的情况,我们需要在调试的时候额外运行一段代码,但是实际发布的时候却不需要这段代码呢。那该怎么办,绝大多数数的初学者会选择使用注释,即在发布的时候将无用的测试代码注释掉。但是这样很麻烦,下面我们就为大家介绍一种全新的方法——使用调试标记。 这种方法借助了预处理指令,方法很简单,我们首先定义一个宏作为处于调试状态的标记,后面的代码我们用 #ifdef 和 #endif 预处理指令检测宏是否被定义,然后由编译器判断是否编译其中的代码。这么做的好处就是可以减少发布程序的体积,另一方面可以提高发布程序的运行效率。下面是一段示范代码: ```c #include #define _DEBUGNOW int main(){ #ifdef _DEBUGNOW printf("正在为调试做准备..."); #endif // _DEBUGNOW printf("程序正在运行..."); return 0; } ``` 那如果我们想让这个宏在我们发布程序的时候失效呢,我们该怎么做?其实很简单,我们依然可以使用预处理指令完成这项操作,下面我们来看一套完整的代码(同时使用调试标记和宏)。 ```c #include #include #define _PAUSE() system("pause"); #if (defined DEBUG) || (defined _DEBUG) //检测构建模式是否为调试模式 //如果构建模式为调试模式,这里定义几个宏 #define _DEBUGNOW #define _PUTSIL(NUM) printf("%d\n",NUM) //输出整数 #define _PUTFD(NUM) printf("%f\n",NUM) //输出浮点数 #else //如果构建模式为发布模式,自动忽略这些宏的存在 #define _PUTSIL(NUM) ((void)0) #define _PUTFD(NUM) ((void)0) #endif int main(){ #ifdef _DEBUGNOW printf("正在为调试做准备...\n"); #endif // _DEBUGNOW printf("程序正在运行...\n"); _PUTSIL(12666); _PUTFD(3.1415926535898); printf("程序运行完毕...\n"); _PAUSE(); // 暂停程序 return 0; } ``` 调试其实是一项比较复杂的活,需要大量的操作,所以在我们编写代码的时候要万分谨慎!因为很多时候,BUG都是因为我们的粗心大意导致的笔误引起的! ## VS调试常用快捷键 | 快捷键 | 说明 | | ------------ | -------------- | | F5 | 开始调试 | | shift+F5 | 结束调试 | | ctrl+F5 | 只执行不调试 | | F9 | 断点开关 | | F7 | 回到代码编辑器 | | ctrl+F7 | 编译 | | ctrl+shift+B | 生成解决方案 | | alt+shift+. | 选择相同的内容 |