C语言第五章VS调试技巧

发布于:2025-07-29 ⋅ 阅读:(22) ⋅ 点赞:(0)

一.什么是bug?

        bug本意是“昆虫”或“虫子”,现在一般是指在电脑系统或程序中,隐藏着的⼀些未被发现的缺陷或问题,简称程序漏洞。 “Bug”的创始人是格蕾丝·赫柏(Grace Murray Hopper),她是一位为美国海军工作的电脑专家,1947年9月9日,格蕾丝·赫柏对设置好17000个继电器进行编程后,技术人员正在进行整机运行时,机器突然停止了工作。于是他们爬上去找原因,发现这台巨大的计算机内部一组继电器的触点之间有一只飞蛾,这显然是由于飞蛾受光和热的吸引,飞到了触点上,然后被高电压击死,进而导致机器停止了工作。所以在报告中,赫柏用胶条贴上飞蛾,并把“bug”来表示“一个在电脑程序里的错误”,“Bug”这个说法一直沿用到今天。

二.什么是调试?

        当我们发现程序中存在的问题的时候,那下一步就是找到问题,并修复问题。 这个找问题的过程叫称为调试,英文叫debug(消灭bug)的意思。调试一个程序,首先是承认出现了问题,然后通过各种手段去定位问题的位置,可能是逐过程的调试,也可能是隔离和屏蔽代码的方式,找到问题所在位置,然后确定错误产生的原因,再修复代码, 重新测试,直到代码无问题。

三.Debug和Release

        在VS2022上面有Debug和release 两个版本,意思分别是什么呢?

        Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序;程序员在写代码的时候,需要经常性的调试代码,就将这里设置为 debug ,这样编译产生的是 debug 版本的可执行程序,其中包含调试信息,是可以直接调试的。

        Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的, 以便用户很好地使用。当程序员写完代码,测试再对程序进行测试,直到程序的质量符合交付给用户使用的标准,这个时候就会设置为 release版本 ,编译产生的就是 release 版本的可执行程序,这个版本是用户使用的,无需包含调试信息等。

上方两个可执行文件:左边大的是Debug版本 ,右边小的是Release版本。一般在公司内,测试开发人员均用Release版本,主要原因是:Release版本为最后发行的用户会使用的版本,企业的测试开发人员会站在用户的角度进行项目的测试。Release版本没有调试信息,所以在VS2022对运行的代码进行测试的时候,应该使用Debug版本,否则无法进行逐步测试。

四.VS调试快捷键

1.环境准备

        首先,如上述我们应该在调试代码之前保证代码是Debug版本,因为此版本有调试信息可供程序员调试代码使用。 

 2.调试快捷键

        F9:创建断点和取消断点。(将光标放在需要设置断点的行上面,然后点击F9就设置成功了,再点击一次就会取消断点)。断点的作用是可以在程序的任意位置设置断点,打上断点就可以使得程序执行到想要的位置暂停执行,接下来我们就可以使用F10,F11这些快捷键,观察代码的执行细节。条件断点:满足这个条件,才触发的断点。条件断点可以在普通断点设好的前提下,在断点处右击鼠标,然后就能设置条件了。(比如断点在标记循环位置处进行到第五次时,开始调试)

        F5:启动调试,经常用来直接跳到下一个断点处,一般是和F9配合使用。(这里的下一个断点处不是物理意义上的下一个断点,而是逻辑意义上的下一个断点处。比如断点在循环内部,那么下一个逻辑意义上的下一个断点就在下一次循环的该断点处)

        F10:逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。点击一下运行一个语句。遇到函数的话,如果依然点击F10,调试便会直接把函数调用当作一个语句调试过去,这样遇到函数调试的效果并不精确。           

        F11:逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部。在函数调用的地方,想进入函数观察细节,必须使用F11,如果使用F10,就会直接完成函数调用。在没有函数的情况下,F10和F11的调试效果是一样的。但是碰到函数的情况下,只有F11可以进入函数内部进行调试

        CTRL+F5:开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。

五.监视和内存观察

        在调试的过程中我们如果要观察代码执行过程中上下文环境中的变量值的变化情况,这时候我们就可以用到监视。这些观察的前提条件一定是开始调试后观察,比如:

