C语言调试教程总结(以visual studio和Dev C++为例)

您所在的位置:网站首页 调试错误怎么解决问题视频 C语言调试教程总结(以visual studio和Dev C++为例)

C语言调试教程总结(以visual studio和Dev C++为例)

2024-06-04 12:40| 来源: 网络整理| 查看: 265

文章目录 一、前言二、调试的概念以及调试器的选择调试器(Debugger)1) Remote Debugger2) WinDbg3) LLDB4) GDB 三、设置断点,开始调试断点的真正含义继续执行程序删除断点代替暂停语句 四、查看和修改变量的值更加快捷的方式添加监视 五、单步调试(逐语句调试和逐过程调试)逐过程调试(F10)和逐语句调试(F11)修改代码运行位置 六、即时窗口的使用调用函数 七、查看、修改运行时的内存八、有条件断点的设置九、assert断言函数九、调试信息的输出十、VS调试的总结以及技巧十一、DEV C++调试教程调试步骤如下一、打开调试工具二、通过鼠标查看变量三、调试方法

一、前言

C语言常见错误有三种:

语法错误:指程序中含有不符合语法规定的语句逻辑错误:指程序中没有语法错误,可以通过编译、连接生成可执行程序,但程序运行的结果与预期不相符的错误。运行时错误:一种是由于考虑不周或输入错误导致程序异常(Exception),比如数组越界访问,除数为零,堆栈溢出等等。另一种是由于程序设计思路的错误导致程序异常或难以得到预期的效果。

对于语法错误,编译器可以帮助我们解决;对于逻辑错误和运行时错误,编译器可以通过,但是在实际使用过程中得不到预期的结果或者运行崩溃,我们就需要通过调试来解决。

二、调试的概念以及调试器的选择

所谓调试(Dubug),就是跟踪程序的运行过程,从而发现程序的逻辑错误(思路错误),或者隐藏的缺陷(Bug)。

在调试的过程中,我们可以监控程序的每一个细节,包括变量的值、函数的调用过程、内存中数据、线程的调度等,从而发现隐藏的错误或者低效的代码。

我敢保证,每个人都会遇到逻辑错误,而且会经常遇到,初学者更是错的离谱,所以,必须掌握调试技能,没有选择的余地,没有学会调试就是没有学会编程!

在编写代码的过程中,相信大家肯定遇到过这样的情况:代码能够编译通过,没有语法错误,但是运行结果却不对,反复检查了很多遍,依然不知道哪里出了问题。这个时候,就需要调试程序了。

所谓调试(Debug),就是让代码一步一步慢慢执行,跟踪程序的运行过程。比如,可以让程序停在某个地方,查看当前所有变量的值,或者内存中的数据;也可以让程序一次只执行一条或者几条语句,看看程序到底执行了哪些代码。

在调试的过程中,我们可以监控程序的每一个细节,包括变量的值、函数的调用过程、内存中数据、线程的调度等,从而发现隐藏的错误或者低效的代码。

编译器可以发现程序的语法错误,调试可以发现程序的逻辑错误。所谓逻辑错误,是指代码思路或者设计上的缺陷。

对于初学者来说,学习调试也可以增加编程的功力,它能让我们更加了解自己自己的程序,比如变量是什么时候赋值的、内存是什么时候分配的,从而弥补学习的纰漏。

调试是每个程序员必须掌握的技能,没有选择的余地!

调试器(Debugger)

调试需要借助专业的辅助软件——调试器(Debugger)。现在主流C/C++调试器有下面几种:

1) Remote Debugger

Remote Debugger 是 VC/VS 自带的调试器,与整个IDE无缝衔接,使用非常方便,初学者建议使用该调试器,本教程也以 VS2010 为例讲解调试技巧。

2) WinDbg

大名鼎鼎的 Windows 下的调试器,它的功能甚至超越了 Remote Debugger,它还有一个命令行版本(cdb.exe),但是这个命令行版本的调试器指令比较复杂,不建议初学者使用。

3) LLDB

