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阶乘之和为9
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); //打印结果 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阶乘之和为9
int 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、运行时错误
借助调试,逐步定位问题。最麻烦。