目录
什么是bug?
调试是什么?有多重要?
debug和release的介绍
windows环境调试介绍、
一些调试实例
如何写出(易于调试)的代码
编程常见的错误
什么是bug?
其实bug在英文翻译中有表示臭虫的含义,因为第一次被发现的导致计算机程序错误的是飞蛾,也是第一个计算机程序错误。
调试是什么?有多重要?
写代码就是破案的过程,错误都是有迹可循的。迹象越多就越容易顺藤而上,这就是推理的途径。
发现错误-->调试-->解决错误
2.1调试的基本步骤:
发现程序错误的所在
以隔离、消除等方式对错误进行定位
确定错误产生的原因
提出错误的解决办法
对程序错误予以改正、重新测试
Debug和release的介绍(掌握)
Debug通常称为 调试版本,它包含调试信息,并且不做任何优化,便于程序员调试。
Release称为 发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便于用户很好的使用。
Debug和Release的区别就是Release的速度更快,所占的内存更少,但是Release不能调试,Debug可以进行调试。
windows环境调试介绍
3.1调试环境的准备
在环境中选择debug选项,才能使代码正常调试。
3.2调试快捷键
1.F5 - 开始调试
2.ctrl + F5 开始执行(不调试)
3.F9 设置断点/取消断点
4.F10 逐过程
5.F11 逐语句
使用Fn辅助功能键
F10和F11有什么区别呢?
他们在执行的过程F10是逐过程进行的,F11是逐语句进行的,所以在函数的调用时,F11可以更加细节的进入到函数的内部进行逐语句的调试。
F5是和F9配合使用的
F9是设置断点,F5是开始执行并跳到第一个断点
举例:
光标放到16行,按Fn+F9,我们就在16行加了一个断点,意思是F5开始调试直到断点处停止调试。
按F5开始调试
得到了我们的程序执行结果
在以上的基础上继续按F5开始调试结果会是什么样的呢?
在循环里面的断点,每次循环都会继续停到断点处。
抛开上面我们所说的,依然是下面的程序:
如果循环的次数很大,达到几百上千时,而我们的错误刚好在第400次循环时该怎么办呢?
在VS编译器中我们也可以设置断点。
如果我们不明白条件断点,我们也可以在代码中加上一条条件语句,之后将断点打在条件语句处,如果满足条件则断点被触发。
3.3调试时查看当前程序的信息
如果我们想在调试过程中观察i的值,点击调试->窗口->自动窗口
在我们按F10进行逐过程调试时,自动窗口会自动的显示我们在这个过程中,当前程序的信息。
如果我们想在调试过程中观察程序的局部变量的变化,点击调试->窗口->局部变量
如果我们想在调试过程中监视任何变量,点击调试->窗口->监视
监视不仅可以监视变量的值,也可以监视变量的地址,表达式的值
内存
调用堆栈
就是数据结构里面的栈。
反映数据的调用逻辑。
调试实例
求1!+2!+......+10!
int main()
{
int n = 0;
int sum = 0;
scanf("%d", &n);
int ret = 1;
for (n = 1; n <= 10; n++)
{
for (int i = 1; i <= n; i++)
{
ret *= i;
}
sum += ret;
}
for (int i = 1; i <= n; i++)
{
ret *= i;
}
printf("%d\n", sum);
return 0;
}
经过计算,我们发现我们的代码是错误的,错误在于我们每一次循环没有及时修改ret的值为1。
但是如果我们不能一眼看出来错误在哪里,还是要依据调试来解决问题的。
1.在监视窗口中添加我们想要监视的变量
2.F11逐语句并且观察监视窗口中变量的值。
我们在调试的过程中发现ret在执行过程中,继续进行阶乘运算时,初始值不是1,这就是我们通过调试发现的问题所在,经过修改,代码如下:
int main()
{
int n = 0;
int sum = 0;
//scanf("%d", &n);
int ret = 1;
int i = 0;
for (n = 1; n <= 10; n++)
{
ret = 1;
for (int i = 1; i <= n; i++)
{
ret *= i;
}
sum += ret;
}
for (int i = 1; i <= n; i++)
{
ret *= i;
}
printf("%d\n", sum);
return 0;
}
如何写出(易于调试)的代码
代码运行正常
bug很少
效率高
可读性高
可维护性高
注释清晰
文档齐全
常见coding技巧
使用assert
尽量使用const
养成良好的编码风格
添加必要的注释
避免编码的陷阱
示例:模拟实现strcpy函数
void my_strcpy(char* dest, char* scr)
{
while (*scr != '\0')
{
*dest = *scr;
dest++;
scr++;
}
*dest = *scr;//'\0‘的拷贝
}
int main()
{
char arr1[20] = "hello world";
char arr2[40] = { 0 };
my_strcpy(arr2, arr1);
printf("%s\n", arr2);
return 0;
}
但是在我们看来这样的代码并不是一个好的代码,我们可以继续将代码进行优化。
my_strcpy函数部分我们可以将它改为以下形式
void my_strcpy(char* dest, char* scr)
{
while (*scr != '\0')
{
*dest++ = *scr++;
//dest++;
//scr++;
}
*dest = *scr;//'\0‘的拷贝
}
除了上述的代码形式外,我们还可以将代码改为以下形式:
void my_strcpy(char* dest, char* scr)
{
while (*dest++ = *scr++)
{
;
}
}
int main()
{
char arr1[20] = "hello world";
char arr2[40] = { 0 };
my_strcpy(arr2, arr1);
printf("%s\n", arr2);
return 0;
}
但是我们在传递参数的时候不能够保证我们传递过来的指针是否为空指针,也不能保证指针的有效性,所以在这个时候我们就要使用assert断言,来保证指针的有效性。
为什么使用assert断言?
如果在传参的这个过程中,我们真的传递了一个空指针,或者说是一个无效的指针,那么使用断言就会提示我们这样的问题所在,但是如果你没有使用断言来判断指针的有效性,整个程序运行起来的最终结果就会崩掉,并且同时我们也不会知道它为什么会运行失败。
#include <assert.h>
void my_strcpy(char* dest, char* scr)
{
//assert(dest != NULL);
//assert(scr != NULL);
assert(dest && scr);
while (*dest++ = *scr++)
{
;
}
}
int main()
{
char arr1[20] = "hello world";
char arr2[40] = { 0 };
my_strcpy(arr2, arr1);
printf("%s\n", arr2);
return 0;
}
当然断言不仅仅是应用于指针的判断,断言是依据assert后面表达式真假而言的。对于我们程序员来讲是一个很好的编程习惯。
other:
我们在C语言的库中发现strcpy函数和我们自己写的my_strcpy函数有所出入。详细请看下图:
使用const修饰和不使用const又有什么区别呢?
const修饰指针的作用:
const放在*的左面,const int* p或者int const* p,const放在*的左面限制的是*p而不是p。
*p不能改,是p指向的内容;但是p可以改,p可以指向其他变量
int main()
{
const int m = 0;
int n = 0;
//m = 20;err
const int* p = &m;
//*p = 20;错误
p = &n;//ok
return 0;
}
const放在*的右面,int* const p ,如果const放在*右边,限制的是p而不是*p,*p可以改,p不可以改。
int main()
{
int m = 0;
int n = 0;
int* const p = &m;
*p = 20;//ok
p = &n;//no
return 0;
}
在我们strcpy中是如何使用的呢?
如果我们将源头和目的地写反,就出现了一个bug,这个时候使用const修饰char* scr就可以避免这个错误。提高了代码的健壮性。
#include <assert.h>
void my_strcpy(char* dest, const char* scr)//意思是*scr不能被改变
{
//assert(dest != NULL);
//assert(scr != NULL);
assert(dest && scr);
while (*dest++ = *scr++)
{
;
}
}
int main()
{
char arr1[20] = "hello world";
char arr2[40] = { 0 };
my_strcpy(arr2, arr1);
printf("%s\n", arr2);
return 0;
}
#include <assert.h>
char* my_strcpy(char* dest, const char* scr)//意思是*scr不能被改变
{
//strcpy函数返回的是目标空间的起始地址
assert(dest && scr);
char* ret = dest;//记住起始空间的地址
while (*dest++ = *scr++)
{
;
}
return ret;
}
int main()
{
char arr1[20] = "hello world";
char arr2[40] = { 0 };
//链式访问
printf("%s\n", my_strcpy(arr2, arr1));
return 0;
}
编程常见的错误
6.1编译型错误
例如:缺少分号等语法错误,可以直接查看错误信息,解决问题。
6.2链接型错误
例如:拼写错误,或者标识符不存在提示有无法解析的外部命令。
主要在代码中找到错误信息的标识符,然后定位问题所在。
ctrl+f--搜索
6.3运行时错误
借助调试,逐步定位问题。
感谢阅读,欢迎大家批评指正!