XCode 自带的调试器,Mac OS X 下开发必备调试器。

4) GDB

Linux 下使用最多的一款调试器,也有 Windows 的移植版,如果你不使用 VC/VS,GDB 将是一个不错的选择。

考虑到大部分读者使用Windows,本教程以 VS2010 为例讲解调试技巧,也即使用 Remote Debugger 调试器。当然你也可以使用 VS2012、VS2013、VS2015 等,它们之间的差别很小。

三、设置断点,开始调试

默认情况下,程序不会进入调试模式,代码会瞬间从开头执行到末尾。要想观察程序的内部细节,就得让程序在某个地方停下来,我们可以在这个地方设置断点。

所谓断点(BreakPoint),可以理解为障碍物,人遇到障碍物不能行走,程序遇到断点就暂停执行。

img

上图中,我们希望让程序在第4行代码处暂停执行,那么在第4行代码左侧的灰色部分单击鼠标即可插入断点。你也可以将光标定位到要暂停的代码行,然后按F9键插入断点。也可以在要暂停的位置单击鼠标右键,在弹出菜单中插入断点,如下图所示:

img

插入断点后,点击上方的“运行”按钮,或者按F5键,即可进入调试模式,如下图所示:

img

可以看到,程序虽然运行了,但并未输出任何数据,这是因为在输出数据之前就暂停了。同时,在IDE的上方出现了与调试相关的工具条,下方也多出了几个与调试相关的窗口:

调用堆栈可以看到当前函数的调用关系。断点窗口可以看到当前设置的所有断点。即时窗口可以让我们临时运行一段代码,后续我们会重点讲解。输出窗口和我们之前看到的没有,用来显示程序的运行过程,给出错误信息和警告信息。自动窗口会显示当前代码行和上一代码行中所使用到的变量。局部变量窗口会显示当前函数中的所有局部变量。线程和模块窗口暂时无需理会。

如果你的VS缺少某个窗口,可以通过VS上方的“调试”菜单调出,如下图所示:

img

注意:必须在调试状态下才能看到图中的菜单。

如果你希望关闭某个窗口,可以在窗口标题处单击鼠标右键,在弹出菜单中隐藏,如下图所示:

img

断点的真正含义

严格来说,调试器遇到断点时会把程序暂时挂起,让程序进入一种特殊的状态——中断状态,这种状态下操作系统不会终止程序的执行,也不会清除与程序相关的元素,比如变量、函数等,它们在内存中的位置不会发生变化。

关键是,处于中断状态下的程序允许用户查看和修改它的运行状态,比如查看和修改变量的值、查看和修改内存中的数据、查看函数调用关系等,这就是调试的奥秘。

继续执行程序

点击“运行”按钮或者按F5键即可跳过断点,让程序恢复正常状态,继续执行后面的代码,直到程序结束或者遇到下一个断点。

在调试过程中,按照上面的方法可以设置多个断点,程序在执行过程中每次遇到断点都会暂停,如下图所示:

img

删除断点

如果不希望程序暂停,可以删除断点。删除断点也很简单,在原有断点处再次单击鼠标即可,也可以将光标定位到要删除断点的代码行,再次按F9键,或者在右键菜单中删除,如下图所示

img

代替暂停语句

在VS下,程序运行结束后不会自动暂停(一闪而退),要手动添加暂停语句system("pause");,如果大家觉得麻烦,也可以在代码最后插入断点,强制程序暂停。

四、查看和修改变量的值

设置了断点,就可以观察程序的运行情况了,其中很重要的一点就是查看相关变量的值,这足以发现大部分逻辑错误。

将下面的代码复制到源文件中:

#include int main(){ int value_int, array_int[3]; float value_float; char* value_char_pointer; //在这里插入断点 value_int = 1048576; value_float = 2.0; value_char_pointer = "Hello World"; array_int[0] = 379; array_int[1] = 94; //在这里插入断点 return 0; }

在第7行和第12行插入断点。运行到第一个断点时,在局部变量窗口可以看到各个变量的值:

img

