您的位置:首页 > 房产 > 家装 > 学习C语言之调试技巧

学习C语言之调试技巧

2025/1/7 4:53:56 来源:https://blog.csdn.net/weixin_44029398/article/details/140503209  浏览:    关键词:学习C语言之调试技巧

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、运行时错误

        借助调试,逐步定位问题。最麻烦。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com