六、调试实例
注:以下代码均为问题代码
1、实例一
实现代码:求1! + 2! + 3! … + n! ; 不考虑溢出
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> int main() { int n = 0; scanf("%d", &n); int i = 0; int j = 0; int ret = 1; int sum = 0; for(j = 1; j <=n; j++) { for(i = 1; i <= j; i++) { ret *= i; } sum += ret; } printf("%d\n", sum); return 0; }
现象,当求3的阶乘时,输出的是15,答案与预期不符(这段代码相对简单这里就自己调试解决)这种错误被称为运行时错误,也是未来比较常见和比较难发现的一种错误,能通过调试解决的就是运行时错误
2、实例二(出自《C陷阱和缺陷》曾经 nice2016的校招笔试题)
#include<stdio.h> 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; }
现象:死循环
经调试发现造成死循环的直接原因是:
为什么改了arr[12],而i也改了 ?其实不难想象它们同在一块空间
我们不妨大胆的猜测一下
这里面是有原因的,当然也有一定程度的巧合
1、i 和arr 是局部变量,而局部变量是放在栈区上的(注意不要跟数据结构的栈混淆了)
2、栈区内存的使用习惯:先使用高地址空间,再使用低地址空间
3、数组随着下标的增长,地址是由低到高变化的
这里如何避免死循环呢?
1、只要先定义arr数组再定义 i 即可
2、控制循环次数,<=11即可
经测试不同的编译器下 i 和 arr 在内存中的布局:中间相距的空间也不同,以上面代码为例:
1、VC6.0 -> 相差0个整型,<=10即死循环
2、gcc -> 相差1个整型,<=11即死循环
3、VS2017 -> 相差2个整型,<=12即死循环
所以数组只要向上越界的合适就会造成死循环
Release相比于Debug的还有一点就是Release会对代码进行优化(使之不会死循环)
Release是怎么优化的?
这里Release在发现问题后,会对局部变量 i 和 arr 在栈区上的顺序进行适应的调整
七、如何写出好(易于调试)的代码
1、优秀的代码:
- 代码运行正常
- bug很少
- 效率高
- 可读性高
- 可维护性高
- 注释清晰
- 文档齐全
2、常见的coding技巧
- 使用assert
- 尽量使用const
- 养成良好的编码风格
- 添加必要的注释
- 避免编码的陷阱
3、实例示范
1、模拟实现strcpy
简单介绍strcpy函数,所在头string,它可以进行字符串拷贝(包括\0)
#include<string.h> #include<stdio.h> int main() { char arr1[20] = "xxxxxxxxxx"; char arr2[] = "hello"; strcpy(arr1, arr2); printf("%s\n", arr1);//hello return 0; }
调试发现strcpy在拷贝字符串的时,也包含 \0:
使用my_strcpy函数来模拟strlen
#include<stdio.h> void my_strcpy(char* dest, char* src) { while(*src != '\0') { //赋值 *dest = *src; //调整 dest++; src++; } *dest = *src; } int main() { char arr1[20] = "xxxxxxxxxx"; char arr2[] = "hello"; my_strcpy(arr1, arr2); printf("%s\n", arr1);//hello return 0; }
1. 优化1(简洁)
#include<stdio.h> void my_strcpy(char* dest, char* src) { while(*src != '\0') { //赋值+调整 *dest++ = *src++;//hello的拷贝 } *dest = *src;//\0的拷贝 } int main() { char arr1[20] = "xxxxxxxxxx"; char arr2[] = "hello"; my_strcpy(arr1, arr2); printf("%s\n", arr1);//hello return 0; }
2. 再优化2(简洁)
#include<stdio.h> void my_strcpy(char* dest, char* src) { while(*dest++ = *src++)//既拷贝了字符串(包括\0),又可以利用表达式让循环停下来 { ; } } int main() { char arr1[20] = "xxxxxxxxxx"; char arr2[] = "hello"; my_strcpy(arr1, arr2); printf("%s\n", arr1);//hello return 0; }
3. 再优化3(从指针安全的角度考虑)
#include<stdio.h> #include<assert.h> void my_strcpy(char* dest, char* src) { //如果my_strcpy传过来的参数是空指针时,此时再去解引用、++等一系列操作时,这是非法的 //这里有一个函数assert:断言,所在头assert。如果表达式里为真,则什么都不执行,否则将会停留在断言为假的那一行,不再执行下面代码,并且会详细输出错误信息(当然不仅限于指针) //在以后编码中,如果要对指针进行一些操作时,断言可以讯速的帮我们找到问题所在 assert(dest != NULL); assert(src);//同assert(src != NULL); while(*dest++ = *src++) { ; } } int main() { char arr1[20] = "xxxxxxxxxx"; char arr2[] = "hello"; my_strcpy(arr1, arr2); printf("%s\n", arr1);//hello return 0; }
4. 再优化4(使用const来限定不需要操作的字符串)
对比上面我们模拟的my_strcpy来说,库里的strcpy在原字符串上加了const来修饰。先来看一个场景:
赋值写反了:所造成的arr2数组越界
这里分析arr2的这块空间是不需要被改变的,所以加上const限定更安全,如果对const限定的字符串操作,编译器会主动报错
优化后
#include<stdio.h> #include<assert.h> void my_strcpy(char* dest, const char* src) { assert(dest != NULL); assert(src); while(*dest++ = *src++) { ; } } int main() { char arr1[20] = "xxxxxxxxxx"; char arr2[] = "hello"; my_strcpy(arr1, arr2); printf("%s\n", arr1);//hello return 0; }
1、延伸const
在之前的文章中有提到const,被const修饰的变量不能被修改
#include<stdio.h> int main01() { const int num = 0; num = 20;//err printf("%d\n", num); return 0; }
这里把num的地址交给p指针管理,然后发现被const限定的num能通过指针p改变num的值。当然这不是我们想要的
#include<stdio.h> int main() { const int num = 0; int* p = # *p = 20; printf("%d\n", num); return 0; }
const和指针
#include<stdio.h> //const如果放在*的左边,修饰的是*p,表示指针指向的内容,是不能通过指针来改变的。但是指针变量本身(p->地址)是可以修改的 int main01() { const int num = 0; int n = 20; const int* p = # //*p = 20;//err p = &n;//ok printf("%d\n", *p);//此时此刻p指针不再指向num,而是指向n return 0; } //const如果放在*的右边,修饰的是p(地址),表示指针的地址,是不能改变指针变量的地址的,但是指针指向的内容是可以改变的 int main02() { const int num = 0; int n = 20; int* const p = # //p = &n;//err *p = 20;//ok printf("%d\n", num); return 0; } //const如果放在*的左边和右边,则指针指向的内容不可以被改变和指针变量也不能被改变 int main03() { const int num = 0; int n = 20; const int* const p = # //p = &n;//err //*p = 20;//err printf("%d\n", num); return 0; }
6. 优化后(函数的返回值) -> 最终版
库里的strcpy的返回值是char*,而我们模拟的是my_strcpy是void
strcpy返回的是目标空间的起始地址,相比来说有返回值的strcpy可以使用链式访问
#include<stdio.h> #include<assert.h> char* my_strcpy(char* dest, const char* src) { assert(dest != NULL); assert(src); char* ret = dest;//备份一份首地址 while(*dest++ = *src++) { ; } return ret;//返回目标空间的首地址 } int main() { char arr1[20] = "xxxxxxxxxx"; char arr2[] = "hello"; printf("%s\n", my_strcpy(arr1, arr2));//hello return 0; }
2、模拟实现strlen
#include<stdio.h> #include<assert.h> size_t my_strlen(const char* str)//size_t是无符号整型 { assert(str); size_t count = 0; while(*str++) count++; return count; } int main() { char arr[] = "hello bit"; printf("%d\n", my_strlen(arr)); return 0; }
八、补充
如果想要去了解一下源码是怎么实现的,建议大家去翻下VS的根目录
VS2017参考路径:
C:\Program Files (x86)\Windows Kits\10\Source\10.0.17763.0\ucrt
这里有个快速查找的工具推荐给大家
九、编程常见错误
1、编译型错误
这种类型属于语法错误,相对简单。
解决方法:直接看错误提示信息,(双击就可定位到有问题的代码上)
2、链接型错误
LNK(链接型错误)这种错误只要了解它为什么会产生,也不难找
主要产生的原因
1、这个函数压根就未定义
2、调用函数名时与定义的函数名不一
解决方法:错误信息上不可以定位到有问题的代码上,但是可以作为一些依据
3、运行时错误
这种错误没有错误信息提示,相对较难找。一般是输出结果与预想或与正确答案不符
解决方法:借助调试,逐步定位问题
可以把每天因为调试所解决的运行时错误代码写一个代码日志