可以看到,未经初始化的局部变量和数组的值都是垃圾值,是随机的,没有意义。双击变量的值,可以进行修改。

点击“运行”按钮或按F5键,程序会运行到下一个断点位置,在局部变量窗口可以看到各个值的变化:

img

更加快捷的方式

除了在窗口中查看变量,还有一种更加便捷的方法:在调试模式下,把鼠标移动到要查看的变量的上方,即可看他它的值。如下图所示:

img

如果是数组、指针、结构体等还可以展开,如下图所示:

img

这种查看变量的方式在实际开发中使用很多。

添加监视

如果你希望长时间观测某个变量,还可以将该变量添加到监视窗口。在要监视的变量处单击鼠标右键,弹出如下菜单:

img

选择“添加监视”,在VS下方的监视窗口就可以看到当前变量:

img

这样,每次变量的值被改变都会反映到该窗口中,无需再将鼠标移动到变量上方查看其值。尤其是当程序稍大时,往往需要同时观测多个变量的值,添加监视的方式就会显得非常方便。

五、单步调试(逐语句调试和逐过程调试)

在实际开发中,常常会出现这样的情况,我们可以大致把出现问题的代码锁定在一定范围内,但无法确定到底是哪条语句出现了问题,该怎么办呢?按照前面的思路,必须要在所有代码行前面设置断点,让代码一个断点一个断点地执行。

这种方案确实可行,但很麻烦,也不专业,这节我们就来介绍一种更加便捷的调试技巧——单步调试。所谓单步调试,就是让代码一步一步地执行。

下面的代码用来求一个等差数列的和,我们以该代码来演示单步调试:

#include int main(){ int start, space, length, i, thisNum; long total = 0; printf("请输入等差数列的首项值:"); scanf("%d", &start); printf("请输入等差数列的公差:"); scanf("%d", &space); printf("请输入等差数列的项数:"); scanf("%d", &length); for(i=0; i printf("%d + ", thisNum); }else{ printf("%d", thisNum); } total += thisNum; } printf(" = %ld\n", total); return 0; }

在第6行设置一个断点,然后点击“逐过程调试”按钮,或者按F10键,程序就会运行到下一行并暂停:

img

再次点击“逐过程”按钮或按F11键,就会执行第7行代码,要求用户输入数据。用户输入结束后,黄色箭头就会指向第8行,并暂停程序。如此重复执行上面的操作,就可以让程序一条语句一条语句地执行,以观察程序的内部细节,这就称为单步调试。

逐过程调试(F10)和逐语句调试(F11)

刚才我们在第6行设置了断点,按下“逐过程”按钮或F10键,程序就会执行 printf(),并暂停在下一行代码处。

printf() 是一个函数,它本身由多条语句构成,如果你希望进入 printf() 函数内部,查看整个函数的执行过程,可以点击“逐语句”按钮,或者按F11键,这样程序就会进入 printf() 所在的源文件,如下图所示:

img

当需要跳出函数时,可以点击“跳出”按钮,或者按Shift+F11键,就会返回刚才的代码。

逐过程(F10)和逐语句(F11)都可以用来进行单步调试,但是它们有所区别:

逐过程(F10)在遇到函数时,会把函数从整体上看做一条语句,不会进入函数内部;逐语句(F11)在遇到函数时,认为函数由多条语句构成,会进入函数内部。

逐语句(F10)不仅可以进入库函数的内部,还可以进入自定义函数内部。在实际的调试过程中,两者结合可以发挥更大的威力。

断点 + 查看/修改变量 + 逐过程调试 + 逐语句调试,这足以解决绝大多数逻辑问题,到此,初学者就已经学到了调试的基本技能。

修改代码运行位置

在VS中,调试器还允许我们直接跳过一段代码,不去执行它们。将下面的代码复制到源文件:

#include int main(){ printf("111\n"); printf("222\n"); printf("333\n"); printf("444\n"); printf("555\n"); printf("666\n"); return 0; }