#include <stdio.h>
int main()
{
 int arr[10] = { 0 };
 int num = 100;
 char c = 'w';
 int i = 0;
 for (i = 0; i < 10; i++)
 {
 arr[i] = i;
 }
 return 0;
}

1.监视

        开始调试后,在菜单栏中【调试】->【窗⼝】->【监视】,打开任意一个监视窗口,输入想要观察的变量名就行。

 

 2.内存

        如果监视窗口看的不够仔细,也是可以观察变量在内存中的存储情况,还是在【调试】->【窗口】-> 【内存】 

上面是打开监视内存的窗口 ,下面是内存监视区域各个模块的含义:

 

 六.编译常见错误归类

1.编译类错误

        编译型错误一般都是语法错误,这类错误一般看错误信息就能找到⼀些蛛丝马迹的,双击错误信息也能初步的跳转到代码错误的地方。编译错误,随着语言的熟练掌握,会越来越少,也容易解决。下面给出相关的例子:

2.链接型错误

        看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是因为标识符名不存在、拼写错误、头文件没包含、引用的库不存在

3.运行时结果错误

        运行时错误,是千变万化的,需要借助调试,逐步定位问题,调试解决的是运行时问题。所以运行时结果出现的问题就需要程序员长年累月练习,通过经验写出更好的代码。

七.调试举例 

1.举例一

int main()
{
 int n = 0;
 int i = 1;
 int sum = 0;
 for(n=1; n<=10; n++)
 {
     for(i=1; i<=n; i++)
     {
         ret *= i;
     }
  sum += ret;
 }
 printf("%d\n", sum);
 return 0;
}

上述代码是求1加到10的阶乘的和的错误代码:1!+2!+3!+4!+...10!,下面我们进行调试寻找问题:先点击F10,开始调试,然后再调试里找到窗口内的监视,然后分别输入不同的变量名。然后通过不断点击F10,进行调试,最终在求3的阶乘时,发现了问题所在:求出3的阶乘为12。原因很明显:ret为重新赋值,就拿着上一步的ret乘了3的阶乘。下面是代码修正后的样子:

int main()
{
 int n = 0;
 int i = 1;
 int sum = 0;
 for(n=1; n<=10; n++)
 {
     ret=1;
     for(i=1; i<=n; i++)
     {
         ret *= i;
     }
     sum += ret;
 }
 printf("%d\n", sum);
 return 0;
}

 2.举例二

        在VS2022、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("hehe\n");
 }
 return 0;
}

        上述代码明显属于数组下标的越界访问,但是代码的运行结果却是:死循环的打印hehe。这是为什么呢?我觉得应该是打印12个hehe代码就终止了,可是为什么会这样呢?下来我们借用调试工具帮助我们理解代码。首先F10,开始调试,然后找到调试里的窗口选项下的监视,输入每个变量(将数组下标为10 11 12的元素名也输入进去,为了后期观察其值的变化),然后逐步点击F10,直到运行至i等于10的时候,发现代码非法访问将arr[10]改为0,arr[11]也是如此,在这之前两者值均为随机数,可到了arr[12],该值和i的值是一样的数字,这个会是巧合呢?还是其它原因呢?等到arr[12]被非法赋值为0时,i变量此时也变成了0。于是我猜测i的地址和arr[12]的地址是一样的,在为数组非法赋值的同时,将变量i也变成了0。导致循环无法终止,最终陷入了死循环。

        1.栈区内存的使用习惯是从高地址向低地址使用的,所以变量i 的地址是较大的。arr数组的地址整体是小于i的地址。

        2. 数组在内存中的存放是:随着下标的增长,地址是由低到高变化的。所以根据代码,就能理解为什么是左边的代码布局了。如果是左边的内存布局,那随着数组下标的增长,往后越界就有可能覆盖到i,这样就可能造成死循环的。

        为什么i和arr 数组之间恰好空出来2个整型的空间呢?这是巧合,在不同的编译器下可能中间的空出的空间大小是不一样的,代码中这些变量内存的分配和地址分配是编译器指定的,所以 不同的编译器之间有差异。 

        注意:栈区的默认的使用习惯是先使用高地址,再使用低地址的空间,但是这个具体还是要编译器的实现,比如:在VS上切换到X64,这个使用的顺序就是相反的,在Release版本的程序中,这个使用顺序也是相反的。


网站公告

今日签到

点亮在社区的每一天
去签到