- 什么是bug?
- 调试是什么?
- Debug 和 Release 的介绍
- windows 环境调试介绍
- 一些调试的实例
- 如何写出好(易于调试)的代码
- 编程常见的错误
1. 什么是bug?
bug指的是第一次被发现的导致计算机错误的飞蛾,也是第一个计算机程序错误。英文释义是“虫子”,由此去除bug也变成了“除虫”,即debug(调试)。
2. 调试是什么?
调试指发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
调试的基本步骤:
- 发现程序错误的存在
- 以隔离、消除等方式对错误进行定位
- 确定错误产生的原因
- 提出纠正错误的解决办法
- 对程序错误予以改正,重新调试
3. Debug 和 Release 的介绍
Debug 通常称为调试版本,它包含调试信息,并且不做任何优化,便于程序员调试程序。
Release 称为可发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便于用户很好的使用。
举例:
在工作中,程序员常用的是Debug版本,而测试人员测试用的是Release版本,有可能两个版本运行有差异。
4. windows 环境调试介绍
学会各种调试快捷键:(常用如下举例)
F9:设置或取消断点
F5:启动调试
F10:逐过程
F11:逐语句
CTRL+F5:开始执行不调试
Fn: 辅助功能键
其它常用快捷键,参考博客:VS中常用的快捷键_vs快捷键_MrLisky的博客-CSDN博客
当我们调试起来之后,通过调试窗口来查看。比如,断点查看、监视窗口查看、自动窗口查看(会自动把某一个值添加进去,一直在,通常用的是监视窗口)、局部变量窗口、内存窗口、反汇编窗口(右击鼠标也可)、寄存器窗口、堆栈窗口等。
举例:
调用堆栈窗口:
如果你想看函数的调用逻辑是咋样的,看调用堆栈窗口
5. 一些调试的实例
1)调试断点的选择
如果碰到一个循环,循环变量i要循环1000次,你知道是在500次左右出现了问题,那么你应该怎么设置断点呢?总不可能从0次一直跑到500次吧......这里就需要对断点进行设置了。
具体步骤:先点击断点,然后右击,选择条件,进行设置i==500。举例:
2)运行是错误
求阶乘1!+2!+3!的和,编译器没有检查出相关的错误,但是输出的结果是15
遇到这种错误,第一步就是设置断点进行调试。经过排查发现是ret *=i; 这行代码出现了错误,即每次循环时,ret用的是上次遗留下来的值,需要对ret每次进行初始化,具体如下:
针对这种错误,我们应该有一个预期,就是事先在心里大致知道程序走到这一步应该是一个什么样子。当我们调试时发现不符合预期,就找到这个错误了。
3)代码为什么死循环?
举例:
分析死循环的原因:
这是巧合吗?
有一定的巧合性。i和arr是局部变量,局部变量是放在栈区上的。栈区内存的使用习惯是:先使用高地址空间,再使用低地址空间。数组随着下标的增长地址,是由低到高变化的。如果越界,从低地址到高地址,恰好遇到i这个局部变量在栈区的高地址位置上,就出现了死循环。
如果将 i <= 11,就会出现程序崩溃;如果将 i <= 13,也会出现死循环。
那从arr[9] 到arr[12]中间空了两格,请问是预留的还是释放出来的?
这个是由编译器决定的。经测试,在VC6.0环境-----0个整形;在gcc环境-----1个整形;在VS2013~2019环境-----2个整形
6. 如何写出好(易于调试)的代码
优秀的代码:
- 代码运行正常
- bug很少
- 效率高
- 可读性高
- 可维护性高
- 注释清晰
- 文档齐全
常见的编程技巧:
- 使用assert
- 尽量使用const
- 养成良好的编码风格
- 添加必要的注释
- 避免编码的陷阱
举例1:
模拟实现库函数:strcpy
strcpy:字符串拷贝函数,在拷贝的时候,还会包括字符串后面的\0
#include <string.h>//得引头文件
int main()
{
char arr1[20] = "xxxxxxxxxxxxx";
char arr2[] = "hello";
strcpy(arr1,arr2);
return 0;
}
现在要模拟实现,创建函数my_strcpy
#include <string.h>
//方法1
/*
void my_strcpy(char* dest, char* src)
{
while (*src != '\0')
{
*dest = *src;
dest++;
src++;
}
*dest = *src;
}
*/
//方法2
/*
void my_strcpy(char* dest, char* src)
{
while (*src != '\0')
{
*dest++ = *src++;
}
*dest = *src;
}
*/
//方法3
void my_strcpy(char* dest, char* src)
{
while (*dest++ = *src++)
//等到传输到\0的时候,\0的ASCII值为0,0为假
{
;
}
}
//多看多读大师的代码,多看多读源码
int main()
{
char arr1[20] = "xxxxxxxxxx";
char arr2[] = "hello";
my_strcpy(arr1, arr2);
//strcpy(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
常见的能运行了之后,开始优化my_strcpy函数,向源码靠拢,避免出现特殊情况
strcpy (&x1,&x2); &x1是目标空间的起始地址,&x2是源空间的起始地址
改进1:避免导如的时候是空指针,需要在函数中先判断一下
char* my_strcpy(char* dest, char * src)
{
assert(src != NULL);//断言
assert(dest != NULL);//断言
//断言的作用就是如果发生错误,在调试后,如果程序崩溃,
//会指出具体哪一行的错误,减少因错误再回头调试找错误点浪费时间
char* ret = dest;
while (*dest++ = *src++)
{
;//hello的拷贝
}
return ret;//返回目标空间的起始地址
}
改进2:避免在main()函数中,使用my_strcpy(x1,x2)的两个成员x1,x2位置不小心被填写反了
添加const ,如果此时的位置错误,调试就会进行不下去,报错
把src指向的内容拷贝放进dest指向的空间中,从本质上讲,希望dest指向的内容被修改,src指向的内容不应该被修改。
知识补充
const
const 修饰变量,这个变量就被称为常变量,但是本质上还是变量
举例:
但是,这里就有存在一个问题了,num虽然不会再被修改了,但是p的地址也存在呀, p依然可以被修改。故,当const修饰指针变量的时候,const 如果放在*的右边,修饰的是指针变量p,表示指针变量不能被改变,但是指针指针的内容,可以被改变。
这里小结一下:
//const 修饰变量,这个变量就被称为常变量,不能被修改,但是本质上还是变量
const int num = 10;
num = 20;//err,num被const修饰,其值无法被改变
//***************************************************************************//
int* const p = #//const 修饰了指针p,指针p未加*,表示的是地址,
//故指针地址不能被改变
//值可以被改变
int n = 100;
//const修饰指针变量的时候
//const 如果放在*的右边,修饰的是指针变量p,表示指针变量不能被改变
//但是指针的内容,可以被改变
*p = 20;//ok,指针的内容可以被改变
p = &n;//err,指针的地址无法被改变
//***************************************************************************//
const int* p = #//const 修饰了*p,指针p加*,表示的是值,
//故指针指向的值不能被改变
//地址可以被改变
int n = 100;
//const修饰指针变量的时候
//const 如果放在*的左边,修饰的是*p,表示指针指向的内容,是不能通过指针来改变的
// 但是指针变量本身是可以修改的
*p = 20;//err
p = &n;//ok
当我们创建的这个my_strcpy函数,我们将src的值传到目标dest中,使之发生改变,既然改变了,我们也要看到这种改变,所以,my_strcpy还需要加上一个返回值return,此时是字符串char类型的,所以返回值也要是char型的。
具体修改好的最终my_strcpy函数如下:
char* my_strcpy(char* dest, const char * src)
{
assert(src != NULL);//断言
assert(dest != NULL);//断言
char* ret = dest;
while (*dest++ = *src++)
{
;//hello的拷贝
}
return ret;//返回目标空间的起始地址
}
int main()
{
char arr1[20] = "xxxxxxxxxxx";
char arr2[] = "hello";
//1. 目标空间的起始地址,2. 源空间的起始地址
printf("%s\n", my_strcpy(arr1, arr2));
//链式访问
//因为my_strcpy有返回值,才可以链式返回
return 0;
}
举例2:
模拟实现库函数:strlen (求字符串长度)
int my_strlen(const char* ret)
//虽然不需要改变ret的值,但是添加const能够增加
//数值健壮性和鲁棒性
{
//assert(ret != NULL);
assert(ret);//也可以这样做,更好
int count = 0;
while (*ret != '\0')
{
ret++;
count++;
}
return count;
}
//也有争议,如果为负数咋办,但是这个用int是完全可以的
//size_t my_strlen(const char* ret)
虽然不需要改变ret的值,但是添加const能够增加
数值健壮性和鲁棒性
//{
// //assert(ret != NULL);
// assert(ret);//也可以这样做,更好
// size_t count = 0;
// while (*ret != '\0')
// {
// ret++;
// count++;
// }
// return count;
//}
int main()
{
char ret[] = "hello";//字符串怎么表示?
//用数组来表示字符串
printf("%d\n",my_strlen(ret));//怎么求解
return 0;
}
如何在VS编辑器上找到函数的源代码(仅供参考的一组比较优秀的代码):
找到电脑上VS的安装位置,直接搜相关的函数名即可(也可以用everything软件来搜)
7. 编程常见的错误
1)编译型错误
直接 看错误提示信息(双击),解决问题,或者凭借经验就可以搞定,相对来说简单。
2)链接型错误
看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在,一般是标识符不存在或者拼写错误。一般直接去查找错误的符号。
3)运行时错误
运行没问题,但结果出错。要借助于调试,逐步定位问题,最难搞。
以后每次遇到错误的时候,做一个有心人,积累错误经验!