在第3行设置断点,开始单步调试。假设我们不希望执行4~6行的代码,那么当程序执行到第4行时,可以将鼠标移动到黄色箭头处,直接向下拖动到第7行,如下图所示:

img

程序执行完成后,在控制台上会输出: 111 555 666

注意:随意修改程序运行位置是非常危险的行为,假设我们定义了一个指针,在第N行代码中让它指向了一个数组,如果我们在修改程序运行位置的时候跳过了第N行代码,并且后面也使用到了该指针,那么就极有可能导致程序崩溃。

六、即时窗口的使用

“即时窗口”是VS提供的一项非常强大的功能,在调试模式下,我们可以在即时窗口中输入C语言代码并立即运行,如下图所示:

img

在即时窗口中可以使用代码中的变量,可以输出变量或表达式的值(无需使用printf()函数),也可以修改变量的值。

即时窗口本质上是一个命令解释器,它负责解释我们输入的代码,再由VS中的对应模块执行,最后将输出结果呈现到即时窗口。

需要注意的是,在即时窗口中不能定义新的变量,因为程序运行时 Windows 已经为它分配好了只够刚好使用的内存,定义变量是需要额外分配内存的,所以调试器不允许在程序运行的过程中定义变量,因为这可能会导致不可预知的后果。

调用函数

在即时窗口中除了可以使用代码中的变量,也可以调用代码中的函数。将下面的代码复制到源文件中:

int plus(int x, int y){ return x + y; } int main(){ return 0; }

在第6行设置断点,并在即时窗口中输入plus(5, 6),如下图所示:

img

七、查看、修改运行时的内存

在 Visual Studio 的调试过程中,有时候我们可能需要对程序的运行内存进行查看,修改该怎么办?Visual Studio 也为我们提供了很好的解决方案。那就是使用 Visual Studio 自带的内存查看窗口。

首先我们通过内存窗口查看变量的值,我们启动 Visual Studio,创建一个工程,输入如下代码:

#include int main() { int testNumber = 5678; printf("testNumber 的内存地址为 0x00%x \n", &testNumber); //输出内存地址 //TODO:在这里插入断点 return 0; }

我们在第七行设置好断点,然后按 F5 启动调试,等待触发断点。触发断点后,我们发现,IDE中并没有显示内存窗口(默认设置下),这时,我们点击菜单 -> 调试(D) -> 窗口 (W) -> 内存 (M) -> 内存1(1),就可以调出内存窗口了,如图:

img

我们看到,内存窗口里面显示着一大堆乱七八糟的数据,这里面的这些就是我们内存中的数据啦,我们可以通过变量 testNumber 的内存地址跳转到器对应的内存空间,我们看到 testNumber 的地址为 0x0018f830 (在不同的进程是不一样的),我们把这个地址输入到我们的内存窗口的地址栏。如图:

img

我们看到,尽管我们已经输入了正确地地址,但是我们还是没有看到正确的数据显示,其实原因非常简单,我们来回顾一下 C 语言的一些入门知识:我们知道,在我们的源代码中,我们的 testNumber 变量被定义为 int 整形,我们再想想 int 整形在内存中占几个字节?没错,是4个字节。所以我们应该以四字节的形式格式化这些内存数据,这是我们在内存窗口中单击我们的鼠标右键,在弹出的菜单中选择“4字节整数(4)”,然后就能正确地显示相关的数据了,如图:

img

没错,查看内存就是这么的简单。接下来我们就来查看与修改浮点数的内存数据,我们看下面这段代码:

#include int main() { double pi = 3.141592653589; printf("pi 的内存地址为 %x \n", &pi); //输出内存地址 //TODO:在这里插入断点 return 0; }

同样的,我们在第7行设置断点,按F5启动调试,等待断点被触发:

img

这时我们看到的内存地址是这样的,与我们在内存窗口看到的不同,我们需要将其补齐,在我们现阶段编写的小程序中,显示的内存地址基本上都是六位的,我们在前面加上 “0x00”,将其补到八位(内存窗口上的地址栏里有几位就补到几位)。然后我们将其输入到内存窗口上的地址栏。

