目录
调试(Debug):
调试的基本步骤:
Debug和Release的介绍:
几个常用的快捷键:
案例一:
案例二:
如何写出好(易于调试)的代码?
案例一:
1.assert用法
2.const用法
案例二:
编程常见的错误
又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户更好的使用。
二者区别:
实现代码:求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
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依旧满足条件,所以又将进行下一轮循环。
这里面是有原因的,当然也有一定程度的巧合
所以arr和i在栈区的空间布局 ,应该是如下图所示:
那如何避免死循环的发生?
需要注意的是,不同编译器下局部变量 i 和 arr 在内存中的布局是不同的:
最后, Release会对代码进行优化使之不会进入死循环。所以,上面的程序在Release下运行是不会陷入死循环的,它会直接打印13遍“hehe”。
优秀的代码:
常见的coding技巧:
模拟实现库函数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;//返回目标空间的地址
}
通过比对初阶版和改进版的区别,对知识点进行下列总结:
assert是个宏,并非是个函数。assert 宏的原型定义在 assert.h 中,其作用是如果它的条件返回错误,则终止程序执行。
#include "assert.h"
void assert( int expression );
assert 的作用是计算表达式 expression ,如果其值为假(即为0),那么它先向 stderr 打印一条出错信息,然后通过调用 abort 来终止程序运行。
使用 assert 的缺点是,频繁的调用会极大的影响程序的性能,增加额外的开销。
assert是一个调试程序时经常使用的宏,在程序运行时它计算括号内的表达式,如果表达式为false(0),程序将报告错误并终止执行。如果表达式不为0,则继续执行后面的语句。这个宏常用来判断程序中是否出现了明显非法的数据,如果出现了则终止程序以免导致严重后果,同时也便于查找错误。
assert只有在 Debug 版本中才有效,如果编译为 Release 版本则被忽略。
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
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;
}
常见的错误分类: