在我们编写程序的时候,难免会遇到程序中存在问题,运行报错或是运行结果不符合预期的情况
而当我们发现程序中存在问题的时候,那下一步就是找到问题,而后通过各种手段去定位问题的位置,最后修复问题,重新测试
调试则是定位代码逻辑的核心手段,本文以VS2026为例,整理集成开发环境下的实用调试技巧,虽然相比专业调试手段难度不高,但对于绝大多数中小型程序是够用的
一. Debug和Release

Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于开发师调试;
我们在写代码的时候,也需要经常性地调试代码,可以将这里设置成debug,这样编译产生的是debug版本的可执行程序,其中包含调试信息,是可以直接调试的
Release成为发布版本,它往往是进行了全量优化,使得程序在代码体积和运行速度上都是最优的,以便用户很好地使用
这个版本是不包含调试信息的,编译产生的就是release版本的可执行程序,这个版本是用户使用的
这里我们举个简单的例子,我们来再X86环境下运行一下这段代码:
#include<stdio.h>
int main()
{
int i = 0;
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
}
printf("程序正常运行\n");
return 0;
}
相信大家都能看出来,数组越界访问了,那在X86环境,debug版本下,运行结果是什么样子的呢?

我们发现程序陷入死循环了,最后一句printf无法执行
那Release版本下的运行结果呢?

我们会发现在Release版本下,程序没有陷入死循环,printf函数最后执行了
我们再来看两个不同版本的可执行程序的体积大小有什么区别?


对比可以看到,同一段代码,编译生成的可执行程序的大小,release版本明显要比debug版本更小,且release优化了debug版本的某些问题
大家也可以在X64环境下运行一下,得出的结果大概率是debug版本下数组越界,直接报错,而release版本下依然可以正常运行
关于这个问题的具体原因,我们放在后面讲,这里我们只需要知道release这个“用户版本”确实对程序做了一定限度的优化即可
二. VS调试快捷键
我们这里列举一些调试最常用的几个快捷键:
- F9:创建断点和取消断点
我们可以在程序的任意位置设置断点,断点的作用是可以使得程序执行到断点的位置后暂停执行,接下来我们就可以使用F10,F11这些快捷键,观察代码的执行细节
当然断点也分为好几种类型,相对比较常用的是条件断点,也就是满足某个条件,才会触发断点
-
F5:启动调试,经常用来直接跳到下一个断点处,一般是和F9配合使用
-
F10:逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句
-
F11:逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部,即在函数调用的地方,如果想进入函数观察细节,就要用到F11,如果只是使用F10的话,就会直接完成函数调用,无法观察函数内的细节
-
ctrl + F5:开始执行不调试,如果我们想让程序直接运行起来而不调试,就可以直接使用这个快捷键
当然,VS的快捷键肯定不止这些,如果对这些有兴趣的,也可以去额外了解更多,这里我给大家提供一个传送门:某佬的博客
三. 监视和内存观察
在调试过程中,我们要观察代码执行过程中上下文环境变量的值,有哪些方法呢?
这些观察的前提一定是开始调试后观察(即debug模式下,按下F10后),比如监视窗口:

我们可以在监视窗口输入变量的名称,就可以在F10/F11的配合下动态观察了,这里我们还是用原来的代码,把循环条件改成i < 10,便于我们观察

随着我们使用F10,代码不断往下执行,我们会发现变量里的值也在随着代码的执行而变化,这个观察是非常直观且清晰的
如果监视窗口看的不够仔细,也是可以观察变量在内存中的存储情况:

打开内存窗口后,在地址栏输入:arr,&i,这类地址,就能观察到该地址处的数据

这里为了方便观察,8个十六进制数字为1个字节,所以我们一般一行设置为4列,一般一列显示两个数字
除此之外,调试的窗口还有很多,比如自动窗口,局部变量,反汇编,寄存器窗口等,感兴趣的小伙伴可以额外拓展一下
四. 调试分析
我们刚才已经说过,在X86,Debug环境下,这段代码运行的结果:
#include<stdio.h>
int main()
{
int i = 0;
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
}
printf("程序正常运行\n");
return 0;
}
那我们能不能知道它为什么死循环呢?我们就可以通过调试的手段去剖析这段代码的问题
我们可以先在监视窗口中去看一下arr数组的各项,以及i是怎么变化的,到底是从哪一步开始与我们的预期不符的