img

我们发现,现在显示的数据依然是错误的,因为查看器现在还是在使用我们之前设置的 4位整形格式 格式化我们的内存数据呢,我们知道,我们的 double 属于64位浮点数,所以我们在查看窗口点击鼠标右键,在弹出的菜单中选择“64位浮点(6)”,然后我们就能看到它正确地输出数据了。

img

我们注意到,在我们设置的变量pi的值的基础上,内存窗口里显示的数据多了几个0,这些0所代表的就是 double 型数据的最高精度。接下来我们尝试在内存窗口修改变量 pi 的值,为其补齐精度。现在我们用鼠标右键点击我们的pi的内存数据,在弹出的菜单中选择编辑值(E),在显示的输入框中我们输入 3.1415926535897931,按回车即可保存。我们看看效果:

img

怎么样,内存的查看与修改是不是很简单呢?其实我们只要记住下面的几个对应关系,常用的数值数据类型的内存查看与修改都不在话下:

类型名变量类型内存查看窗口中应选择的数据格式short16 位整形2 字节整数int32 位整形4 字节整数long32 位整形4 字节整数long long64 位整形8 字节整数float32 位(4字节)单精度浮点数32 位浮点double64 位(8字节)双精度浮点数64 位浮点

内存新手常用的内存窗口操作就到这里了,其实关于内存操作还有很多高级用法,不过那不在我们初级调试教程的范围之内。我们在修改内存的时候要注意安全,防止随意修改导致的程序崩溃,甚至是无法结束进程!

八、有条件断点的设置

在此之前,我们已经了解了无条件断点、跟踪点这两种断点,事实上在 Visual Studio 中还有几种常用的断点,在本节我们将一一为大家介绍。

大家有没有碰到这样的情况,在一个循环体中设置断点,假设有一千次循环,我们想在第五百次循环中设置断点,这该怎么办?反正设置断点不断按 F5 继续运行四百九十九次是不可能的。那该怎么办呢?其实我们的断点是可以设置各种各样的条件的,对于上述情况,我们可以对断点的命中次数做一个限制。

我们首先在 Visual Studio 中创建一个工程,并且输入如下代码:

#include int main(){ for ( int i=1 ; i int a, b; int randNumber; srand((unsigned)time(NULL)); //设置随机数种子,以产生不同随机数 for (int i = 0; i printf("assert 函数测试:"); assert(true); //表达式为真 assert(1 >= 2); //表达式为假 return 0; }

我们按 F5 启动调试:

img

我们看到,我们的输出窗口打印出了断言失败的信息,并且 Visual Studio 弹出了一个对话框询问我们是否继续执行。但是如果我们不绑定调试器,构建发布版程序,按 Ctrl + F5 直接运行呢?是的,这个 assert 语句就无效了,原因其实很简单,我们看看 assert.h 头文件中的这部分代码:

#ifdef NDEBUG #define assert(_Expression) ((void)0) #else /* NDEBUG */

我们看到,只要我们定义了 NDEBUG 宏,assert 就会失效,而 Visual Studio 的默认的发布版程序编译参数中定义了 NDEBUG 宏,所以我们不用额外定义,但是在其他编译器中,我们在发布程序的时候就必须在包含 assert.h 头文件前定义 NDEBUG 宏,避免 assert 生效,否则总是让用户看到“程序已经停止运行,正在寻找解决方案 . . .”的 Windows 系统对话框可就不妙了。

下面我们来了解一下 assert 的常用情境以及一些注意事项。举个C语言文件操作的例子:

#include #include #include int main(void) { FILE *fpoint; fpoint = fopen("存在的文件.txt", "w"); //以可读写的方式打开一个文件 //如果文件不存在就自动创建一个同名文件 assert(fpoint); //(第一次断言)所以这里断言成功 fclose(fpoint); fpoint = fopen("不存在的文件.txt", "r"); //以只读的方式打开一个文件,如果不存在就打开文件失败 assert(fpoint); //(第二次断言)所以断言失败 printf("文件打开成功"); //程序永远都执行不到这里来 fclose(fpoint); return 0; }

