目录
1. 什么是bug?
2. 调试是什么? 有多重要?
2.1 调试是什么?
2.2 调试的基本步骤
2.3 Debug和Release的介绍
3. Windows环境调试介绍
3.1 调试环境的准备
3.2 调试快捷键
3.3 调试的时候查看程序当前信息
3.3.1 查看临时变量的值
3.3.2 查看内存信息
3.3.3 查看调用堆栈
3.3.4 查看汇编信息
4. 调试实例
4.1 实例1
4.2 实例2
5. 如何写出好(易于调试)的代码
5.1 优秀的代码:
5.2 assert的作用
5.3 const的作用
5.4 优秀代码示范
5.4.1 模拟实现strcpy
5.4.2 模拟实现strlen
下面这张图片是历史上的第一个bug:
bug的本意是虫子的意思,历史上第一台计算机出现问题的原因是计算机中出现了一个虫子,后来我们引申了一下,把计算机中出现的漏洞或者造成程序异常的情况我们叫做bug
第一次被发现的导致计算机错误的飞蛾,也是第一个计算机程序错误
所有发生的事情都一定有迹可循,如果问心无愧,就不需要掩盖也就没有迹象了,如果问心有愧,就必然需要掩盖,那就一定会有迹象,迹象越多就越容易顺藤而上,这就是推理的途径。
顺着这条途径顺流而下就是犯罪,逆流而上,就是真相。
每一次调试都是尝试破案的过程
调试不是一眼就能看出来的,需要我们慢慢用心去研究
我们了解了bug是什么,那么调试又是什么呢 ?
调试(Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
1.发现程序错误的存在
2.以隔离、消除等方式对错误进行定位
3.确定错误产生的原因
4.提出纠正错误的解决办法
5.对程序错误予以改正,重新测试
步骤看似简单,实则需要用心体会
Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用
Debug版本可以对程序进行调试,用来发现程序的不足之处
Release版本是发布版本,不能对程序进行调试,而是直接执行程序
这两个版本可以在程序文件下找到,可以发现,Debug版本的程序内存比Release版本的大很多
在环境中选择 debug 选项,才能使代码正常调试。
在调试中有许多可供选择的调试方式:
F5:
启动调试
经常用来直接跳到下一个断点处(配合断点使用)
F9:
创建断点和取消断点
断点的重要作用,可以在程序的任意位置设置断点。
这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去
断点使用:
我们在想要停止的语句上打断点,再按F5,程序会直接到断点处停止
也可以在循环中使用断点,并给断点附上条件,使断点到条件处停止
例如我们把断点的条件定在i == 5 时,当按F5执行程序时,程序会到断点处(i == 5 时)停止
F10:
逐过程
通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句
F11:
逐语句 ( 比逐过程更加细致 )
就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最
长用的)
CTRL + F5
开始执行不调试
如果你想让程序直接运行起来而不调试就可以直接使用
在调试开始之后,用于观察变量的值
可以通过调试中的监视功能查看变量值
在调试开始之后,用于观察内存信息
可以通过调试中的内存功能查看内存所储存的信息
通过调用堆栈,可以清晰的反应函数的调用关系以及当前调用所处的位置
栈:是一种数据结构,其特点是先进后出,这里的调用函数方式就是一种栈的存储模式,使我们清楚看见函数是被哪些函数调用的
通过查看汇编信息,可以深层理解代码结构
第一种方式:
F10调试起来后,鼠标右键选择反汇编
第二种方式:
F10调试起来后,通过调试窗口进行选择:
总结:多多动手,尝试调试,才能有进步
实现代码:求 1!+2!+3! ...+ n! ;不考虑溢出
给出如下有问题的代码:
#include
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;
}
当我们输入3时,3!应该 = 1 + 2 + 6 = 9
这时候我们输入3,期待输出9,但实际输出的是15,很显然此时Bug已经出现,这时候就需要我们进行调试
通过调试的监视功能,我们发现当我们调试到计算3的阶乘时,我们发现此时ret的值为12,也就是说程序计算的3的阶乘值是12,这时候错误就已经显现出来了。
原因是我们在计算完2的阶乘之后,ret的值没有重新赋值成1,导致3的阶乘是在2的阶乘上计算出来的,所以会对3的阶乘计算产生错误影响
解决方案:
此时只需要把计算完每次阶乘的时候,把ret赋值成1,错误就会迎面而解
改进后的代码:
#include
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;//每计算完一个数的阶乘后,重置ret的值
for (j = 1; j <= i; j++)
{
ret *= j;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
研究程序死循环的原因
给出下面代码:
#include
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;
}
我们从表面上看,数组的元素个数为10,,而在for循环中,共循环了12次访问数组内部元素,此时会对其他地方地址产生越界访问,程序因此崩溃
而当我们实际运行起来会发现,程序并没有崩溃,而是死循环打印。
当F10调试起来,通过观察当 a[11] = 0 时,i的值还是这个,而当进行arr[12] = 0操作时,此时i的值变为0
arr[11] = 0;
arr[12] = 0;
而当我们查看i的地址和arr[12]的地址时,我们会发现它们两个的地址相同,所以显而易见,当我们修改arr[12]的值时,会对内存进行非法访问,而非法访问的地址正好也是i的地址,所以会造成i又从0开始,导致程序死循环
(虽然arr[12]不在我们申请的内存中,但它表示arr[9]后面的第3个地址,我们在对arr[12]进行访问,会访问到其他未申请的地址)
总结原因:
i和arr都是局部变量,都存储在栈区中。在栈区中创建局部变量是先使用高地址处的空间 再使用低地址处的空间,所以i的地址在arr的后面,又因为for循环导致数组越界访问,正好arr[12]的地址是i的地址,所以当修改arr[12]的值时,i的值也被修改成0,循环又从头开始,因此导致程序死循环。
i和arr[12]的地址一样
1. 代码运行正常
2. bug很少
3. 效率高
4. 可读性高
5. 可维护性高
6. 注释清晰
7. 文档齐全
常见的coding技巧:
1. 使用assert
2. 尽量使用const
3. 养成良好的编码风格
4. 添加必要的注释
5. 避免编码的陷阱
首先介绍assert函数(头文件 assert.h):
assert( ) ,当()内为假时,程序会报错
assert相当于if判断更加严厉
由const修饰的变量不能被更改
const修饰指针变量的时候
1. const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。
2. const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
strcpy在拷贝字符串的时,会把原字符串中的'\0'也拷贝过去
返回值为目标字符串的首地址
char* my_strcpy(char* dest, const char* src)
{
char* ret = dest;
assert(dest && src);//断言 引用头文件
while (*dest++ = *src++)
{
;
}
return ret;
}
int my_strlen(const char* str)
{
assert(str);
int count = 0;
while (*str)
{
count++;
str++;
}
return count;
}