C语言调试技巧
1、什么是BUG?
计算机程序错误(来源于第一次被发现的导致计算机错误的飞蛾(bug,虫子))。
2、调试是什么?有多重要?
2.1、调试是什么?
调试,又称移错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
2.2、调试的基本步骤
1)发现程序错误的存在
2)以隔离、消除等方式对错误进行定位
3)确定错误产生的原因
4)提出纠正错误的解决方法
5)对程序错误予以改正,重新测试
2.3、Debug和Release的介绍
在VS2019界面左上角可以选择环境。分为Debug/Release 和 x86/x64 。
1)Debug通常称为调试版本,它包含调试信息,并且不做任何优化,便于程序员调试程序。
2)Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
演示如下:
int main()
{int i = 0;int arr[10] = { 0 };for (i = 0; i <= 12; i++){arr[i] = 0;printf("haha\n");}//Debug 模式运行,可以运行,但死循环。//Release 模式运行,可以运行,不死循环。return 0;
}//进入调试发现,arr[12] 地址和i 地址相同
//arr[12] = 0 即 i = 0,循环重新开始,致死循环
3、Windows环境调试介绍
3.1、调试环境的准备——在环境中选择debug选项,才能使代码正常调试。
3.2、学会快捷键
F5——启动调试,经常用来直接跳到下一个断点处。
F9——创建断点和取消断点。断点可以设置在程序的任意位置,就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。断点可以通过右击红点设置条件,节省循环时间。
F10——逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
F11——逐语句,就是每一次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最常用的)。
CTRL+F5——开始执行(不调试),用于不调试,直接运行程序的情况。
3.3、调试的时候产看程序当前信息
3.3.1、查看临时变量的值
调试>窗口>监视>监视1(任选一个都行)>输入变量名:在调试开始之后,用于观察变量的值。
3.3.2、查看内存信息
调试>窗口>内存>内存1(任选一个都行)>输入地址:在调试开始之后,用于观察内存信息。
3.3.3、查看调用堆栈
调试>窗口>调用堆栈:反映函数的调用关系以及当前调用所处的位置。
3.3.4、查看汇编信息
调试>窗口>反汇编(或直接鼠标右键>反汇编):可以切换到汇编代码。
3.3.5、查看寄存器信息
调试>窗口>寄存器:可以查看当前运行环境的寄存器的使用信息。
4、多多动手,尝试调试,才能有进步
程序员大量时间需要调试代码,有必要熟练掌握调试技巧,多使用快捷键提升效率。
4.1、实例1
int main()
{//求1!+2!+...n!,从1到n的阶乘之和int n = 0;scanf("%d", &n); //输入 3 ,预期1-3阶乘之和为9int 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); //打印结果 15 。 不符合预期,开始调试。//进入调试发现,第三轮阶乘开始时,ret是2,致3的阶乘由1*1*2*3=6,变为2*1*2*3=12。//合理推测,出现这一现象,是ret在每次阶乘开始时未置1。//修改程序,使每次阶乘开始时ret = 1。return 0;
}
调试时先找到原因,然后对症下药,如下:
int main()
{//求1!+2!+...n!,从1到n的阶乘之和int n = 0;scanf("%d", &n); //输入 3 ,预期1-3阶乘之和为9int i = 0;int j = 0;int ret = 1;int sum = 0;for (i = 1; i <= n; i++){ret = 1; //每次阶乘时ret置1。for (j = 1; j <= i; j++){ret *= j;}sum += ret;}printf("%d\n", sum); //打印结果 9 。 符合预期。return 0;
}
4.2、实例2
int main()
{//打印13行 "haha" 。int i = 0;int arr[10] = { 0 };for (i = 0; i <= 12; i++) //预期打印13行 haha 。{arr[i] = 0;printf("haha\n"); //死循环打印 haha 。}//进入调试发现,arr[12] 地址和i 地址相同//arr[12] = 0 即 i = 0,循环重新开始,致死循环return 0;
}
//死循环原因为越界,致改变i的值,修改条件至不越界。
调试时先找到原因,然后对症下药,如下:
int main()
{//打印13行 "haha" 。int i = 0;int arr[13] = { 0 }; //扩大数组容量。for (i = 0; i <= 12; i++) //预期打印13行 haha 。{arr[i] = 0;printf("haha\n"); //死循环打印 haha 。}//进入调试发现,arr[12] 地址和i 地址相同//arr[12] = 0 即 i = 0,循环重新开始,致死循环return 0;
}
5、如何写出好(易于调试)的代码
5.1、优秀的代码
1)代码运行正常。
2)bug很少。
3)效率高
4)可读性高。
5)可维护性高。
6)注释清晰。
7)文档齐全。
常见技巧:
1)使用assert(断言)。
2)尽量使用const。
3)养成良好的编码风格。
4)添加必要的注释。
5)避免编译的陷阱。
模拟实现strcopy 库函数,并逐步优化,演示如下(逐步):
//字符串拷贝
//将一个字符串中的字符复制给另一个字符串,\0也复制进去
void my_strcopy(char* dest, char* src)
{while (*src != '\0') //字符源数组不为 \0 执行。{*dest = *src; //字符源数组的首字符赋值给目标字符数组首地址dest++; //目标字符数组地址+1,即指针移动到下一个元素。src++; //字符源数组地址+1,即指针移动到下一个元素。}*dest = *src; //字符源数组的字符赋值给目标字符数组地址,这里对应 \0 。//循环走到最后, *src = \0 ,不执行循环,跳出后 \0 未拷贝。
}int main()
{char arr1[10] = "XXXXXXXXXX";char arr2[] = "hello";my_strcopy(arr1, arr2);printf("%s\n", arr1); //打印结果 hello 。//拷贝成功,'\0'一起拷贝了。return 0;
}
第一步做到代码运行正常,第二步:
//字符串拷贝
//将一个字符串中的字符复制给另一个字符串,\0也复制进去
void my_strcopy(char* dest, char* src)
{while (*src != '\0') //字符源数组不为 \0 执行。{*dest ++ = *src ++; //字符源数组的字符赋值给目标字符数组地址,然后各自+1}*dest = *src; //字符源数组的字符赋值给目标字符数组地址,这里对应 \0 。//循环走到最后, *src = \0 ,不执行循环,跳出后 \0 未拷贝。
}int main()
{char arr1[10] = "XXXXXXXXXX";char arr2[] = "hello";my_strcopy(arr1, arr2);printf("%s\n", arr1); //打印结果 hello 。//拷贝成功,'\0'一起拷贝了。return 0;
}
简单优化,减少语句,保持功能同时减少语句,再优化:
//字符串拷贝
//将一个字符串中的字符复制给另一个字符串,\0也复制进去
void my_strcopy(char* dest, char* src)
{while (*dest++ = *src++) {; }//字符源数组的字符赋值给目标字符数组地址,然后各自+1,并判断表达式结果//赋值表达式的结果是赋值后左边变量的值//字符的值是其对应的ASCII码, \0 的ASCII 码是0。//即前面字符判断都不为0,执行循环。//最后一次判断时,发现*dest被赋值后为 \0 = 0,跳出循环。
}int main()
{char arr1[10] = "XXXXXXXXXX";char arr2[] = "hello";my_strcopy(arr1, arr2);printf("%s\n", arr1); //打印结果 hello 。//拷贝成功,'\0'一起拷贝了。return 0;
}
优化后间接明了,可读性高,继续优化,使用assert(断言)提升可维护性,错误展示:
#include <assert.h>
//字符串拷贝
//将一个字符串中的字符复制给另一个字符串,\0也复制进去
void my_strcopy(char* dest, char* src)
{assert(dest != NULL); //断言,不满足条件会报错并注明报错位置。assert(src != NULL); //断言,不满足条件会报错并注明报错位置。//将arr2 改成 NULL,会如下报错。//Assertion failed: src != NULL, file D:\C Projects\test7_15\test7_15\test.c, line 249//上文显示了报错原因,不满足 src != NULL 。//上文还显示了报错语句所在的文件、行号,方便管理。while (*dest++ = *src++) //循环赋值。{; }}int main()
{char arr1[10] = "XXXXXXXXXX";char arr2[] = "hello";my_strcopy(arr1, NULL);printf("%s\n", arr1); //报错 。return 0;
}
正确展示:
#include <assert.h>
//字符串拷贝
//将一个字符串中的字符复制给另一个字符串,\0也复制进去
void my_strcopy(char* dest, char* src)
{assert(dest != NULL); //断言,不满足条件会报错并注明报错位置。assert(src != NULL); //断言,不满足条件会报错并注明报错位置。while (*src++ = *dest++) //循环赋值。 若写反 dest 和src,会赋值错。{;}}int main()
{char arr1[10] = "XXXXX";char arr2[] = "hello";my_strcopy(arr1, arr2);printf("%s\n", arr1); //打印结果 XXXXX 。return 0;
}
使用const,提升健壮性,错误展示:
#include <assert.h>
//字符串拷贝
//将一个字符串中的字符复制给另一个字符串,\0也复制进去
void my_strcopy(char* dest,const char* src) //const修饰 *src,使其不可被改变。
{assert(dest != NULL); //断言,不满足条件会报错并注明报错位置。assert(src != NULL); //断言,不满足条件会报错并注明报错位置。while (*src++ = *dest++) //循环赋值。 若写反 dest 和src,会直接报错。{;}//这里报错 307 行 左值指定 const 对象。意思*src不能被改变,方便检查。}int main()
{char arr1[10] = "XXXXX";char arr2[] = "hello";my_strcopy(arr1, arr2);printf("%s\n", arr1); //报错 。return 0;
}
正确展示:
#include <assert.h>
//字符串拷贝
//将一个字符串中的字符复制给另一个字符串,\0也复制进去
void my_strcopy(char* dest, const char* src) //const修饰 *src,使其不可被改变。
{assert(dest != NULL); //断言,不满足条件会报错并注明报错位置。assert(src != NULL); //断言,不满足条件会报错并注明报错位置。while (*dest++ = *src++) //循环赋值。 若写反 dest 和src,会直接报错。{;}}int main()
{char arr1[10] = "XXXXX";char arr2[] = "hello";my_strcopy(arr1, arr2);printf("%s\n", arr1); //打印结果 hello 。return 0;
}
将返回值改成char*,可以直接打印返回值,演示如下:
//字符串拷贝,返回目标空间的起始地址。
//将一个字符串中的字符复制给另一个字符串,\0也复制进去
char* my_strcopy(char* dest, const char* src) //const修饰 *src,使其不可被改变。
{assert(dest != NULL); //断言,不满足条件会报错并注明报错位置。assert(src != NULL); //断言,不满足条件会报错并注明报错位置。char* ret = dest;while (*dest++ = *src++) //循环赋值。 若写反 dest 和src,会直接报错。{;}return ret;
}int main()
{char arr1[10] = "XXXXX";char arr2[] = "hello";printf("%s\n", my_strcopy(arr1, arr2)); //打印结果 hello 。一行代码打印,使用时更方便。return 0;
}
5.2、const的作用
const修饰指针变量时
1)const放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。
指针指向内容不能改变,演示如下:
int main()
{//const 在*左边,指针指向的内容不能被改变。int a = 0;const int* pa = &a; //const修饰 *pa,使其指向的内容不可被改变*pa = 10; //报错。左值指定 const 对象,意思不能被改变。return 0;
}
指针变量本身可变,演示如下:
int main()
{//const 在*左边,指针指向的内容不能被改变。int a = 0;int b = 0;const int* pa = &a; //const修饰 *pa,使其指向的内容不可被改变//进入调试,看到这里 pa 是 0x0113fdb8 。pa = &b; //进入调试,看到这里 pa 是 0x0113fdac 。//即*pa指向的内容不能被改变,但pa指针变量本身可以被改变。return 0;
}
2)const放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
指针变量本身不能改变,演示如下:
int main()
{//const 在*右边,指针变量本身不能被改变。int a = 0;int b = 0;int* const pa = &a; //const修饰pa,使其不可被改变pa = &b; //报错 382 行 左值指定 const 对象。意思pa不能被改变。return 0;
}
指针指向的内容可变,演示如下:
int main()
{//const 在*右边,指针变量本身不能被改变。int a = 0;int b = 0;int* const pa = &a; //const修饰pa,使其不可被改变*pa = 1; // a 的值被改为1。//即const 在*右边,指针指向的内容是可以被改变的。return 0;
}
*左右均由const,则指针和指针指向内容均不可变,演示如下:
int main()
{int a = 0;int b = 0;const int* const pa = &a; //const修饰 *pa 和 pa,使其指向的内容不可被改变,其本身也不可被改变。*pa = 1; // 报错。左值指定 const 对象,意思*pa不能被改变。pa = &b; //报错。左值指定 const 对象,意思pa不能被改变。return 0;
}
6、编程常见的错误
6.1、编译型错误
直接看错误提示信息(双击),解决问题。比如未写分号,直接双击就能定位过去,很容易处理。
6.2、链接型错误
看错误提示信息,主要在代码中找到错误信息中的标识符,然后手动定位问题。一般是标识符名不存在或者拼写错误。
6.3、运行时错误
借助调试,逐步定位问题。最麻烦。