阅读代码,我们可以知道,如果我们错误的设置了读写权限,或者没有考虑 Windows 7(或更高版本) 的 UAC(用户账户权限控制) 问题的话,我们的程序出现的问题就很容易在我们的测试中暴露出来。事实上,上面的代码也不是一定能通过第一次断言的,我们把构建生成的(Debug 模式的)可执行文件复制到我们的系统盘的 Program Files 文件夹再执行,那么 Win7 及以上的操作系统的UAC都会导致程序无法通过第一次断言的。所以在这种情况下我们就需要在必要的情况申请管理员权限避免程序BUG的发生。

在我们的实际使用过程中,我们需要注意一些使用 assert 的问题。首先我们看看下面的这个断言语句:

//... assert( c1 /*条件1*/ && c2 /*条件2*/ ); //...

我们思考一下:如果我们的程序在这里断言失败了,我们如何知道是 c1 断言失败还是 c2 断言失败呢,答案是:没有办法。在这里我们应该遵循使用 assert 函数的第一个原则:每次断言只能检验一个条件,所以上面的代码应该改成这样:

//... assert(c1 /*条件1*/); assert(c2 /*条件2*/); //...

这样,一旦出现问题,我们就可以通过行号知道是哪个条件断言失败了。

接下来我们来思考一个问题:我们可以使用 assert 语句代替我们的条件过滤语句么?我们来举个例子,我们写了一个视频格式转换器,我们本应输出标准MP4格式的视频,但是由于编码解码库的问题,我们在转换过程中出现了错误,像这种情况下,我们能否用 assert 代替掉我们的 if 语句呢(假设这里忽略了 Visual Studio 发布版程序编译过程中自动定义的 NDEBUG 宏)?很明显,这样做是不可以的。这是为什么呢?

现在我们来区分一下程序中的错误与异常,所谓错误,是代码编写途中的缺陷导致的,是程序运行中可以用 if 语句检测出来的。而异常在我们的 C 语言中,一般也可以使用 if 语句判断,并进行对应的处理。而 assert 是用来判断我们程序中的代码级别的错误的。像用户输入错误,调用其他函数库错误,这些问题我们都是可以用 if 语句检测处理的。另一方面,使用 if 语句的程序对用户更友好。

下面我们通过代码展示使用 assert 的另外一个注意意事项,我们新建一个工程,输入如下代码:

#include #include int main(void) { int i = 0; for ( ; ; ) { assert(i++ //注意!这段代码我们指定使用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; }

我们按 F5 开始调试:

img

我们看到了输出的结果,怎么样,大家是不是觉得这样调用这个函数很麻烦?为解决此问题,这里C语言中文网为大家提供了一个更好的解决方案,我们可以自己写一个前端函数,然后保存到头文件中(编译生成 dll 也可以,有兴趣的同学可以试试)。为了方便,我们已经编写好了这么一套函数。代码如下:

#include #include #ifndef _DEBUG_INFO_HEADER_ //防止头文件被重复载入出错 #define _DEBUG_INFO_HEADER_ #if (defined UNICODE)||(defined _UNICODE) #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.h 并添加到当前工程中,就可以直接通过如下代码调用:

#include #include #include #include "debuginfo.h" int main(){ int num; //这里我们使用微软提供的 xxxx_s 安全函数 printf_s("请输入数值:\n"); scanf_s("%d", &num); DebugInfo(TEXT("用户输入的数是 %d\n"), num); return 0; }

我们按 F5 开始调试,我们输入666,观察 IDE 的输出窗口:

img

我们可以看到,它正确地输出了 “用户输入的数是 666” ,这就说明我们的代码执行成功了,上面的代码大家可以随意修改我们上面提供的代码以符合自己的实际需要。但是请注意不要让缓冲区溢出,否则会造成不可估计的后果。

