调试是我们每个程序员都必备的技能之一,调试能在程序逐步运行过程中锁定目标变量,找出问题,解决问题。我们一般把程序中的运行问题称为 "Bug" ,Bug是程序员一生之敌;正所谓一物降一物,面对Bug,我们有调试,只要调试玩的溜,Bug就无所遁形。👾👾👾
动图原作者:@我的邻居全是猫
世界上第一个Bug,一只夹死在大型计算机中的飞蛾
我们的调试环境:VS2019,当然VS系列方法都大同小异,掌握核心方法就行了。
Release 为发布版本,用户使用的程序就是这个版本;Debug 则是调试版本,是我们程序员使用的开发版本,两者的最大区别:是否对代码进行了优化。
Release版本是经过编译器优化后的版本,去除了很多对用户来说无用的功能,因此Release版本大小要小于Debug版本,同时代码在运行速度上也要优于开发版本。
Debug版本最大优势就是能随便调试,各项开发功能功能齐全,因此找Bug都是在这个版本中进行的。Release版本会对代码进行优化,因此某些开发版本中的Bug可能不会在发布版本中复现,测试人员用的是Release版本,大概率是不会让用户找Bug的,除非某UI。
我们的键盘最顶部有一排 Fx 键,这些都是辅助功能键,因为用户用的少,所以大多数厂商都会把这些辅助功能键映射成不同的功能(大多数笔记本自带键盘),比如音量+ - 亮度调节等。而我们开发人员需要用到这些辅助键,因此首先我们要把这些映射状态解除,不同品牌解除方法可能略有差异,联想拯救者的解除方法是 Fn+Esc,这样我们就能愉快的在VS中使用快捷键了。
这个快捷键的作用是直接开始执行程序,不进入调试模式,一般用于验证程序的执行情况。
F5 一般和 F9断点搭配使用,当断点创建后,按F5就可以直接跳到断点处,当然直接使用 F5也是可以进入我们的调试模式的。如果程序没有输入环节,我们的 F5调试会直接执行完程序的。
F9 作用就是创建断点,一般是和F5 配合使用,比如我们的程序有30行(假设没有输入语句),但我们想查看20行的变量情况,只需要在20行创建一个断点,然后再按F5 就可以了,程序执行到第20行就会自动停止。当然断点也能用来跳过无用的循环,只需要在创建的断点上右击断点添加条件即可,提高调试效率。
创建断点的方法(两种): 1.直接鼠标左击想要停止语句所对应的左侧灰色区域。
2.鼠标点击目标语句,确保处于选定状态,按F9即可创建断点。
F10 的作用是可以一步步的进行调试,在调试状态下,按一下F10,程序就走一步,如此重复。因为F5 进入的调试模式,如果没有输入语句或断点截停,程序可能会直接执行完,不利于调试观察,因此我们一般使用F10 进入调试模式,一步一步的走,F5 还得搭配 F9使用。如果遇到函数的话,按F10 并不会进入到函数内部,而是直接出结果,因此如果我们想要实现真正的逐语句调试,还得看F11(后面会介绍的)。
F11 才是真正意义上的逐步调试,使用F11 可以很仔细的观察到程序的走向,同时F11 能进入函数内部(函数是C语言程序的重要组成部分),因此我们一般使用F10 唤起调试模式,F11 进行调试
以上便是我们在调试时常用的几个快捷键,灵活使用就能很好的进行调试找错。F5 一般是在有断点的情况下使用,而断点是用来跳过无用语句或循环的,F10 是在不想进入函数时使用,F11 则是非常详细的进行调试,会进入函数内部。推荐使用F10 或F11 进入调试模式,而不是F5,避免程序一闪而过。当然VS中还有很多快捷键,比如Ctrl+k+c 注释代码,Ctrl+k+u 取消注释,这里推荐一篇博客,里面介绍了很多快捷键:VS中常用的快捷键_MrLisky的博客-CSDN博客_vs快捷键
现在我们已经能进入调试并使用调试了,那么调试中的各种窗口信息又该怎么看呢? 下面让我给大家简单介绍几个常用窗口:
这个窗口主要是用来监视各种变量、表达式的值变化的,也是最常用的窗口之一。
当我们设好监视值后,就可以按F10 或F11 来观察变量的变化情况,当然监视窗口内的监视值可以随时改变,也可以在调试过程中唤出监视窗口,使用都很自由。
这两个窗口就是监视窗口的改版,偏向于半自动化,但是不如监视窗口自由。
内存窗口主要是用来查看变量在内存中的地址信息的,对于指针的纠错需要时刻观察指向地址。
汇编代码出现于高级语言之前,这种夹杂着机器语言和汇编指令组成的代码比较难懂,但电脑能快速读懂,运行速度是极快的。
想要读懂汇编代码需要经过一定的学习,在某些场景下,汇编代码更能展示代码实现逻辑。
寄存器是指有限存贮容量的高速存贮部件,寄存器位于CPU上,通常用来存储反复使用的数据。
其实还有很多窗口,比如调用堆栈、线程、进程等,但目前还用不上,因此没有做介绍。
现在我们已经对调试有一定的了解了,话不多说,直接把问题程序拿出来调试一下!
代码如下:
猜猜运行结果是什么?栈溢出,报错?还是直接运行失败?
答案都不是,是死循环!
当然这个死循环是需要条件的:1.在VS内 2.x86环境下 3.先定义i,后定义arr
//调试示例
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("%d\n", i);
}
return 0;
}
像这种隐性的Bug不通过调试是根本无法发现的,只有通过调试分析,我们才能找出问题。
我们只有养成良好的编码风格,才有可能从源头上避免Bug的产生,比如:
下面展示一段编码风格十分优秀的代码:
//模拟实现strcpy
#include<stdio.h>
#include<assert.h>
char* my_strcmp(char* pa1, const char* pa2)
{
assert(pa1 && pa2);//断言防止空指针
char* str = pa1;//纪录arr1的首地址
while (*pa1++ = *pa2++)
{
;
}
return str;
}
int main()
{
char arr1[20] = "xxxxxxxxxxx";
char arr2[] = "Hello Coder!";
printf("%s\n", my_strcmp(arr1, arr2));
return 0;
}
在上面这段代码中,模拟实现了strcpy字符串拷贝函数,涉及到的知识有:链式访问、const保护被拷贝的指针,assert断言、利用 '\0' 巧妙赋值和结束循环、完善函数的返回类型。通过这几点优化,使得我们的代码变动更加安全、效率也更高,安全就意味着不容易出现Bug,算得上是一段非常漂亮的代码。
优秀的风格见过了,下面就来看看有哪些编程技巧吧!
我们在编写程序时常常会犯很多错误,根据严重等级,大概可以分为一下三种错误:
编译型错误很容易发现,属于犯错编译器立马就能给出提示的那种,比如逻辑错误(判断式写错、悬空else)、关键字拼写错误等,这种错误编译器能够辅助定位错误,很简单,修改解决错误即可。
链接型错误一般指自定义函数拼写错误或者标识名未定义,这种错误需要结合上下文寻找,用点心仔细寻找也能发现问题,当然编译器肯定也会辅助定位错误位置。
这种错误就比较棘手了,程序能运行,说明语法没有问题,问题出在语句的逻辑搭配上面,比如计算1+1=3,此时需要进入调试模式,逐步寻找,利用本文的知识,就能解决问题!
调试的内容并不多,无非就是几个快捷键和几个信息窗口搭配使用,解决问题最关键的还是经验,只有调试的多了,我们才能积累到这些失败经验,牢记于心,不断升级,成为一名调试大师!
调试就像破案的过程,我们就像侦探,而Bug就是我们的目标,通过蛛丝马迹,解决Bug!