- 什么是bug?
- 调试是什么?有多重要?
- debug和release的介绍
- windows环境调试介绍
- 一些调试的实例
- 如何写出好(易于调试)的代码
- 编程常见的错误
1.什么是bug?
第一次被发现的导致计算机错误的飞蛾,也是第一个计算机程序错误。
后来把导致计算机出现问题的错误称之为bug
2.调试是什么?有多重要?
所有发生的事情都有一定有迹可循,如果问心无愧,就不需要掩盖也就没有迹象了,如果问心无愧,就必然需要掩盖,那就一定会有迹象,迹象越多就越容易顺藤儿上,就是推理的途径
顺着这条途径顺流而下就是犯罪,逆流而上,就是真相
一名优秀的程序员是一名出色的侦探
每一次调试都是尝试破案的过程
拒绝迷信式调试!!!
2.1调试是什么?
调试,又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
调试的前提:已经知道程序应该怎么执行,然后验证自己的想法,当前的程序是不是按照自己的想法在走
2.2调试的基本步骤?
- 发现程序错误的存在
- 以隔离、消除等方式对错误进行定位
- 确定错误产生的原因
- 提出纠正错误的解决办法
- 对程序错误予以改正,重新测试
2.3Debug和Release的介绍
Debug通常称为调试版本,它包含调试信息,并且不做任何优化,便于程序员调试程序
Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用
Debug版本是程序员用于寻找错误的版本
Release版本是用户使用的版本,没有调试信息
3.windows环境调试介绍
3.1调试环境的准备
在环境中选择debug选项,才能是代码正常调试
3.2学会快捷键
F5
启动调试,经常用来直接跳到下一个断点处
当程序中没有设置断点时,程序就会一步一步的往下走直到程序运行结束为止
此时的效果跟用CTRL+F5的效果是一样的
通常F5是和F9一起搭配使用
F9
创建断点和取消断点(按一下设置断点,再按一下取消断点)
断点的重要作用,可以在程序中的任意位置设置断点
这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去
下端代码中假设已经知道赋值是没有问题的,问题可能出现在打印处,所以在第15行处设置断点,按F5就能跳过前面的代码直接到断点处
再按一次F5并不能跳到下一个断点处,按一下F5它会跑到逻辑上的下一个断点
取消断点,再按F5才能到下一个断点或者右击鼠标有一个禁用断点
假设我们知道打印qian5次是没有问题,此时我们可以设置条件断点
此时为条件断点,只有当i==5时才会触发这个断点,按F5此时i就是5
F10
逐过程,通常用来处理一个过程可以是一次函数调用,或者是一条语句
F11
逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最常用的)
F10和F11在普通语句上是没有什么区别的,在函数调用上是有区别的
F11能够进入函数内部观察细节,而F10认为被调用的函数是一条语句,直接到下一条语句
CTRL+F5
开始执行不调试,如果想要程序直接运行而不调试就可以直接使用
3.3调试的时候查看程序当前信息
调试开始后,窗口中才能看到上面的选项
3.3.1.查看临时变量的值
在监视中虽然是我们自己手动输入,但是我们想观察什么就观察,像自动窗口和局部变量这两个观察东西都不太方便
想要看a中4个元素的内容写成a,4
3.3.2查看内存信息
3.3.3查看调用堆栈
代码之间进行互相调用,观察代码之间是如何相互调用的
查看调用堆栈,就很容易看出函数之间是如何调用的
数据结构中的栈:
压栈
出栈
调用堆栈反映函数调用逻辑,在上面代码执行时,调用的函数依次放到栈区,释放时,从栈顶处先释放
栈是一种先进后出,后进先出的数据结构
在很多行代码中,函数之间的互相调用,直接浏览代码查看非常麻烦,借助调用堆栈就变得清晰明了
3.3.4查看汇编信息
在不同的编译器中汇编是不一样的,所以不会汇编没事,只需要知道当我们要找汇编信息时,我们应该在哪里找,分析一些简单的流程
出了在调试窗口中找反汇编,右击鼠标也能找到反汇编
3.3.5查看寄存器信息
像上面的eax、ebx就是寄存器
在调试窗口中能够找到寄存器,以及右击鼠标也能找到寄存器
4.多多动手,尝试调试,才能有进步
- 一定要熟悉掌握调试技巧
- 初学者可能80%的时间在写代码,20%的时间在调试,但是程序员可能20%的时间在写程序,但是80%的时间在调试
- 这篇文章所写都是一些简单的调试,以后可能会出现很复杂调试场景;多线程程序的调试等
- 多多使用快捷键,提升效率
5.一些调用实例
5.1 实例1:
计算3!=1+2+6=9,但是此时打印出15
这里我们就得找我们的问题
1.首先推测问题出现的原因。初步确定问题的原因
2.实际上手调试很有必要。
3.调试的时候我们心里有数
当我们调试的狮虎发现前两次的计算是正确的,但是第三次ret=12了,所以3的阶层算出来时15,也就是本来应该时6,但是现在x2了,也就是上一次的ret=2遗留下来,所以在每次使用ret之前都要重新等于1;
更改:
在第二个for循环之前加上一个ret=1;
5.2 实例2:
#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;
}
上面的代码陷入了死循环
经过调试我们发现当arr[12]的值被改为0的时候,i的值也被改为0,arr[12]的值在变的同时i的值也在变,通过&i和&arr[12]的地址我们发现它们共用同一块空间
6.如何写出好(易于调试)
6.1 优秀的代码
1.代码运行正常
2.bug很少
3.效率高
4.可读性高
5.可维护性高
6.注释清晰
7.文档齐全
常见的coding技巧
1.使用assert
2.尽量使用const
3.养成良好的编码风格
4.添加必要的注释
5.避免编码的陷阱
6.2示例:
模拟实现strcpy
将*strSource中的数据放到*strDestination,strcpy在拷贝字符串的时候,会把源字符串中的\0也拷贝过去
第一次改进
但是此时还是有缺陷,这里传参的时候,可能会将空指针传过来,所以我们需要断言一下
第二次改进
但是这里还是有缺陷有的人可能不小心将dest的内容传到src中
第三次改进,在char*src前面用const修饰,src的内容就不能被更改
注意:在库函数中,strcpy返回的是char*,所以还要再修改一下
第四次修改
为什么返回char*类型?
是为了实现链式访问
strcpy函数返回的是目标空间的起始地址
注意:
1.分析参数设计(命名,类型)。返回类型的设计
2.这里讲解野指针,空指针的危害
3.assert的使用,这里介绍assert的作用
4.参数部分从上图的使用,这里讲解const修饰指针的作用
5.注释的添加
6.3const的作用
num前被const修饰num的值就不能被更改
但是此时采用指针的方式可以将num的值更改,但是我们不想要num的值被更改,我们应该怎么办呢?
此时在*p前加const,*p的值就不能被更改了
const修饰指针变量
- const放在*的左边
const int*p=#
int const *p=#
上面这两种的写法是等价的 ,此时*p的值是不允许更改
意思是:p指向的对象不能通过p来更改了,但是p变量本身的值是可以改变的
const int num=10;
const int*p=#
int n=100;
p=&n;//此时可以更改p的值
printf("%d\n",num);
- const放在*的右边
意思是:p指向的对象是可以通过p来改变的,但是不能修改p变量本身的值
#include<stdio.h>
int main()
{
const int num=10;
int* const p=#
*p=0;//ok
int n=100;
p=&n;//err
}
7.编程常见错误
7.1编译型错误
直接看错误提示信息,解决问题,或者凭借经验就可以搞定,相对来说简单
语法错误是再编译期间发现的问题,也是编译错误
7.2链接型错误
看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标示符名不存在或者拼写错误
比如:LNK201 无法解析的外部符号
一般是看报错给的名字,可能是没引调用函数的头文件,也可能引用函数的头文件了,但是名字拼写错误(注意:并且大小写要一致)
声明:
extern 函数的类型名 函数名 ((函数参数的类型)函数参数);//引用另一个文件时用extern
7.3运行时错误
借助调试,桌布定位问题。运行错误比较难搞。
通常时借助于调试解决的
tip:
release版本和debug版本优化也有所差异
debug版本会陷入死循环
而release版本会做相应的优化
这个程序和环境有很大的关系
release版会做相应的优化
release版本将内存开辟顺序发生了改变