我们除了在调试器中可以看到调试字符串的输出,我们还可以借助 Sysinternals 软件公司研发的一个相当高级的工具 —— DebugView 调试信息捕捉工具,这个工具可以在随时随地捕捉 OutputDebugString 调试字符串的输出(包括发布模式构建的程序),可以说这是个神器,大家可以在微软 MSDN 库上搜索下载。接下来我们运行 DebugView 调试信息捕捉工具。

首先我们打开 DebugView:

img

我们可以看到随时有程序在输出调试信息,接下来我们运行(按Ctrl+F5 运行,不要启动调试器)刚才的程序,输入 222,看看 DebugView 有什么反应:

img

这个调试信息查看工具显示了正确的调试信息。注意!如果挂载了 Visual Studio 的调试器,这个工具就会失效。

使用这个工具可以捕获发布版程序的输出,我们一般可以用来跟踪测试产品的运行状态。以趁早发现我们程序中存在的问题,避免发布后出现的容易被检测到的BUG。

十、VS调试的总结以及技巧

我们已经对 Visual Studio 的调试有了一些了解,在这一节,我们将向大家展示我们在调试过程中可以使用的一些实用技巧。使大家的调试操作更加轻松愉快。

首先我们再了解一下Visual Studio 中,Release构建模式和Debug 构建模式的区别。我们在img可以切换构建模式。Release构建模式下构建的程序为发行版,而Debug构建模式下构建的程序为调试版。在 Visual Studio 中调试模式还会定义两个宏 _DEBUG 和 DEBUG,后文我们将介绍它们的一些妙用。在 Visual Studio 中,如果我们要更改编译参数的话,可以点击菜单 -> 项目§ -> 属性§,我们在弹出的页面左侧选择配置属性即可对编译参数进行修改。

接下来,我们来了解一下调试标记。不知道大家有没有遇到这样的情况,我们需要在调试的时候额外运行一段代码,但是实际发布的时候却不需要这段代码呢。那该怎么办,绝大多数数的初学者会选择使用注释,即在发布的时候将无用的测试代码注释掉。但是这样很麻烦,下面我们就为大家介绍一种全新的方法——使用调试标记。事实上这种方法我们在前面使用过,但是没有详细讲解。

这种方法借助了预处理指令,方法很简单,我们首先定义一个宏作为处于调试状态的标记,后面的代码我们用 #ifdef 和 #endif 预处理指令检测宏是否被定义,然后由编译器判断是否编译其中的代码。这么做的好处就是可以减少发布程序的体积,另一方面可以提高发布程序的运行效率。下面是一段示范代码:

#include #define _DEBUGNOW int main(){ #ifdef _DEBUGNOW printf("正在为调试做准备..."); #endif // _DEBUGNOW printf("程序正在运行..."); return 0; }

当我们要发布上面的这个程序的时候,我们只要将 #define _DEBUGNOW 注释掉即可,无需进行任何额外的操作。怎么样?是不是很方便呢?善用调试标记可以大大地提高我们的调试效率,但是有一点记住,调试标记名不要过于简单,否则可能和程序中的变量/常量产生冲突。

在我们的调试过程中,我们常常会需要在不触发断点的情况下输出一些数值,这时我们一般会这么做:

printf("%d\n",Value/*要输出的数值*/);

但是像这种代码写多了我们可能会对它产生一种厌恶之情,这是我们的预处理器又可以派上用场了,我们可以定义一个宏解决这个问题,定义宏的代码如下:

#define _PUTINT(NUM) printf("%d\n",NUM)

然后我们只要这样调用我们的宏:

_PUTINT(45/*要输出的数值*/);

怎么样?是不是很方便呢?那如果我们想让这个宏在我们发布程序的时候失效呢,我们该怎么做?其实很简单,我们依然可以使用预处理指令完成这项操作,下面我们来看一套完整的代码(同时使用调试标记和宏):

#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; }

我们看到,宏的定义和函数差不多,有的简单的函数可以直接用宏实现,这样做带来的好处就是,我们可以不用频繁的判断是否处于调试状态,一次定义,一直有效。

