杜苹,2017斯诺克世锦赛赛程,mba学费贷款
1.调试的概念以及调试器的选择
在编写代码的过程中,相信大家肯定遇到过这样的情况:代码能够编译通过,没有语法错误,但是运行结果却不对,反复检查了很多遍,依然不知道哪里出了问题。这个时候,就需要调试程序了。
所谓调试(debug),就是让代码一步一步慢慢执行,跟踪程序的运行过程。比如,可以让程序停在某个地方,查看当前所有变量的值,或者内存中的数据;也可以让程序一次只执行一条或者几条语句,看看程序到底执行了哪些代码。
在调试的过程中,我们可以监控程序的每一个细节,包括变量的值、函数的调用过程、内存中数据、线程的调度等,从而发现隐藏的错误或者低效的代码。
编译器可以发现程序的语法错误,调试可以发现程序的逻辑错误。所谓逻辑错误,是指代码思路或者设计上的缺陷。
对于初学者来说,学习调试也可以增加的功力,它能让我们更加了解自己自己的程序,比如变量是什么时候赋值的、内存是什么时候分配的,从而弥补学习的纰漏。
调试是每个程序员必须掌握的技能,没有选择的余地!
2.设置断点,开始调试
默认情况下,程序不会进入调试模式,代码会瞬间从开头执行到末尾。要想观察程序的内部细节,就得让程序在某个地方停下来,我们可以在这个地方设置断点。
所谓断点(breakpoint),可以理解为障碍物,人遇到障碍物不能行走,程序遇到断点就暂停执行。
f9
键插入断点。也可以在要暂停的位置单击鼠标右键,在弹出菜单中插入断点,如下图所示:
f5
键,即可进入调试模式,如下图所示:
调用堆栈
可以看到当前函数的调用关系。断点
窗口可以看到当前设置的所有断点。即时窗口
可以让我们临时运行一段代码,后续我们会重点讲解。输出
窗口和我们之前看到的没有,用来显示程序的运行过程,给出错误信息和警告信息。自动窗口
会显示当前代码行和上一代码行中所使用到的变量。局部变量
窗口会显示当前函数中的所有局部变量。线程
和模块
窗口暂时无需理会。
注意:必须在调试状态下才能看到图中的菜单。如果你希望关闭某个窗口,可以在窗口标题处单击鼠标右键,在弹出菜单中隐藏,如下图所示:
f5
键即可跳过断点,让程序恢复正常状态,继续执行后面的代码,直到程序结束或者遇到下一个断点。
f9
键,或者在右键菜单中删除,如下图所示
system("pause");
,如果大家觉得麻烦,也可以在代码最后插入断点,强制程序暂停。
3.查看和修改变量的值
设置了断点,就可以观察程序的运行情况了,其中很重要的一点就是查看相关变量的值,这足以发现大部分逻辑错误。
将下面的代码复制到源文件中:
#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行插入断点。运行到第一个断点时,在局部变量
窗口可以看到各个变量的值:
f5
键,程序会运行到下一个断点位置,在局部变量
窗口可以看到各个值的变化:
这种查看变量的方式在实际开发中使用很多。
监视
窗口就可以看到当前变量:
4.单步调试(逐语句调试和逐过程调试)
在实际开发中,常常会出现这样的情况,我们可以大致把出现问题的代码锁定在一定范围内,但无法确定到底是哪条语句出现了问题,该怎么办呢?按照前面的思路,必须要在所有代码行前面设置断点,让代码一个断点一个断点地执行。
这种方案确实可行,但很麻烦,也不专业,这节我们就来介绍一种更加便捷的调试技巧——单步调试。所谓单步调试,就是让代码一步一步地执行。
下面的代码用来求一个等差数列的和,我们以该代码来演示单步调试:
#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 thisnum = start + space * i; if( length-i > 1 ){ printf("%d + ", thisnum); }else{ printf("%d", thisnum); } total += thisnum; } printf(" = %ld\n", total); return 0;}在第6行设置一个断点,然后点击“逐过程调试”按钮,或者按
f10
键,程序就会运行到下一行并暂停:
f11
键,就会执行第7行代码,要求用户输入数据。用户输入结束后,黄色箭头就会指向第8行,并暂停程序。如此重复执行上面的操作,就可以让程序一条语句一条语句地执行,以观察程序的内部细节,这就称为单步调试。
f10
键,程序就会执行
printf(),并暂停在下一行代码处。f11
键,这样程序就会进入
printf() 所在的源文件,如下图所示:
shift+f11
键,就会返回刚才的代码。逐过程(f10)
和逐语句(f11)
都可以用来进行单步调试,但是它们有所区别:
逐过程(f10)
在遇到函数时,会把函数从整体上看做一条语句,不会进入函数内部;逐语句(f11)
在遇到函数时,认为函数由多条语句构成,会进入函数内部。
逐语句(f10)
不仅可以进入库函数的内部,还可以进入自定义函数内部。在实际的调试过程中,两者结合可以发挥更大的威力。#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行,如下图所示:
程序执行完成后,在控制台上会输出:
5.即时窗口的使用
“即时窗口”是vs提供的一项非常强大的功能,在调试模式下,我们可以在即时窗口中输入c语言代码并立即运行,如下图所示:
int plus(int x, int y){ return x + y;}int main(){ return 0;}
在第6行设置断点,并在即时窗口中输入plus(5,
6)
,如下图所示:
6.查看、修改运行时的内存
在 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),就可以调出内存窗口了,如图:
#include int main(){ double pi = 3.141592653589; printf("pi 的内存地址为 %x \n", &pi); //输出内存地址 //todo:在这里插入断点 return 0;}
同样的,我们在第7行设置断点,按f5启动调试,等待断点被触发:
类型名 | 变量类型 | 内存查看窗口中应选择的数据格式 |
---|---|---|
short | 16位整形 | 2字节整数 |
int | 32位整形 | 4字节整数 |
long | 32位整形 | 4字节整数 |
longlong | 64位整形 | 8字节整数 |
float | 32位(4字节)单精度浮点数 | 32位浮点 |
double | 64位(8字节)双精度浮点数 | 64位浮点 |
7.有条件断点的设置
在此之前,我们已经了解了无条件断点、跟踪点这两种断点,事实上在
visual studio 中还有几种常用的断点,在本节我们将一一为大家介绍。
大家有没有碰到这样的情况,在一个循环体中设置断点,假设有一千次循环,我们想在第五百次循环中设置断点,这该怎么办?反正设置断点不断按 f5 继续运行四百九十九次是不可能的。那该怎么办呢?其实我们的断点是可以设置各种各样的条件的,对于上述情况,我们可以对断点的命中次数做一个限制。
我们首先在 visual studio 中创建一个工程,并且输入如下代码:
#include int main(){ for ( int i=1 ; i //todo:插入计次断点 printf("我真行!\n"); }}首先,我们在第4行插入断点,分析代码,我们不难得出它会输出 1000 行“我真行!”,那么我们思考一下,在不修改代码的情况下,如何才能让他输出 1499 行“我真行!”呢,其实很简单,我们只要在i 等于500的时候暂停程序,再将变量 i 的值修改为 1 即可,思路很简单,接下来我们就来实现这个命中条件的限制吧。
#include #include #include //time函数所在头文件int main(){ int a, b; int randnumber; srand((unsigned)time(null)); //设置随机数种子,以产生不同随机数 for (int i = 0; i { a = rand() % 7; //产生0-6的随机数 b = rand() % 7; //产生0-6的随机数 //todo:在这里插入条件断点: a == b } return 0;}
我们让程序运行过程中 a 等于 b 的时候触发断点,首先,我们在第十四行插入断点,然后我们鼠标右键单击左侧的断点图标,在弹出的菜单中选择条件(c),ide会弹出如下对话框,我们在条件输入框中输入 a==b ,然后在下面选择 为 true ,然后点击确定即可。
8.assert断言函数
在我们的实际开发过程之中,常常会出现一些隐藏得很深的bug,或者是一些概率性发生的bug,通常这些bug在我们调试的过程中不会出现很明显的问题,但是如果我们将其发布,在用户的各种运行环境下,这些程序可能就会露出马脚了。那么,如何让我们的程序更明显的暴露出问题呢?这种情况下,我们一般都会使用
assert 断言函数,这是c语言标准库提供的一个函数,也就是说,它的使用与操作系统平台,调试器种类无关。我们只要学会了它的使用,便可一次使用,处处开花。
接下来我们来了解一下 assert 函数的用法,这个函数在 assert.h 头文件中被定义,在微软的 cl 编译器中它的原型是这样的:
#define assert(_expression) (void)( (!!(_expression)) || (_wassert(_crt_wide(#_expression), _crt_wide(__file__), __line__), 0) )
我们看到 assert 在 cl 编译器中被包装成了一个宏,实际调用的函数是 _wassert ,不过在一些编译器中,assert 就是一个函数。为了避免这些编译器差异带来的麻烦,我们就统一将 assert 当成一个函数使用。#include #include int main(){ printf("assert 函数测试:"); assert(true); //表达式为真 assert(1 >= 2); //表达式为假 return 0;}
我们按 f5 启动调试:
#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*/); //...这样,一旦出现问题,我们就可以通过行号知道是哪个条件断言失败了。
#include #include int main(void){ int i = 0; for ( ; ; ) { assert(i++ printf("我是第%d行\n",i); } return 0;}
我们按 f5 运行调试器,我们会看到这样的情景:
assert(i++ <=>=>
我们的条件表达式为 i++ <= 100,这个表达式会更改我们的运行环境(变量i的值),在发布版程序中,所有的="" assert="" 语句都会失效,那么这条语句也就被忽略了,但是我们可以把它改为="" i++="" ;="" assert(i="">=><= 100);="">=>
9.调试信息输出
上一节,我们讲解了
assert 断言函数的使用,本节我们来学习在调试器的调试窗口上输出我们自己的调试信息,在这里,我们将用到一个 windows 操作系统提供的函数 —— outputdebugstring,这个函数非常常用,他可以向调试输出窗口输出信息(无需设置断点,执行就会输出调试信息),并且一般只在绑定了调试器的情况下才会生效,否则会被 windows 直接忽略。接下来我们了解一下这个函数的使用方法。
首先,这个函数在 windows.h 中被定义,所以我们需要包含 windows.h 这个头文件才能使用 outputdebugstring 函数。这个函数的使用方式非常的简单,它只有简单的一个参数——我们要输出的调试信息。但是有一点值得注意:准确来说
outputdebugstring 并不是一个函数,他是一个宏。在高版本的 visual studio 中,因为编译的时候 visual studio 默认定义了 unicode 宏,所以我们查找 outputdebugstring 的定义会看到如下代码:
#ifdef unicode#define outputdebugstring outputdebugstringw#else#define outputdebugstring outputdebugstringa#endif // !unicode
我们可以从代码高亮上看到,outputdebugstring 实际上等价于 outputdebugstringw,这就意味着我们必须传入宽字符串(事实上只要定义了 unicode ,调用所有 windows 提供的函数都需要使用宽字符),或者使用 text 或 _t 宏,并且这是最好的方法,这个宏会自动识别编译器是否处于默认宽字符的状态并对传入字符串做些处理,使用这个宏可以加强代码对不同编译器不同编译参数的兼容性。下面我们就来看一段示例代码:
#include //使用 outputdebugstring 包含此文件#include //使用 text 宏需要包含此文件int main(){ outputdebugstring(text("你好,c语言中文网。")); outputdebugstring(_t("大家好才是真的好。")); //也可以:outputdebugstringa("大家好才是真的好。"); //也可以:outputdebugstringw(l"大家好才是真的好。"); //使用自动字符宏 text 或者 _t 可以自动判断是否使用宽字符 system("pause"); //使程序暂停一下 return 0;}
在程序执行 system("pause"); 暂停的时候我们来观察一下我们的,调试输出窗口:
#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;}
我们按 f5 开始调试:
#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 的输出窗口:
10.vs调试的总结以及技巧
我们已经对
visual studio 的调试有了一些了解,在这一节,我们将向大家展示我们在调试过程中可以使用的一些实用技巧。使大家的调试操作更加轻松愉快。
首先我们再了解一下visual studio 中,release构建模式和debug 构建模式的区别。我们在可以切换构建模式。release构建模式下构建的程序为发行版,而debug构建模式下构建的程序为调试版。在
visual studio 中调试模式还会定义两个宏 _debug 和 debug,后文我们将介绍它们的一些妙用。在 visual studio 中,如果我们要更改编译参数的话,可以点击菜单 -> 项目(p) -> <项目名>属性(p),我们在弹出的页面左侧选择配置属性即可对编译参数进行修改。
接下来,我们来了解一下调试标记。不知道大家有没有遇到这样的情况,我们需要在调试的时候额外运行一段代码,但是实际发布的时候却不需要这段代码呢。那该怎么办,绝大多数数的初学者会选择使用注释,即在发布的时候将无用的测试代码注释掉。但是这样很麻烦,下面我们就为大家介绍一种全新的方法——使用调试标记。事实上这种方法我们在前面使用过,但是没有详细讲解。
这种方法借助了预处理指令,方法很简单,我们首先定义一个宏作为处于调试状态的标记,后面的代码我们用 #ifdef 和 #endif 预处理指令检测宏是否被定义,然后由编译器判断是否编译其中的代码。这么做的好处就是可以减少发布程序的体积,另一方面可以提高发布程序的运行效率。下面是一段示范代码:
#include #define _debugnowint 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)#endifint 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;}
我们在自动完成将或者鼠标移动到编辑器内函数名上,就有了智能提示。
这种智能提示不但可以减轻我们编写代码时的负担,在调试情境下也可以增加我们检查代码的效率。如此一来,岂不美哉!
接下来我们为大家总结了一些调试的经验以及调试思路:
当我们运行我们编写的程序发现运行结果与我们预想的不同的时候,我们可以先用即时窗口,使用一些比较简单的数据来测试我们的各个函数运行结果是否符合我们的预期,如果都符合的话,我们可以使用程序中产生的一些比较复杂的数据来进一步测试我们的各个函数,直至找到可能导致错误的函数。
找到可能导致错误的函数之后,我们就可以使用逐语句调试来一步步跟踪运行程序了,渐渐的的我们就可以缩小范围直至定位错误(无关代码可以考虑暂时注释掉),在这期间,我们要仔细观察程序运行过程中各个数据的变化情况,观察的仔细与否直接与我们能否找到错误直接挂钩。
如果上一步运行的数据一直是正常的,我们就可以排除这个函数的嫌疑了(减少对他的调试次数)。此时,我们就应该考虑问题是否出现在之前的函数上了,可能因为偶然性,我们第一次测试函数的时候并没有发现其错误,导致范围锁定产生偏差,此时我们需要再次耐心的对所有未排除嫌疑的进行调试,直至再次找到出错的函数。再重复上一步,直至找到错误。
可以看到,调试其实是一项比较复杂的活,需要大量的操作,所以在我们编写代码的时候要万分谨慎!因为很多时候,bug都是因为我们的粗心大意导致的笔误引起的!
项目名>
如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复
如何在没有core文件的情况下用dmesg+addr2line定位段错误
用QT制作3D点云显示器——QtDataVisualization
网友评论