什么是bug?
第一次被发现的导致计算机错误的飞蛾,也是第一个计算机程序错误🙂。
调试是什么?有多重要?
调试是什么
调试(英语:Debugging/Debug),又称出错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
调试这个词本质上是从硬件这个词来的,比如早期我们说的调试这个机器、调试这个硬件到底能不能工作。后来引申到我们程序里面也是一样的,程序里面也会出现一些问题,这个动作就叫调试。
所有发生的事情都有迹可循,如果问心无愧,就不需要掩盖也就没有迹象了,如果问心有愧,就必然需要掩盖那就一定会有迹象,迹象越多就越容易顺藤而上,这就是推理的途径。
顺着这条途径顺流而下就是犯罪,逆流而上,就是真相。
一名优秀的程序员是一名出色的侦探。
每一次调试都是尝试破案的过程。
调试的基本步骤
发现程序错误的存在。
以隔离、消除等方式对错误进行定位。(屏蔽一块代码或者放出一块代码看看会不会出现问题,进而定位到错误产生的区域,然后再解决问题。)
确定错位产生的原因。
提出纠正错误的解决方法。
对程序错误予以改正,重新测试。
debug和release的介绍
Debug通常被称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户更好地使用。
当时Debug的时候,我们是可以进行调试的,我们是可以通过调试窗口来观察到的。因为这段代码在编译的过程中产生各种各样的调试信息,它把程序在运行过程的上下文环境的相关信息保留下来。
当我们改成Release版本时,请看:
这个时候我们是不可以进行调试的。此时我们按F10进行调试时,它可能是跳着走的,它压根就没有按照我们的逻辑来。一些值不能进行很好的观察。
Debug文件夹底下放的是编译出来的Debug版本的相关信息,调试版本的可执行程序;而Release文件夹底下放的是Release版本的相关信息,发布版本的可执行程序。
windows环境调试介绍
快捷键
这是VS2022中的快捷键:
F5
启动调试,经常用来直接跳到下一个断点处。
F9
创建断点和取消断点
断点的重要作用,可以在程序的任意位置设置断点
这样就可以使得程序在想要的位置随意停止断点,继而一步步执行下去
那段点有什么用吗?如果说我们有500行代码、5000行代码、甚至是50000行代码让我们进行调试,设置断点可以帮助我们提高调试的效率。
注意断点要打在有意义的地方。
F10
逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
F11
逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最常用的)。即我们可以看到函数内部的细节。
CTRL+F5
开始执行不调试,如果你想要程序直接运行起来而不进行调试就可以直接使用。即使我们打断点程序也不会停下来。
当然VS中还有很多的快捷键,操作起来非常方便,这里就不进行一一列举。
注意:F10和F11处理函数的方式是截然不同的。
断点在多行代码、跨文件使用方面是非常方便的。
调试的时候查看程序当前信息
查看临时变量的值
在调试开始之后,用于观察变量的值。
注意只有按完F10进入调试状态之后才可以。
注意上图:当数组传参的时候,如果你想在形参的这一部分看到这个数组其实是看不到的,所以我们要添加一个逗号即(arr,12)。
接下来看反汇编:
反汇编可以看到我们的C语言代码翻译出来的汇编代码是什么样子的。
接下来看寄存器:
如果我们知道寄存器的名字,我们也可以在监视窗口看到,请看:
以上是怎么来查看程序执行过程中上下文环境中的变量的值、以及它的内存里的值等等。
下面是调用堆栈:
用栈的一种这样的形式模拟出来函数的一个调用逻辑,当未来看到函数调用堆栈写的是这样一个逻辑的时候,就能够清楚的看到函数调用的一个逻辑是什么。倘若工程非常大,有几千甚至上万行代码,调试起来逻辑非常复杂的时候,我们可以用调用堆栈来看到当前是怎样的一个调用关系的。
上这些只是简单的调试技巧,需要的是我们多多动手尝试调试,才能有所进步。
调试实例
实例一
实现代码:求1!+2!+3!+…+n!;不考虑溢出。
在这之前我们先来算n!:
//求n! #include<stdio.h> int main() { int n = 0; int ret = 1; int i = 0; scanf("%d", &n); for (i = 1; i <= n; i++) { ret *= i; } printf("%d\n", ret); return 0; }
//需要注意的点是这里的ret进行初始化时一定要初始化为1,而不是0。
下面就来看一下求1!+2!+3!+…+n!:
#include<stdio.h> int main() { int n = 0; int i = 0; int ret = 1; int sum = 0; scanf("%d", &n); for (i = 1; i <= n; i++) { int j = 0; ret = 1; for (j = 1; j <= i; j++) { ret *= j; } sum += ret; } printf("%d\n", sum); return 0; } //这里要注意的点就是每次循环时要重新对ret进行初始化。
实例二
#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++) { printf("hehe\n"); arr[i] = 0; } return 0; }
这段代码会出现死循环的情况:
这个地方
这个地方错误的本质数组的越界导致的。
如何写出好(易于调试)的代码
优秀的代码
1.代码运行正常
2.bug很少
3.效率高
4.可读性高
5.可维护性高
6.注释清晰
7.文档齐全
常见的coding技巧:
1.使用assert
2.尽量使用const
3.养成良好的编程风格
4.添加必要的注释
5.避免编码的陷阱
我们来模拟一下strcpy函数,然后不断优化:
#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); return 0; }
进行优化:
#include<stdio.h> //void my_strcpy(char* dest, char* src) //{ // while (*src != '\0') // { // *dest++ = *src++; // // } // *dest = *src;//拷贝\0 //} void my_strcpy(char* dest, char* src) { while (*dest++ = *src++) { ; } } int main() { char arr1[20] = "xxxxxxxxxx"; char arr2[] = "hello"; my_strcpy(arr1, arr2); printf("%s\n", arr1); return 0; }
进行优化:
#include<stdio.h> #include<assert.h> void my_strcpy(char* dest, char* src) { /*if (src == NULL || dest == NULL) { return; }*/ //断言 //assert中可以放一个表达式,表达式的结果如果为假,就报错,如果为真啥事也不发生 //assert其实在release版本中优化掉了 /*assert(src != NULL); assert(dest != NULL);*/ assert(dest && src); while (*dest++ = *src++) { ; } } int main() { char arr1[20] = "xxxxxxxxxx"; char arr2[] = "hello"; my_strcpy(arr1, arr2); printf("%s\n", arr1); return 0; } //所以未来使用指针之前像判断指针的有效性,我们可以用assert来进行断言 //assert不是仅仅只用来断言指针,一个变量的值,你不想它是什么,我们就可以用assert来进行断言。
进行优化:
#include<stdio.h> #include<assert.h> void my_strcpy(char* dest,const char* src) { assert(dest && src);//断言指针的有效性。 while (*dest++ = *src++) { ; } } int main() { char arr1[20] = "xxxxxxxxxx"; char* p = "hello";//p指向的常量字符串是不可以被修改的 my_strcpy(arr1, p); printf("%s\n", arr1); return 0; }
#include<stdio.h> #include<assert.h> void my_strcpy(char* dest, const char* src)//这里const修饰的是指针变量本身,但指针所指向的内容,可以通过指针来改变 //int num = 10; //int* p = # //int n = 1000; //const修饰指针变量的时候 //1.const放在*的左边,const修饰的是指针所指向的内容,不能通过指针来改变量,不过通过指针来改变了;但是指针变量本身可以修改 // const int* p=# // *p=20//err // p=&n;//ok // const int* p=# //2.const放在*的右边,const修饰的指针变量本身,表示指针变量本身的内容不能够被修改,但是指针指向的内容,可以通过指针来修改 //int* const p=# //*p=20;//ok //p=&n;//err { assert(dest && src);//断言指针的有效性。 while (*dest++ = *src++) { ; } } int main() { char arr1[20] = "xxxxxxxxxx"; char* p = "hello";//p指向的常量字符串是不可以被修改的 my_strcpy(arr1, p); printf("%s\n", arr1); return 0; }
优化:
#include<stdio.h> #include<assert.h> char* my_strcpy(char* dest, const char* src) { char* ret = dest; assert(dest && src);//断言指针的有效性。 while (*dest++ = *src++) { ; } return ret; } int main() { char arr1[20] = { 0 }; char* p = "hello";//p指向的常量字符串是不可以被修改的 //链式访问 //当我们把一个函数的返回值作为另外一个参数它就实现了链式访问 printf("%s\n", my_strcpy(arr1, p)); return 0; }
在这里我们考虑了const、考虑了指针的断言、考虑它的返回值类型,同时也包括\0的拷贝。
//模拟实现strlen #include<stdio.h> size_t my_strlen(const char* str) { size_t count = 0; while (*str) { count++; str++; } return count; } int main() { char arr[] = "helloworld"; printf("%d\n", my_strlen(arr)); return 0; } //我们也可以用指针-指针的方式来实现
编程常见的错误
编译型错误
直接看错误提示信息(双击),解决问题,或者凭借经验就可以搞定。相对来说简单。
链接型错误
看错误提示信息,主要在代码中找到错误信息的标识符,然后定位错误问题所在,一般是标示符名不存在或者拼写错误。
运行时错误
借助调试,逐步定位问题,最难搞。
最后:做一个有心人,积累排错经验。
本文到此也就结束了,感谢各位!!!



