为了方便我们的调试(检查)操作以及日后的团队合作,我们在编写函数的时候应该为其加上 Visual Studio 的智能提示,方法比较简单,我们只要在函数定义前面加上提示注释即可(格式自由),Visual Studio 便会自动分析我们的代码并加入其智能提示列表,下面我们举一个定义函数设置智能提示的例子:

// // 函数: Convert2Jpeg(wchar_t*, wchar_t, int) // // 目的: 转换图片到 Jpeg 格式 // // orgiPath : 源文件路径 // destPath : 目标路径 // quality : 图像质量 // bool Convert2Jpeg(wchar_t* orgiPath, wchar_t* destPath, int quality){ return true; }

我们在自动完成将或者鼠标移动到编辑器内函数名上,就有了智能提示。 img

这种智能提示不但可以减轻我们编写代码时的负担,在调试情境下也可以增加我们检查代码的效率。如此一来,岂不美哉!

接下来我们为大家总结了一些调试的经验以及调试思路: 当我们运行我们编写的程序发现运行结果与我们预想的不同的时候,我们可以先用即时窗口,使用一些比较简单的数据来测试我们的各个函数运行结果是否符合我们的预期,如果都符合的话,我们可以使用程序中产生的一些比较复杂的数据来进一步测试我们的各个函数,直至找到可能导致错误的函数。

找到可能导致错误的函数之后,我们就可以使用逐语句调试来一步步跟踪运行程序了,渐渐的的我们就可以缩小范围直至定位错误(无关代码可以考虑暂时注释掉),在这期间,我们要仔细观察程序运行过程中各个数据的变化情况,观察的仔细与否直接与我们能否找到错误直接挂钩。

如果上一步运行的数据一直是正常的,我们就可以排除这个函数的嫌疑了(减少对他的调试次数)。此时,我们就应该考虑问题是否出现在之前的函数上了,可能因为偶然性,我们第一次测试函数的时候并没有发现其错误,导致范围锁定产生偏差,此时我们需要再次耐心的对所有未排除嫌疑的进行调试,直至再次找到出错的函数。再重复上一步,直至找到错误。

可以看到,调试其实是一项比较复杂的活,需要大量的操作,所以在我们编写代码的时候要万分谨慎!因为很多时候,BUG都是因为我们的粗心大意导致的笔误引起的!

十一、DEV C++调试教程

上面的教程是使用VS来演示的,下面我们也简单介绍一下使用DEV C++来进行调试的过程。

调试步骤如下 一、打开调试工具

1、点击菜单栏中的工具[T]

img

2、选择编译选项[C]

img

3、更改调试信息 点击【代码生成/优化】,将【连接器】中【产生调试信息】一项的No改为Yes,然后点击下方的确定即可。

img

二、通过鼠标查看变量

1、点击菜单栏中的工具[T],选择环境选项[v]

img

2、更改信息 找到【浏览Debug变量】,选中【查看鼠标指向的变量】,然后点击下方的确定即可。

img

使用方法:当开始调试以后,把鼠标划过代码中想查看的变量就可以在左边调试一栏中查询该变量的当前值。

img

三、调试方法

1、打开一个.c或.cpp文件,编译它,一方面,要确保这个代码能够编译通过,另一方面,在每次调试前都是需要先编译的。

2、设置“断点” 在要设置断点的那行代码开头处的数字上单击一下即可,可以设置多个断点,也可以只设置一个断点。如果设置了多个断点,程序会在断点与断点之间进行调试。如果只有一个断点,程序会从设置断点处开始,随着每次点击,一步一步进行下去,直到程序结束。

img

3、点击“调试”,开始进入调试 点击菜单栏中的【√】或者下方的【调试】都可以。

img

4、如果程序需要输入,那么点击进入后,就会跳出黑框。输入数据,按“回车”。蓝色一行表示当前程序运行的位置。

img

5、调试开始后,可以点击调试一栏中的‘下一步’,让程序运行到想要的位置。

ref:http://c.biancheng.net/view/vip_2085.html https://www.jianshu.com/p/1602264dadf2



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3