前言
要想成为一个合格的程序员,不仅仅要会写代码,更要会调试代码。咔咔一通敲代码,敲出了BUG,这时就分两种程序员,一种是质疑编译器的程序员,“什么?我写出了BUG,是不是机器出了问题?”,然后接着就是迷信式地调试,直至解决BUG。另一种是默默地高效地调试代码,把BUG给修复了。一个合格的程序员不但要会写BUG,还要懂得通过调试来修复BUG,毕竟第一个人想写BUG那是拦都拦不住的。本篇文章案例所使用环境为VS2019。
1.什么是BUG?
bug在英文中的意思是虫子,这又为什么会和计算机程序沾边呢?原来在1947年9月9日,技术员赫柏对继电器编程完毕后,尝试运行,发现继电器无法正常工作。在排查问题后,发现是一只飞蛾飞到了继电器里,触电身亡后,它的尸体导致了机器卡住。
因此在提交错误报告的时候,赫柏直接将飞蛾贴在了报告了,并标了“bug”,从而导致bug有了后面的衍生意。这也是计算机程序的第一个bug。
BUG通常指的是计算机程序程序因为编写错误从而产生的错误以及漏洞。
2.调试的重要性
2.1.什么是调试?
调试(英语:Debugging / Debug),又称排错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
2.2.调试的基本步骤
1、发现程序错误的存在
2、以隔离、消除等方式对错误进行定位
3、确定错误产生的原因
4、提出纠正错误的解决办法
5、对程序错误予以改正,重新测试
程序编写时的常见错误
通常程序的错误分为以下三种:
1、编译型错误
2、链接型错误
3、运行时错误
1、编译型错误:通常是指语法语法出现了问题而产生错误。
2、链接型错误:通常是指标识符名不存在或者拼写错误。
此时编译这段代码,编译器并没有报错。control + F5让代码运行起来。
当代码较多时,不方便一眼看出拼写错误,可以通过control+F 文本查找功能,输入错误的标识符来定位。
3、运行时错误:当编译和运行代码都没有问题时,程序此时运行发生了错误。这也是最难处里的错误。此时就需要借助调试,逐步定位问题。例如:
int main()
{
int i = 0;
int sum = 0;//保存最终结果
int n = 0;
int ret = 1;//保存n的阶乘
scanf("%d", &n);
for(i=1; i<=n; i++)
{
int j = 0;
for(j=1; j<=i; j++)
{
ret *= j;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
这个BUG我会在后面进行调试修复的演示。
2.3.debug版本与release版本的介绍
debug版本:通常被称为调试版本。它包含了调试信息,不对程序进行优化,便于程序员调试程序。
release版本:通常被称为发布版本。它不包含调试信息,所以无法进行调试。release版本对程序进行了各种的优化,使得程序的代码大小以及运行速度都是最优的,便于用户的使用。
//本段代码在debug版本与release版本的的代码大小区别
#include <stdio.h>
int main()
{
char str[]=“hello world!”;
printf(“%s\n”,str);
return 0;
}
debug版本与release版本的exe.文件的大小对比
//本段代码在debug版版本与release版本的区别
#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;
}
debug版本下程序死循环打印hehe。
release版本下程序并没有死循环打印hehe
这也是说明了程序在release版本下会进行优化。至于为什么在debug版本下会死循环打印hehe呢?首先可以肯定的错误是数组越界访问了,至于为什么会导致程序死循环呢?待会我会对此进行分析。
以上就是release版本与debug版本的一些区别。程序在release版本为什么不能调试?这里我就不做演示,下面将介绍VS2019环境下如何调试。如果你对此感兴趣,不妨在看完下文的介绍后上手试试看看,毕竟实践出真知。
3.VS环境调试介绍
补充:本文使用环境为VS2019。VS环境下,调试的方式和调试的逻辑大同小异。
3.1.环境准备
在当前环境下使用debug模式,这样才能正常的进行调试。
3.2如何开始调试?
开始调试步骤如下:
开始调试后,打开窗口菜单后,如图所示
未开始调试,打开窗口菜单。如图所示
所以进入调试是需要按快捷键或者在调试菜单中单击开始调试(s)进入调试。
3.3学会使用快捷键进行调试
合理的使用快捷键可以大大地提高我们的调试效率,所以学会使用快捷键进行调试是很重要的。
常用的快捷键:
F5
启动调试,经常用来直接跳到下一个断点处。
F9
创建或取消断点。
补充: 断点是在特定点暂停程序执行的特殊标记,使用断点可以检查当前程序状态和行为。断点一旦设置便保留在你的项目中,直到你明确删除它。
F10
逐过程,通常用来处理一个过程。一个过程可以使一条语句,或是一次函数的调用
F11
逐语句,就是每次都执行一条语句,但是这个快捷键可以使执行逻辑进入函数内部(这是最
常用的)。
CTRL + F5
开始运行不调试,直接让程序运行起来。如果不想进行调试可以直接使用此快捷键运行程序。
3.4.查看当前程序的信息
3.4.1.查看当前临时程序变量的值
在调试时,观察变量的值的变化。
3.4.2.查看内存信息
通过&取地址操作符,找到变量存放的位置以及存储在内存中的值。
3.4.3.查看调用堆栈
通过调用堆栈,可以清晰的反应函数的调用关系以及当前调用所处的位置。
3.4.4.查看汇编信息
在调试开始之后,有两种方式转到汇编:
方式一:右击鼠标,选择【转到反汇编】:
方法二:在调试菜单中的窗口菜单中:
3.4.5.查看寄存器信息
查看当前的寄存器信息
4.调试问题代码
4.1.例一
int main()
{
int i = 0;
int sum = 0;//保存最终结果
int n = 0;
int ret = 1;//保存n的阶乘
scanf("%d", &n);
for (i = 1; i <= n; i++)
{
int j = 0;
for (j = 1; j <= i; j++)
{
ret *= j;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
为什么统计的各个阶乘的和是错的呢?
我们通过调试来看看
通过调试我们发现,当i = 3时,此时ret的值为2,所以才导致求3的阶乘时程序产生了bug。找到了问题,下面是分析该问题,这样每次统计完一个阶乘并把它放入累加的变量中,进入下一次循环前,要把ret的值修改为1,以确保求出阶乘是 正确的。故修改后代码如下
int main()
{
int i = 0;
int sum = 0;//保存最终结果
int n = 0;
int ret = 1;//保存n的阶乘
scanf("%d", &n);
for (i = 1; i <= n; i++)
{
int j = 0;
ret = 1;//每次求n的阶乘将ret的值改成1
for (j = 1; j <= i; j++)
{
ret *= j;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
4.2.调试实例例二
//为什么死循环打印hehe?
#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;
}
数组越界访问后,这里程序为什么死循环打印hehe呢?调试一下你就知道。
通过监视发现,arr[12]的值随着 i 的值发生变化。
根据查看内存中的地址,可以发现,arr[12]的地址和i的地址是一样的。所以当i = 12进入循环,执行arr[i] = 0;后,i 的值又变成0。循环往复造成程序死循环输出hehe。需要注意的是:这段代码仅在VS2019 X86环境有效。下面我通过画图的方式来演示一下这段代码在内存中的情况。
越界访问会造成程序的错误,我们应该在使用数组的时候检查下标避免越界访问。
5.如何写出易于调试的代码
5.1.什么是优秀的代码?
- 代码运行正常
- bug很少
- 效率高
- 可读性高
- 可维护性高
- 注释清晰
- 文档齐全
编写优秀代码技巧
- 使用assert
- 尽量使用const
- 养成良好的编码风格
- 添加必要的注释
- 避免编码的陷阱。
5.2.优秀编码实例
//模拟实现库函数strlen
size_t my_strlen(const char* str)
{
//断言判断指针有效性
assert(str != NULL);
//记录起始位置
char* start = str;
while(*str++)//str指向的内容不为\0指针++
{
;
}
//通过指针减指针求出字符长度
return str - start - 1;
}
补充:
1、使用断言assert需要包含对应头文件 #include<assert.h>.
2、表达式应注意操作符的优先级问题。
3、通过const来修饰形式参数,保证指针的安全性
4、确定合适的返回类型。因为字符串长度不可能是负数,所以返回类型定义为size_t
5、添加合适的注释
5.3.const修饰指针变量
//const放在*号左边
int main()
{
int num = 10;
int n = 1000;
const int* p = #
*p = 20;//err
p = &n;//ok
return 0;
}
当const放在 * 号左边时,修饰的是指针变量指向的内容,指针变量指向的内容不可以被修改,但是指针变量本身可以被修改。
//const放在 *号右边
int main()
{
int num = 10;
int n = 1000;
int* const p = #
*p = 20;//ok
p = &n;//err
return 0;
}
当const放在 * 号右边时,修饰的是指针变量本身,指针变量不可以被修改,但是指针变量指向的内容可以被修改。
总结
只有多多动手调试,才能够有进步!当遇到bug时,不要再迷信式的调试。我们要尝试通过调试来验证我们的想法是否能够实现。也只有加化自身的调试技巧,才能够让编程之路越走越远。