什么是bug?
bug一次的原意是“昆虫”或虫子;而在电脑系统或程序中隐藏着一些违背发现的缺陷或问题,人们也叫它“bug”。
“bug”的创始人格蕾丝·赫柏(Grace Murray Hopper),是一位为美国海军工作的电脑专家,也是最早将人类语言融入到电脑程序的人之一。而代表电脑程序出错的“bug” 这名字,正是由赫柏所取的。1947年9月9日,赫柏对Harvard Mark II设置好17000个继电器进行编程后,技术人员正在进行整机运行时,它突然停止了工作。于是他们爬上去找原因,发现这台巨大的计算机内部一组继电器的触点之间有一只飞蛾,这显然是由于飞蛾受光和热的吸引,飞到了触点上,然后被高电压击死。所以在报告中,赫柏用胶条贴上飞蛾,并把“bug”来表示“一个在电脑程序里的错误”,“Bug”这个说法一直沿用到今天。
第一次发现的导致计算机错误的飞蛾,也是第一个计算机程序错误。
调试是什么?
程序调试是将编写的程序投入实际运行前,用手工或编译程序等方法进行测试,修正语法错误和逻辑错误的过程。这是保证计算机信息系统正确性的必不可少的步骤。编完计算机程序,必须送入计算机中测试。根据测试时所发现的错误,进一步诊断,找出原因和具体的位置进行修正。
拒绝迷信式调试!!!
调试基本步骤
- 发现程序错误的存在。
- 以隔离、消除等方式对错误进行定位。
- 确定错误产生的原因。
- 提出纠正错误的解决办法。
- 对程序错误予以改正,重新测试。
Debug和Release的介绍
- Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序猿们调试程序。
- Realease称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
Debug和Release产生文件大小对比
#include <stdio.h>
#include <stdlib.h>
int main(){
char str[] = "hello, world!";
printf("%s\n", str);
system("pause");
return 0;
}
上述代码在Debug环境的结果展示:
上述代码在Realease环境的结果展示:
Debug和Release反汇编对比
所以说调试就是在Debug版本的环境中,找代码中潜伏的问题的一个过程。
那编译器做了哪些优化呢?
先来看一段代码
#include <stdio.h>
#include <stdlib.h>
int main(){
int i = 0;
int arr[10] = { 0 };
for (i = 0; i <= 12; ++i){
arr[i] = 0;
printf("hello, world!\n");
}
system("pause");
return 0;
}
首先这个代码在Debug模式下是死循环,让我们来看看为什么是死循环?
让我们来调试看看。
从上面可以看出来,数组先创建,i变量后创建。它们之间间隔48个字节。也就是i和arr[12]指向的是同一块内存空间。所以当for循环执行到i = 12时,会将arr[12]指向空间的内容修改为0,也就是将i的值修改为0。从而导致死循环。
如果是Release模式去编译,程序没有死循环。这又是为什么呢?
从上面的结果可以看出,i变量先于数组创建,就不会出现Debug模式下出的问题。因此Release模式下不会陷入死循环主要就是由于优化导致的,变量在内存中开辟的顺序发生了变化,影响到了程序执行的结果。
Windows环境调试介绍
调试环境的准备
VS2013:
在环境中选择Debug选项,正常调试。
快捷键介绍
- F5:
启动调试,经常用来直接跳到下一个断点出。 - F10:
逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。 - F11:
逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最常用的)。 - CTRL + F5:
开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。
调试时查看程序当前信息
查看临时变量的值
查看内存信息
查看调用堆栈
查看汇编信息
方法一
右键函数名,转到反汇编。
方法二
查看寄存器信息
调试实例
实例一
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(){
int i, j, n;
int sum = 0;
int ret = 1;
printf("Please input the n: \n");
scanf("%d", &n);
for (i = 1; i <= n; ++i){
for (j = 1; j <= i; ++j){
ret *= j;
}
sum += ret;
}
printf("%d! = %d\n", n, sum);
system("pause");
return 0;
}
运行结果
3的阶乘加2的阶乘加1的阶乘为15,结果明显不对,开始调试。
将断点打在for循环入口处。
按F5启动调试。
输入n的值3。
打开监视窗口,监视变量i、j、ret和sum的值。
按F11逐语句进行调试,观察监视变量的值。
观察监视窗口过程中,发现ret的值变化不正常,根据代码可知,ret应该是i的阶乘。但是当i为3时,ret竟然是12,这明显不正常,我们在取看源代码就会发现,ret一直在累乘,并未在计算完i的阶乘后将其值设为1。故而出错。
修改之后,结果如下:
实例二
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(){
int i = 0;
int arr[10] = { 0 };
for (i = 0; i <= 12; ++i){
arr[i] = 0;
printf("hello, world!\n");
}
system("pause");
return 0;
}
运行结果
结果是死循环,这是为什么呢?调试来看看。首先既然是死循环,我们就将断点打在循环入口处。
按F5开始调试。
打开监视窗口,既然是死循环,我们就来监视一下i的值。
从监视窗口可以看出,i在增加到12后,再加1就变成了0,这是为什么呢?
我们在监视窗口中添加&i和&arr[i]来观察一下。
观察监视窗口可以看出,i变量的地址和arr[12]是同一个地址,也就是程序对arr[12]的值进行修改的时候,i的值也会得到相应的修改。即i的值被赋0。
常见的coding技巧
- 使用assert。
- 尽量使用const。
- 养成良好的编码风格。
- 添加必要的注释。
- 避免编码的陷阱。
coding常见的错误
- 编译型错误
直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。 - 链接型错误
看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或拼写错误。 - 运行时错误
借助调试,逐步定位问题。最难搞。