4.多多动手,尝试调试,才能有进步
①一定要熟练掌握调试技巧;
②初学者可能80%的时间在写代码,20%的时间在调试。但是一个程序员可能20%的时间在写代码,但是80%的时间在调试。
③我们现在所讲的都是一些简单的调试,以后可能会出现很复杂的调试场景:多线程程序的调试等。
④多多使用快捷键,提升效率。
5.一些调试的实例
实例一:
实现代码:求 1!+2!+3! ...+ n! ;不考虑溢出。
代码1:实现阶乘
#include<stdio.h> int main() { //输入求几的阶乘 int n = 0; scanf("%d", &n); //实现求n! n!=n*(n-1) int ret = 0; int i = 0; for (i = 1; i <= n; i++) { ret *= i; } //输出结果 printf("%d\n", ret); return 0; }
如果我们输入3,想输出6,但实际输出的是0.
why?
这里我们就得找我们的问题:
①首先通过经验推测问题出现的原因,初步确定问题可能的原因最好。
②实际上手调试很有必要。
③调试的时候我们要心里有数。
通过初步推测ret变量有问题,我们在在for循环打断点调试观察变量ret具体有什么问题。
代码2: 求 1!+2!+3! ...+ n!
#include<stdio.h> int main() { //输入有n个阶乘 int n = 0; scanf("%d", &n); //循环 求 1!+2!+3! ...+ n! int ret = 1; int i = 0; int sum = 0;//存放阶乘的累加和 for (i = 1; i <= n; i++) { int j = 0; //实现求i的阶乘 for (j = 1; j <= i; j++) { ret *= j; } sum += ret; } //输出结果 printf("%d\n", sum); return 0; }
如果我们输入3,想输出9,但实际输出15。
why?
分析:推测循环出错了,第一次调试在第二个循环处打断点,一步步调试监视变量的变化。
但是没有发现是哪里错了,第二次调试在断点处右击设置断点条件快速调试到错误处,符合断点条件就停止,再F10观察具体原因。
实例二:
#include <stdio.h> int main() { int i = 0; //数组下标界限0~9 int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; for (i = 0; i <= 12; i++) { //数组下标为10~12时数组越界 arr[i] = 0; printf("hehe\n"); } return 0; }
数组越界应该是程序错误,不执行但是我们运行后发现程序死循环了。
why?
我们F10调试起来观察变量。
在调试的时候,我们发现每一次i的值都和arr[12]的值一样,当arr[12]=0时,i也变成0了,所以死循环。
那arr[12]和i是不是地址一样?我们调试观察之后确实是一样的。
图解:
在i和arr数组中间恰好就是2个整形吗?
答:不一定,该代码只是在VS2019 X86环境下实验的结果。
如果是VC6.0——i和arr之间没有多余的空间,gcc——i和arr之间有一个整形空间。
所以说平时我们写代码要注意不数组越界了。
6.如何写出好(易于调试)的代码
6.1优秀的代码:
①代码运行正常
②bug很少
③效率高
④可读性高(如良好的代码风格,函数名、变量名见名知意等)
⑤可维护性高
⑥注释清晰
⑦文档齐全
常见的coding技巧:
①使用assert
②尽量使用const
③养成良好的编码风格
④添加必要的注释
⑤避免编码的陷阱
6.2示范
模拟实现库函数strcpy:
strcpy:
1.函数原型:
2.函数功能:
3.函数参数:
4.函数的返回类型:
代码1:模拟实现strcpy
分析:
#include<stdio.h> #include<string.h> //自定义strcpy //代码1 void my_strcpy(char* dest, const char* src) { //拷贝'\0'之前的字符 while (*src != '\0') { *dest = *src; dest++; src++; } //拷贝'\0' *dest = *src; } int main() { //将arr2中的字符串拷贝在arr1 char arr1[20] = "#############"; char arr2[] = "hello"; //调用库函数实现 //strcpy(arr1, arr2); //调用自定义函数实现 my_strcpy(arr1, arr2); //打印拷贝后的arr1 printf("%s\n", arr1); return 0; }
代码2:优化函数体
#include<stdio.h> #include<assert.h> //自定义strcpy //代码2 void my_strcpy(char* dest, const char* src) { //优化1:使用指针之前一定要检查是否有效,如果无效就报错 //assert--断言 // assert中可以放一个表达式,表达式结果为假就报错,为真就啥事都不发生,正常运行 //assert的头文件是assert.h //assert其实在release版本中被优化调了 assert(dest && src);//断言指针的有效性 //优化2:使代码简化 //*dest++ = *src++; //等价于 //*dest = *src; //dest++;src++; while (*dest++ = *src++)//'\0'的ASCII码值就是0,所以拷贝到'\0'停止 { ; } } int main() { //将p中的字符串拷贝在arr1 char arr1[20] = "#############"; char* p = NULL; //调用自定义函数实现 my_strcpy(arr1, p); //打印拷贝后的arr1 printf("%s\n", arr1); return 0; }
程序结果:
代码3:优化函数的形参
如下代码我们程序不报错,但是没有成功完成我们想要的拷贝:
#include<stdio.h> #include<assert.h> void my_strcpy(char* dest, char* src) { assert(dest && src);//断言指针的有效性 while(*src++ = *dest++)//程序员喝酒,写反了这样我们没有实现拷贝的目的 { ; }//将src所指向内容拷贝到dest所指向数组 } int main() { char arr[20] = "#############"; char arr1[20] = "hello"; my_strcpy(arr, arr1); printf("%s\n", arr); return 0; }
该怎么避免出现这种错误呢?
我们先来学习const的作用:
#include <stdio.h> void test() { //代码1 //定义两个整型变量 int n = 10; int m = 20; //没有const修饰 int* p = &n; //可以通过指针变量p将指针所指向的内容n的值改成20? *p = 20;//ok //可以修改指针变量本身? p = &m; //ok } void test1() { //代码2 const int num = 10; //num = 20;//err,因为num被const修饰,所以不能修改 //但是通过指针变量p,num能被修改了(p就像卖票的黄牛一样) int* p = # *p = 20; } void test2() { //代码3 int n = 10; int m = 20; //const放在*的左边 const int* p = &n;//也可写成:int const* p = &n; //*p = 20;//err,因为const修饰的指针p指向的内容,所以不能通过指针来修改 p = &m; //ok,因为const只修饰的是指针p指向的内容,所以指针变量本身可以修改 } void test3() { //代码4 int n = 10; int m = 20; //const放在*的右边 int* const p = &n; *p = 20; //ok,因为const只修饰的是指针变量本身,所以指针指向的内容可以通过指针改变 //p = &m; //err,因为const修饰的是指针变量本身,所以指针变量本身不能被修改 } int main() { //测试无cosnt的 test(); //测试const修饰变量 test1(); //测试const放在*的左边 test2(); //测试const放在*的右边 test3(); return 0; }
结论:
const修饰指针变量的时候:
1.const如果放在*的左边,const修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变;但是指针变量本身可以修改。
2.const如果放在*的右边,const修饰的是指针变量本身,保证指针变量本身的内容不能被修改;但是指针指向的内容,可以通过指针来改变。
3.const就像法律,不能被修改。
学习了const的作用,我们来修改刚在代码的问题,可以运行但是没有完成拷贝,是因为*dest++和*src++写反了,因为src所指向的内容不变。所以我们可以在把第二个形参改成const int* src,用const修饰指针指向的内容,这样的话如果不小心将*dest++和*src++写反直接就编译错误,不会运行成功,很快就发现代码的错误了。
#include<stdio.h> #include<assert.h> void my_strcpy(char* dest,const char* src) { assert(dest && src);//断言指针的有效性 while (*src++ = *dest++)//程序员喝酒,写反了这样我们没有实现拷贝的目的 { ; }//将src所指向内容拷贝到dest所指向数组 } int main() { char arr[20] = "#############"; char arr1[20] = "hello"; my_strcpy(arr, arr1); printf("%s\n", arr); return 0; }
如下图:
代码4:优化函数的返回类型(最终的优化版本)
#include<stdio.h> #include<assert.h> //库函数strcpy的返回值是目的地的起始地址 char* my_strcpy(char* dest,const char* src) { assert(dest && src);//断言指针的有效性 char* ret = dest;//存放目的地的起始地址 while (*dest++ = *src++) { ; }//将src所指向内容拷贝到dest所指向数组 return ret; } int main() { char arr[20] = "#############"; char arr1[20] = "hello"; //优点:链式访问(有返回值才可以) printf("%s\n", my_strcpy(arr, arr1)); return 0; }
运行结果:
练习:模拟strlen
#include<stdio.h> #include<assert.h> //size_t是unsigned int的别名,因为长度没有负数 size_t my_strlen(const char* str) { assert(str != NULL);//断言指针的有效性 size_t count = 0;//计数 while (*str++) { count++; } return count; } int main() { char arr[] = "abcdef"; printf("%d\n", my_strlen(arr)); return 0; }
7.编程常见的错误
7.1编译型错误(语法错误)
直接看错误提示信息(双击锁定),解决问题。或者凭借经验就可以搞定,相对来说简单。
7.2链接型错误
看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。
类型1:库函数不包含头文件
类型2:拼写错误
我们怎么找到错误位置?
7.3运行时错误(编译、链接都没错,但是运行结果有问题)
借助调试,逐步定位问题,最难搞。
最后温馨提示:
做一个有心人,积累排错经验!