到这里,我们还没有发现任何问题,虽然数组的确越界访问了,但在越界的基础上,i在逻辑上确实自增到了12,且遍历了arr数组下标从0到11的各项,赋值为0
我们继续往下走,由于判断条件是i <= 12,接下来按照正常逻辑来说,arr[12]要被赋值为0,随后i自增到13,循环结束

我们会发现从这里就开始出现问题了
i变成了0
为什么呢?正常循环逻辑上来讲,不应该是arr[12]被赋值为0后,i变成13吗?为什么i会变成0呢?
这里我们会有一个猜想:arr[12]变成了0,i就莫名其妙地变成了0,难道i和arr[12]用的是同一块内存空间吗?
我们在监视窗口给它们取地址,观察一下

我们会发现这两个变量竟然真的共用了同一块内存空间,那一切就能解释的通了,arr[12]被赋值为0的时候,i同时也被赋值为了0,循环条件i <= 12依然满足,于是继续从给arr[0]赋值开始循环,直到arr[12]再次被赋值为0,i又“被迫”变成0,以此类推,程序陷入死循环,自然而然没有给“数组越界”这个问题报错的机会,因为程序还没跑完
但是我们知道X64环境下是没有这个问题的,Debug环境下,程序没有死循环,最终会报出数组越界的错误
那两者底层的差异在哪?
这里我们不得不讨论一些内存布局的知识

我们首先要知道,栈区的默认使用习惯是从高地址向低地址使用的,所以变量i的地址是较大的,arr数组的地址整体是小于i的地址的
而数组在内存中是怎么存放的:随着下表的增长,地址是由低到高变化的
所以,如果内存的布局如上图所示,那随着数组下标的增长,往后越界就有可能覆盖到i,这样就可能造成死循环的
这里肯定有小伙伴有疑问:为什么i和arr数组之间恰好空出来2个整形的空间呢?这里确实是巧合,在不同的编译器下可能中间空出来的空间大小是不一样的,代码中间这些变量内存的分配和地址分配是编译器指定的,所以不同的编译器之间就有差异了,也就是说这个题目是和环境强相关的
而且栈区的默认使用习惯虽然确实是先使用高地址,再使用低地址,但这个具体还是要取决于编译器的实现,比如我们之前聊到的,X64环境下,这个使用顺序其实就是相反的,也就是说i变量的地址是低于arr数组首元素的地址的,数组的地址永远无法和i重叠,自然也就不可能陷入死循环,最终也就必然会导致数组越界报错
当然在Release版本中,这个使用顺序也是相反的
从这个理解上来说,我们也就能体会到调试的重要性了,只有调试才能观察到程序内部执行的细节,就像医生给病人做B超,CT一样
五. 补充
我们也可以在这里多思考一些:如果遇到一个稍复杂的代码,那怎么调试呢?
这里我们可以上手调试一下我们之前实现过的简易扫雷游戏的代码
我们这里不做过多赘述,直接演示:
- 在函数内部打断点,搭配F11快速跳转到函数内部

- 在数组传参时,调试进入函数,如何在监视窗口观察数组的内容:数组名,n 的形式
这里需要注意,数组传参时,如果在监视窗口只填写数组名,是观察不全的
需要数组名,n 的形式,才能观察到全部的数组元素

我们在调试过程中,要做到心中有数,也就是我们自己心里要清晰地知道希望代码怎么执行,然后再去看代码有没有按照我们预定的路线在执行;同时调试也是需要反复去动手练习的,它也可以增加我们对代码的理解和掌控,掌握了调试的能力,就能看到本质,就像能给程序做B超一样,对程序内部一览无余
1125



被折叠的 条评论
为什么被折叠?



