3.3.5查看寄存器信息
4.一些调试的实例
实例1:
题目:实现1!+2!+3!+4!+......+n!
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int n = 0; scanf("%d", &n); int i = 0; int j = 0; int ret = 1; int sum = 0; for (i = 1; i <= n; i++) { for (j = 1; j <= i; j++) { ret *= j; } sum += ret; } printf("%d\n", sum); return 0; }
如果我们输入3,应该输出9,结果输出的是15,为什么?
自行尝试调试以上代码,找出错误,并修正代码。
实例2:
再看一段代码:
#define _CRT_SECURE_NO_WARNINGS 1 #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; }
在X86环境下运行,会出现怎样的结果?
运行一下会发现,一直在打印hehe,陷入死循环。
根据上述代码能够发现,for循环在访问数组时存在越界访问的问题,但这解释不了死循环,一般数组越界访问,运行时程序会崩溃。
那造成死循环的原因是什么呢?
先来调试一下会发现:
虽然存在数组越界访问的问题,但是这段代码依然将arr[10]、arr[11]、arr[12]的值改了,此时如果再按F10调试一次会发现i又变成了0,循环又重新开始了。到下一次i又会变成0,i永远满足循环条件,就陷入了死循环。
问题又来了,按照代码,arr[10]和arr[11]确实改成了0,但是为什么arr[12]的值和i的值一样呢?
打开监视窗口,查看一下i和arr[12]的地址会发现:
它们的地址空间竟然是同一个。原来在修改arr[12]的同时,由于它和i的地址空间是同一个,所以i的值也被修改成0。
那为什么它们的地址空间能重合呢?
这就涉及到数据存储的知识了。
前面我们讲过,数据在内存中的存储有三个区域,栈区、堆区、静态区。因为代码中的i和数组arr[10]都是局部变量,所以存放在栈区。
首先我们要知道:栈区的使用习惯是:先使用高地址处的空间,然后使用低地址处的空间
而我们在创建局部变量i和arr[10]时,先创建的i,后创建的arr[10],所以在栈区i的地址空间应该在数组arr[10]之上,又因为数组元素随着下标的增长,地址是由低到高变换的。所以i和arr[10]在内存中的存储应该如下图所示:
随着对数组元素的访问,总有一天会访问到arr[12],巧合的是i地址空间和arr[12]地址空间重合了,导致每次到修改arr[12]的值的时候,都是在同时修改i的值,i的值永远不可能大于12,这就是造成死循环的原因。
如果我们将for循环中的循环条件改为 i <= 11;运行一下会发现,程序崩溃了。
这是因为对数组元素的访问到不了arr[12],所以就不会发生i的值也被修改的问题,此时程序崩溃的原因是数组的越界访问,而之前也越界访问但没有报错的原因是,程序陷入死循环,根本没时间报错。
上文讲过,编译环境分为Debug和Realse两种,案例二是在Realse版本上运行,下面我们试试在Realse版本上编译一下:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int i = 0; int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("%p\n", &i); printf("%p\n", &arr[0]); printf("%p\n", &arr[9]); for (i = 0; i <= 12; i++) { arr[i] = 0; printf("hehe\n"); } return 0; }
运行结果:
可以发现,此时并没有发生死循环,并且我们通过打印i、arr[0]、arr[9]的地址得知,此时i的地址空间在数组arr的下面:
这其实就是编译器默认的优化方式。
5.如何写出优秀(易于调试)的代码
5.1优秀的代码:
1.代码运行正常
2.bug很少
3.效率高
4.可读性高
5.可维护高
6.注释清晰
7.文档齐全
常见的coding技巧:
1.使用assert
2.尽量使用const
3.养成良好的编码风格
4.添加必要的注释
5.避免编码陷阱
5.2示范:
模拟实现strcpy函数的功能:
前面讲过strcpy函数,它的功能是拷贝字符串(注意拷贝时将原字符串的“\0”也会拷贝过去):
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { char arr1[] = "hello bit"; char arr2[20] = { 0 }; strcpy(arr2, arr1); printf("%s\n", arr2); return 0; }
运行结果:
下面我们来模拟实现一下:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> my_strcpy(char* arr1, char* arr2) { while (*arr1 != '\0') { *arr2 = *arr1; arr1++; arr2++; } *arr2 = *arr1;//\0的拷贝 } int main() { char arr1[] = "hello bit"; char arr2[20] = { 0 }; my_strcpy(arr1,arr2); printf("%s\n", arr2); return 0; }
以上代码就可以实现拷贝字符串的功能了,但是上述代码好吗?
不见得。为什么呢?
我们想一下,如果有人在写代码传参时,传了一个空指针,那我们在函数中对空指针解引用是很危险的,代码还能正常运行吗?
很明显,不能了。所以当前代码的处理并不是最优的。那怎么处理呢?
这时候就要用到断言了。
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<assert.h> my_strcpy(char* arr1, char* arr2) { assert(arr1 != NULL);//断言 assert(arr2 != NULL);//断言 while (*arr1 != '\0') { *arr2 = *arr1; arr1++; arr2++; } *arr2 = *arr1;//\0的拷贝 } int main() { char arr1[] = "hello bit"; char arr2[20] = { 0 }; char* p = NULL; my_strcpy(p,arr2); printf("%s\n", arr2); return 0; }
此时如果传的是空指针NULL,遇见断言程序就会报错,那么我们就很清楚地知道错误所在,可以及时对代码进行修正。相反,如果没有写断言程序,我们在程序崩了之后还要一步一步的调试寻找错误,这就大大浪费了时间。
注意:在使用断言时,一定要引用头文件<arrert.h>
我们还可以对代码进行优化,
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<assert.h> my_strcpy(char* arr1, char* arr2) { assert(arr1 != NULL);//断言 assert(arr2 != NULL);//断言 while (*arr2 = *arr1) { arr1++; arr2++; } } int main() { char arr1[] = "hello bit"; char arr2[20] = { 0 }; char* p = NULL; my_strcpy(arr1,arr2); printf("%s\n", arr2); return 0; }
当然也可以直接将while循环写为:
while (*arr2++ = *arr1++) { ; }
因为strcpy函数返回的是目标数组的起始地址,所以我们还可以对代码进行优化:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<assert.h> char* my_strcpy(char* arr1, char* arr2) { char* ret = arr2; assert(arr1 != NULL);//断言 assert(arr2 != NULL);//断言 while (*arr2++ = *arr1++) { ; } return arr2; } int main() { char arr1[] = "hello bit"; char arr2[20] = { 0 }; char* p = NULL; my_strcpy(arr1,arr2); printf("%s\n", arr2); return 0; }
那么此时大家就觉得代码已经完美了吗?
当然没有,如果有个程序员在写代码时不小心将while循环中的交换的代码写反了怎么办?
我们要及时发现这个问题呀,这时候就可以用到 const 了。
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<assert.h> char* my_strcpy(const char* arr1, char* arr2)//const修饰arr1 { char* ret = arr2; assert(arr1 != NULL);//断言 assert(arr2 != NULL);//断言 while (*arr1++ = *arr2++)//交换顺序写反 { ; } return arr2; } int main() { char arr1[] = "hello bit"; char arr2[20] = { 0 }; char* p = NULL; my_strcpy(arr1, arr2); printf("%s\n", arr2); return 0; }
运行结果:
此时编译器会报错,即使写错了,也可以及时发现错误并修改。
下面我们来仔细讲一下const的用法:
下面我们使用两种方法对定义好的变量num的值进行修改:
//法1: int num = 0; num = 100; //法2: int* p = # *p = 200; return 0;
但是我们加上const之后呢?
const int num = 0; num = 100;
编译器出现报错:
为什么呢?
我们说过,const修饰的变量具有常属性,它的值不能再被修改。
但是此时能不能用指针来修改变量num的值呢?
int main() { const int num = 0; int* p = # *p = 200; printf("%d\n", num); return 0; }
运行结果:
答案是可以的。
那如果用const修饰指针,num的值还能被修改吗?
int main() { const int num = 0; const int* p = # *p = 200; printf("%d\n", num); return 0; }
大家自行运行一下会发现,也是不行的,说明const也是可以修饰指针的,并且经过const修饰之后,通过指针对指针所指对象的值也不能进行修改了.
那const修饰指针的时候有什么作用呢?
当我们把const修饰在int前面会发现:
此时通过*p改变变量num的值已经不行了,但是可以改变指针p的指向。
如果const放在int的后面,* 的前面(即int const *p=#),运行一下我们会发现结果和上面相同,下面我们将const放在*的后面看结果是否相同:
此时我们发现可以通过*p改变变量num的值,但是不能改变指针p的指向了。
下面我们就可以来总结一下const修饰指针时的作用:
当const 放在*的左边的时候,限制的是指针指向的内容,不能通过指针变量改变指针指向的内容,但是指针变量的本身是可以改变的。
当const 放在*的右边的时候,限制的是指针变量本身,指针变量的本身是不能改变的,但是指针指向的内容是可以通过指针来改变的。
那如果在*号的前后都加上const,那既不能通过*p改变变量num的值,也不能改变指针p的指向。
以上就是我们讲的如何写出一篇优秀代码,下面我们也可以写一段模拟实现strlen函数功能的代码应用一下:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<assert.h> int my_strlen(const char* str) { assert(str); int count = 0; while (*str) { count++; str++; } return count; } int main() { char arr[] = "abc"; int len = my_strlen(arr); printf("%d\n", len); return 0; }
6.编程常见的错误
6.1编译型错误
直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单
例如:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int n=0 return 0; }
像这种忘记写分号的语法错误就是编译型错误,编译器报错后,直接点击报错就能找到错误的代码行进行修改。
6.2链接型错误
看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。
例如:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int Add(int x, int y) { return x + y; } int main() { int ret = add(2, 3); return 0; }
像这种将函数名Add写成add破坏了主函数和Add函数链接,就叫做链接型错误。
编译器报错时,一般前面都有LNK,而且在报错中双击,并不能找到错误的代码行,比较难解决:
上述代码只需将add改为Add就可以成功运行了。
6.3运行时错误
借助调试,逐步定位问题。最难搞
例如:
我们想要实现加法,却写成了减法。
像这种能够成功运行,但是结果不是我们想要的,这就是逻辑上出了问题,要重新考虑写代码的逻辑,这种问题最难解决。需要我们重新将代码梳理一遍甚至几遍才能找到错误。
今天就学到这,未完待续。。。