5.一些调试的实例
注
前面我们讲过,在VS的编译器里,scanf会被认为是一个不安全的函数,会推荐你使用scanf_s函数。不过前面的文章中也说过,scanf_s是VS编译器自己的函数,在别的编译器上是不存在这个函数的。如果就想要使用scanf函数只需要在最开头定义一句话即可:#define_CRT_SECURE_NO_WARNINGS
但还是有细心的同学发现,即使定义了那句话(关闭安全检查)、代码跑过去了但还是有一个警告
返回值被忽略,虽然这个警告影响不大,但能消除一个就消除一个。可以使用 :
#pragma warning(disable:6031),即忽略掉6031号警告
这句话可以让编译器忽略指定编号的警告,跳过警告直接运行程序,可用来忽略一部分不重要的警告报错(一部分、不重要!!!)
-- -- -- -- -- -- -- -- -- -- -- -- -- -- --
实例一
实现代码:求 1!+2!+3! ...+ n! (不考录溢出)
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:6031)
#include <stdio.h>
int main()
{
int i = 0;
int sum = 0;//保存最终结果
int n = 0;
int ret = 1;//保存n的阶乘
scanf("%d", &n);
for (i = 1; i <= n; i++)
{
int j = 0;
for (j = 1; j <= i; j++)
{
ret *= j;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
运行结果:
输入3
输出15
但自己简单口算一下就可以发现,3*2*1+2*1+1=9,显然程序是存在错误的,直接看不好看出Bug来时,就可以使用一步一步调试来处理了
我们先Fn+F10逐过程开始调试。调试栏——>窗口——>监视,随便打开一个监视窗口,分别对变量i、n、j、ret和sum进行监视
我们继续Fn+F10逐语句调试,同时仔细关注监视中变量的值的变化
可以发现,在i=1,j=2(跳出内层循环,去sum累加)时,ret=1,sum=1(1的阶乘就是1,计算正确)
继续调试,在i=2,j=3(跳出内层循环)时,ret=2,sum=3(1的阶乘加2的阶乘就是3,计算正确)
继续调试,其实在i=3,j=3时就已经可以看出错误了,看看ret的值居然是12!这当然不对,3!=6呀。如果ret=12的话,再加上上一次计算的sum=3不就得到错误答案12+3=15了吗
在i=3,j=4时(跳出内层循环),ret=12,sum=15
到这里就可以打出结论,错误的原因是因为ret中保留了上一次计算的值,因此我们应该在每次使用之前对其初始化一次,即在for循环中添加ret=1
- 首先推测问题出现的原因。初步确定问题可能的原因最好
- 实际上手调试很有必要
- 调试的时候我们心里有数
-- -- -- -- -- -- -- -- -- -- -- -- -- -- --
实例二
在前面的章节中(本章第一节)说过,这个代码在Debug环境下是会发生死循环的(不报错的话)
#include <stdio.h>
int main()
{
int i = 0;
int arr[10] = {0};
for(i=0; i<=12; i++)
{
arr[i] = 0;
printf("shizhanglao\n");
}
return 0;
}
现在使用逐语句调试再次验证
首先我们来看变量i和数组arr的地址,可见确实是i创建在arr的前面,向从低地址向高地址访问数组确实是有可能把i的值改为0从而陷入死循环的
小结:
在监视中可以发现,arr[10],arr[11]都是随机值,arr[12]中是12,且在arr[12]改为0后,i的值也被改为0了
于是我们在监视中去监视&i,&arr[12],发现二者的地址居然相同!也就是意味着,二者使用的是同一块空间,如果对arr[12]修改,那么必定把i也改动了①
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
6.如何写出好的、易于调试的代码
优秀的代码
- 代码运行正常
- Bug很少
- 效率高
- 可读性高
- 可维护性高
- 注释清晰
- 文档齐全
-- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-
常见的coding技巧
- 使用assert
- 尽量使用const
- 养成良好的编码风格
- 添加必要的注释
- 避免编码的陷阱
coding:编码
assert:断言(一个函数)
- 使用assert函数要引用头文件assert.h
- assert(表达式)
- 若表达式为真(非0),这什么事都不发生;若表达式为假(0),则会报错
-- -- -- -- -- -- -- -- -- -- -- -- -- -- --
示范
#include <stdio.h>
void my_strcpy(char* dest, char* src)
{
while (*src != '\0')//没找到'\0'就对应元素交换并且指针加加
{
*dest = *src;
src++;
dest++;
}
*dest = *src;//跳出循环就是说明找到'\0'了,把'\0'也拷贝过去
}
int main()
{
char arr1[] = "#####################\n";
char arr2[] = "xiemingshouhuang";
my_strcpy(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
运行结果:xiemingshouhuang
-- -- -- -- -- -- -- -- -- -- -- -- -- -- --
优化
显然,前面的代码还能进行优化
#include <stdio.h>
#include <assert.h>
char* my_strcpy(char* dest, char* src)
{
char* ret = dest;
assert(src != NULL);
assert(dest != NULL);
//把src指向的字符拷贝到dest指向的空间,包含'\0'字符
while (*dest++ = *src++)
{
;//空语句也是语句
}
return ret;//返回一个char*类型的指针,所以返回类型也要更改
}
int main()
{
char arr1[] = "#####################\n";
char arr2[] = "xiemingshouhuang";
printf("%s\n", my_strcpy(arr1, arr2));
return 0;
}
运行结果:xiemingshouhuang②
对于 assert(src != NULL); assert(dest != NULL);还有别的写法
- assert(src); assert(dest);//因为空指针NULL的的值本身就是0,如果表达式判断为假(0)一样会报错
- assert(src!=NULL&&dest!=NULL);//两个指针有一个为NULL都立刻判断为假并且报错
- assert(dst && src);
以上写法自己喜欢哪个用哪个
-- -- -- -- -- -- -- -- -- -- -- -- -- -- --
const的作用
const修饰指针变量时
- const如果在*的左边(const int* p = &i;),修饰的时*p,即不能通过p来改变i,如*p = 0;(这样是不行的)
- const如果在*的右边(int* const p = &i;),修饰的是p,即p不能再改成(存放)其他地址,如p = &j;(这样是不行的)
-- -- -- -- -- -- -- -- -- -- -- -- -- -- --
实例
对于前面模拟实现的strcpy函数,我们可以再次做一点优化。因为我们已经知道,字符串拷贝时,时将src指向的内容拷贝到dest指向的内容,即src指向的内容是不需要修改的,所以我们可以给它加上一个const,使代码更加健壮、安全
#include <stdio.h>
#include <assert.h>
char* my_strcpy( char* dest, const char* src)
{
char* ret = dest;
assert(src != NULL);
assert(dest != NULL);
//把src指向的字符拷贝到dest指向的空间,包含'\0'字符
while (*dest++ = *src++)
{
;//空语句也是语句
}
return ret;//返回一个char*类型的指针,所以返回类型也要更改
}
int main()
{
char arr1[] = "#####################\n";
char arr2[] = "xiemingshouhuang";
printf("%s\n", my_strcpy(arr1, arr2));
return 0;
}
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
7.编程常见的错误
- 编译型错误:直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单
- 链接型错误:看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误
- 运行时错误:借助调试,逐步定位问题
前面两种错误编译器基本会帮你搞定(找出来),你只需要修改即可。但是最后一种错误是,编译器没有报错,也没什么警告,代码成功运行起来了,但是没有完成预期的任务,即输出的结果不是你想要的,这是就要自己慢慢来调试了。
同样,如果我们在代码中多去判断、多使用assert和const等,这样的好习惯可以减少出错,就算出Bug也能以较快的速度发现并修改
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
附
①前面(本章第一节附言部分)已经说过了,在64位平台上是演示不出来的,要在32位平台(x86)上才可以演示。不然,就算你把循环条件改成i<=15都不能把i改成0,我在这里就简单演示一下就好了。这个具体的调试感觉还需要同学亲自上手去试试才能感觉到其中的变化
i=12后,再执行一次循环
i就会变成0
为什么这个效果在64位平台上面演示不出来我也不知道,如果有那位同学或者大佬知道原因也教教本长老咯~~
②假如我们往dest指针传一个空指针,看看assert的反应