持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情
前言
对于一个程序员来说,写代码固然重要,但其实熟悉调试更重要,本文旨在分享一波VS下调试技巧,以提高防范bug、调试纠错的意识与能力。本文是中篇,讲讲调试的实例。
笔者水平有限,难免存在纰漏,欢迎指正交流。
一些调试的实例
实例一
实现代码:求 1!+2!+3! ...+ n! ;不考虑溢出。
int main()
{
int i = 0;
int j = 0;
int sum = 0;//保存求和结果
int n = 0;
int ret = 1;//保存n的阶乘
scanf("%d", &n);
for(i=1; i<=n; i++)
{
for(j=1; j<=i; j++)
{
ret *= j;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
这时候我们如果输入3,期待输出9,但实际输出的是15。
为啥捏?
首先,我们瞟一眼在for循环之前的变量初始化与scanf函数,明显没有问题,那我们大致可以将问题定位在for循环里面。
我们在for循环开头打个断点,启动调试跳过去,输入3,回车一下把值传给scanf,打开监视窗口把要观察的变量输进去。
因为我们不清楚具体是循环内的哪一步出现问题了,而我们测试的数值为3,逐步调试成本不高,所以考虑逐语句调试。
i = 1循环完后,ret存的是1的阶乘,sum存的也是1的阶乘,一切正常,继续
i = 2循环完后,ret存的值是2的阶乘,sum存的是1的阶乘和2的阶乘之和,一切正常,继续
i = 3循环完后,ret存的值是12,明显不是3的阶乘6,从这里开始出现问题。
我们回溯一下,我们设计ret就是让它存放n的阶乘,思路是用for循环从1乘到n再把值放到ret,所以发现问题了没?
前两次在算阶乘时刚好ret初值都为1,不会影响阶乘计算,而这一次(i = 3)中ret初值为2而非1!这就影响了阶乘的计算,并且每一次的ret的值会遗留到下一次循环去,使得结果偏差越来越大。
通过调试发现了问题,现在考虑如何解决。ret的值每次都需要变为1嘛,也就是要刷新它的值,那只需要每次出了内层for循环之后马上把ret的值重置为1即可,小小改动一下便解决了问题。
如下:
实例二
#include <stdio.h>
int main()
{
int i = 0;
int arr[10] = {0};
for(i=0; i<=12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
我们把程序跑起来看看,发现陷入了死循环,这是为什么呢?
你说我数组越界了吧,但是也能访问啊,不应该循环13次就结束了吗?到底怎么回事?
这确实和数组越界访问有关,更重要的是产生了“巧合”。
要是干看着的话很难找出问题根源,我们调试一下进入循环内部看看。
先走到for循环前
进入for循环一轮一轮地看
发现了i的值竟然和arr[12]的值同步变化,莫非……它们的值在同一块内存上?
还真是这样,所以在给arr[12]赋0的时候也改变了循环变量i的值变为0,小于12满足循环条件,再次循环,下一次i累加到12时又变回0,一直往复最终导致死循环。
那为什么i和arr[12]的值在同一块内存上呢?
这其实与函数栈帧有关。
(更多关于函数栈帧请戳这里):[深入浅出C语言]深入函数栈帧 - 掘金
这里简单地解释一下,来,上动图!
最终的图
分析:
之所以说是“巧合”,是因为在VS下函数栈帧中局部变量创建时间隔8字节,而代码中数组越界访问恰好向后8个字节,于是使得循环变量i发生改变,最终导致程序死循环。
可能有人就会说了,那就先创建数组再创建变量i呗,这样一来不管你怎么越界都不会改变循环变量i的值,也就不会出现死循环了。
实际上不建议这样考虑,容易忽视根本原因,仔细想想,“罪魁祸首”是谁?当然是数组的越界访问!要从根源上制止的话就要预先防止数组越界,而不是死记数组先创建循环变量后创建。
诸如这类问题,不调试一下很有可能想破脑袋都想不到原因(除非对函数栈帧局部变量创建很熟悉),你不能保证每一个问题都能直接想到可能原因,很多情况下的问题都需要妙用调试来一探究竟。
以上就是本文全部内容,感谢观看,你的支持就是对我最大的鼓励~