目录
调试(Debug):
调试的基本步骤:
Debug和Release的介绍:
几个常用的快捷键:
案例一:
案例二:
如何写出好(易于调试)的代码?
案例一:
1.assert用法
2.const用法
案例二:
编程常见的错误
调试(Debug):
又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
调试的基本步骤:
- 发现程序错误的存在;
- 以隔离,消除等方式对错误进行定位;
- 确定错误产生的原因;
- 提出纠正错误的解决办法;
- 对程序错误予以改正,重新测试
Debug和Release的介绍:
Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户更好的使用。
二者区别:
- 从生成文件的大小来看:Debug的文件比Release的文件大;
- Debug可以用于代码的调试,而Release不能;
- Debug不对代码进行优化,而Release则对代码进行优化
几个常用的快捷键:
- F5:启动调试,经常用来直接跳到下一个断点处,常和F9搭配使用;
- F9:创建断点和取消断点。断点的作用:可以在程序的任意位置设置断点。
- F10:逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句;
- F11:逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们进入函数内部,这也是最长用的一个快捷键;
- CTRL + F5:开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。
案例一:
实现代码:求1!+2!+3!+...+n!的值
先思考如何用代码实现求n!
int main()
{
int n = 0;
printf("请输入一个正整数n:");
scanf("%d",&n);
int i = 0;
int ret = 1;
for (i = 1; i<=n; i++)
{
ret = ret * i;
}
printf("%d\n",ret);
return 0;
}
通过调试可以发现,ret=1*2*3=6,代码运行正确,程序逻辑没有问题
接着我们试着去求1!+2!+3!+...+n!
int main()
{
int n = 0;
scanf("%d",&n);
int i = 0;
int j = 0;
int sum = 0;
int ret = 1;
for (i = 1; i <= n; i++)
{
for (j = 1; j <= i; j++)
{
ret *= j;
}
sum += ret;
}
printf("%d\n",sum);
return 0;
}
当n=4时,通过运行程序可以发现结果却不等于33,那问题出在哪?通过进一步调试可以发现,当我们在求3!的阶乘时,ret的值本该等于6,但是最后的结果却是12,可想而知,问题应该是出现在求一个数的阶乘上。当我们在求2!时,ret的值等于2,但是当n++变为3的时候,我们发现ret的依旧等于2,此时的j=1<i=3,所以ret会继续*1*2*3,也就是将值变为了2*1*2*3=12,所以这才是ret=12的由来。可见,在求一个数的阶乘时,ret的值并没有初始化为1,而是将上一次运行时的结果累乘到本次运算上,进而导致问题的出现。我们只要在对某个数求阶乘之前,将ret初始化为1即可避免问题的发生。
int main()
{
int n = 0;
scanf("%d",&n);
int i = 0;
int j = 0;
int sum = 0;
int ret = 1;
for (i = 1; i <= n; i++)
{
ret = 1;
for (j = 1; j <= i; j++)
{
ret *= j;
}
sum += ret;
}
printf("%d\n",sum);
return 0;
}
改进版:
int main()
{
int n = 0;
scanf("%d",&n);
int i = 0;
int sum = 0;
int ret = 1;
for (i = 1; i <= n; i++)
{
ret *= i;
sum += ret;
}
printf("%d\n",sum);
return 0;
}
案例二:
研究程序死循环的原因
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;
}
通过调试可以发现,数组中的元素是按地址连续存放的,即使是越界的三个元素,也是连续存放的。同时,我们可以发现变量i和arr[12]的地址竟是相同的,说明它们是共用一块内存空间。因此我们猜测,在对i的值进行修改时也会导致arr[12]的变化。
数组下标即便是越界的,在运行到arr[10]和arr[11]时, 同样会将其初始化为0。另外,我们发现,arr[12]的值确实是随着i进行变化的,说明我们的猜测是正确的。
当i=12时,将arr[12]的值也为12。但是,当运行到arr[12]=0时,i的值也在此刻同时变化为0。此时i的值为0依旧满足条件,所以又将进行下一轮循环。
这里面是有原因的,当然也有一定程度的巧合
- i 和arr 是局部变量,而栈区一般存放的是局部变量以及函数参数
- 栈区的使用习惯:先使用高地址处的空间,再使用低地址处的空间
- 数组随着下标的增长,地址是由低到高变化的
所以arr和i在栈区的空间布局 ,应该是如下图所示:
那如何避免死循环的发生?
- 先定义arr数组再定义 i;
- 控制循环次数,i<=9
需要注意的是,不同编译器下局部变量 i 和 arr 在内存中的布局是不同的:
- 在vc6.0中i和arr数组之间没有空隙,即i<=10就陷入死循环
- 在gcc中i和arr数组之间空出一个int空间,即i<=11就陷入死循环
最后, Release会对代码进行优化使之不会进入死循环。所以,上面的程序在Release下运行是不会陷入死循环的,它会直接打印13遍“hehe”。
如何写出好(易于调试)的代码?
优秀的代码:
- 代码运行正常;
- bug很少;
- 效率高;
- 可读性高;
- 可维护性高;
- 注释清晰;
- 文档齐全
常见的coding技巧:
- 使用assert;
- 尽量使用const;
- 养成良好的编码风格;
- 添加必要的注释;
- 避免编码的陷阱
案例一:
模拟实现库函数strcpy
头文件:string.h
原型声明:char *strcpy(char* dest, const char *src)
功能:把从src地址开始且含有NULL结束符的字符串复制到以dest开始的地址空间(连同字符串串结束标志’\0’也一并拷贝)
初阶版:
void my_strcpy(char* dest, char* src)
{
while (*src!='\0')
{
*dest = *src;
src++;
dest++;
}
*dest = *src;//把\0拷贝进去
}
改进版:
char* my_strcpy(char* dest,const char* src)
{
assert(dest&&src);//断言:如果表达式dest != NULL为假就会报错
char* ret = dest;//保存目标空间的地址
//注意:字符'\0'就是数组0
while (*dest++ = *src++)
{
;
}
return ret;//返回目标空间的地址
}
通过比对初阶版和改进版的区别,对知识点进行下列总结:
1.assert用法
assert是个宏,并非是个函数。assert 宏的原型定义在 assert.h 中,其作用是如果它的条件返回错误,则终止程序执行。
#include "assert.h"
void assert( int expression );
assert 的作用是计算表达式 expression ,如果其值为假(即为0),那么它先向 stderr 打印一条出错信息,然后通过调用 abort 来终止程序运行。
使用 assert 的缺点是,频繁的调用会极大的影响程序的性能,增加额外的开销。
assert是一个调试程序时经常使用的宏,在程序运行时它计算括号内的表达式,如果表达式为false(0),程序将报告错误并终止执行。如果表达式不为0,则继续执行后面的语句。这个宏常用来判断程序中是否出现了明显非法的数据,如果出现了则终止程序以免导致严重后果,同时也便于查找错误。
assert只有在 Debug 版本中才有效,如果编译为 Release 版本则被忽略。
2.const用法
const放在*的左边(const int* p)
const修饰的是*p,表示p指向的对象不能通过p来修改,但是p变量中的地址是可以改变的
const int*p=int const*p
int main()
{
const int num = 10;
const int* p = #//加上const之后,值就不能进行修改
//int const* p = #
int n = 100;
p = &n;//可以修改
//*p = 20;//不能进行修改
}
const放在*的右边(int* const p)
const修饰的是p,表示p的内容不能被改变,但是p指向的对象是可以通过p来改变的
int main()
{
const int num = 10;
int* const p = #//const限制的是p,p不可以进行修改
*p = 200;//可以修改
int n = 100;
//p = #//不能修改
}
案例二:
模拟实现库函数strlen
#include<assert.h>
int my_strlen(const char* str)
{
int count = 0;
assert(str);
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
int len = my_strlen("abcdef");
printf("%d\n",len);
return 0;
}
编程常见的错误:
常见的错误分类:
- 编译型错误:直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。
- 链接型错误:看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。
- 运行时错误:借助调试,逐步定位问题。最难搞。