• 【C语言】实用调试技巧


    🌠作者:@阿亮joy.
    🎆专栏:《学会C语言》
    🎇座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
    在这里插入图片描述



    👉什么是bug?👈

    Bug作为一个英文单词,实际上它的中文含义并没有“漏洞”这个意思,原意是指小虫子、传染病、着迷以及窃听等等,但是在1947年9月9日之后,“Bug”就变成了错误或者漏洞的代称,据说当时一位计算机专家赫柏正在对17000个继电器进行程序的编辑,奈何突然发现整机停止运作。

    等到工作人员查看了巨大的计算机整体后发现,原来是其中的一组继电器上的触点被一只飞蛾所妨碍了,当时因为这个触点的电压非常高,而飞蛾正好受到光和热的吸引撞在了上面,于是就被电死在触点上,于是赫柏就拿出一个纸条写上了“bug”,也就是虫子的意思,以此来代表“一个电脑程序中的错误”,最终“Bug”的说法也就流传了下来。

    世界上的第一个BUG:
    在这里插入图片描述
    那么我们怎么才能避免写出BUG呢?如果我们想要不写出BUG,就要提升我们调试代码的能力了。

    👉调试是什么?有多重要?👈

    所有发生的事情都一定有迹可循,如果问心无愧,就不需要掩盖也就没有迹象了;如果问心有愧,就必然需要掩盖,那就一定会有迹象。迹象越多就越容易顺藤而上,这就是推理的途径。
    顺着这条途径顺流而下就是犯罪,逆流而上,就是真相。

    而一名优秀的程序员就是一名出色的侦探,拥有强大的调试能力,能够快速地找出BUG。

    每一次调试都是尝试破案的过程。

    相信大家未来都是一名优秀的程序员!但是似乎现在,也有些人像下图那样子去写代码(BUG)。希望大家不要这样子,一定要写出优秀的代码。

    在这里插入图片描述
    而且排查问题也不应该下图那样,一顿乱弄,然后自己都不知道错在哪里和对在哪里。所以我们一定要学会调试代码。
    在这里插入图片描述

    调试是什么?

    调试(英语:Debugging / Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。

    调试的基本步骤

    • 发现程序错误的存在
    • 以隔离、消除等方式对错误进行定位
    • 确定错误产生的原因
    • 提出纠正错误的解决办法
    • 对程序错误予以改正,重新测试

    Debug和Release的介绍

    Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
    Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。

    代码示例:

    #include 
    int main()
    {
    	char* p = "hello bit.";
    	printf("%s\n", p);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上述代码在Debug环境的结果展示:在这里插入图片描述

    上述代码在Release环境的结果展示:
    在这里插入图片描述
    Debug和Release反汇编展示对比:
    在这里插入图片描述
    在这里插入图片描述
    通过上面的对比,我们就可以发现Debug版本和Release版本有着明显的区别,这是因为Debug版本中含有调试信息,而且Release版本是经过优化的。

    那么编译器进行了那些优化呢?我们通过下面的代码来了解一下。

    #include 
    int main()
    {
    	int i = 0;
    	int arr[10] = { 0 };
    	for (i = 0; i <= 12; i++)
    	{
    		arr[i] = 0;
    		printf("hehe\n");
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    如果是debug模式去编译,程序的结果是死循环。而如果release模式去编译,程序没有死循环。这就是优化所带来的的明显差异。

    👉Windows环境调试介绍👈

    调试环境的准备

    在这里插入图片描述
    在环境中选择Debug选项,才能使代码正常调试。如果选择Release选项,是无法进行调试的。

    学会快捷键

    最常使用的几个快捷键:
    F5

    启动调试,经常用来直接跳到下一个断点处。

    F9

    创建断点和取消断点。可以在程序的任意位置设置断点,
    使得程序在想要的位置随意停止执行,继而一步步执行下去。

    F10

    逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。

    F11

    逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最常用的)。

    CTRL + F5

    开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。

    注意:如果你发现你按下F9、F10和F11键,都没有出现效果的话,试着同时按下Fn+F9/F10/F10就可以了。

    F5的使用
    F5一般是配合着F5使用的,F9设置一个断点,然后再按下F5跳到断点处。如果直接按下F5,代码直接运行结束了。举个例子:如果我想直接让箭头执行157行,那么我可以摁一下F9将断点设置在157行,再摁一下F5就能跳到157行了。
    在这里插入图片描述

    在这里插入图片描述
    F11的使用
    F11也是逐语句执行,和F10的功能一样,当时F11可以让执行逻辑进入到函数内部。举个例子:进入调试后,当箭头执行157行时,再摁下F11就可以进入函数内部了。
    在这里插入图片描述
    在这里插入图片描述

    调试的时候查看程序当前信息

    1.查看临时变量的值

    在调试开始之后,监视可用于观察临时变量的值。
    在这里插入图片描述
    在这里插入图片描述
    监视是调试是最最最常用的功能,也是最好用的功能。无论你想查看变量的值,还是变量的地址,都可以使用监视来查看。只要输入变量的名称,就可以查看变量的相关信息了。

    2.查看内存信息

    在调试开始之后,内存用于观察内存信息。
    在这里插入图片描述
    跳出内存窗口之后,可以调整显示的列数。因为整型变量是4个字节,所以我们将其调整为显示4列,可以更好的观察整型的数据。如下图所示:
    在这里插入图片描述
    我们在内存窗口上输入&a,就能找到a的地址,也可以看到a的数据。如果想要观察其他变量,也可以重新输入。如下图所示:
    在这里插入图片描述

    查看调用堆栈

    在调试开始之后,用于查看调用堆栈。
    在这里插入图片描述
    通过调用堆栈,可以清晰的反应函数的调用关系以及当前调用所处的位置。

    4.查看汇编信息

    在调试开始之后,有两种方式转到汇编代码。
    (1)第一种方式:右击鼠标,选择【转到反汇编】:
    在这里插入图片描述

    (2)第二种方式:
    在这里插入图片描述
    以上两种方式都可以查看程序的汇编代码。

    查看寄存器信息

    在这里插入图片描述
    这样就可以查看当前运行环境的寄存器的使用信息。如果大家想对寄存器想有更加深入了解的话,可以看一下这篇博客👉函数栈帧的创建和销毁👈。相信看完之后,你会对函数栈帧、汇编代码和寄存器的了解会有一定的提升。

    👉多动手,敢调试,才能有进步👈

    • 一定要熟练掌握调试技巧。
    • 初学者可能80%的时间在写代码,20%的时间在调试。但是一个程序员可能20%的时间在写程序,但是80%的时间在调试。
    • 现在我们所学的都是一些简单的调试。 以后可能会出现很复杂调试场景:多线程程序的调试等。
    • 多多使用快捷键,提升效率

    👉一些调试的实例👈

    实例一

    实现代码:求 1!+2!+3! …+ n! ;不考虑溢出

    #include 
    int main()
    {
    	int i = 0;
    	int sum = 0;//保存最终结果
    	int n = 0;
    	int ret = 1;//保存n的阶乘
    	scanf("%d", &n);
    	for (i = 1; i <= n; i++)
    	{
    		int j = 0;
    		for (j = 1; j <= i; j++)
    		{
    			ret *= j;
    		}
    		sum += ret;
    	}
    	printf("%d\n", sum);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述

    这时候,如果我们输入3,期待输出9,但是实际输出的是15。
    这是为什么呢?我们就要找出哪里出了问题了。

    1. 首先推测问题出现的原因。初步确定问题可能的原因最好。
    2. 实际上手调试很有必要。
    3. 调试的时候我们要心里有数。

    调试的时候,一定要把监视窗口调出来,方便我们观察每一个变量的值。通过调试,我们可以发现,当 i = 1 , 2 的时候,都没有什么问题。但是当 i = 3 的时候,结果就出问题了。原来问题是处在求阶乘的时候,我们没有将 ret 重新赋值为1,从而导致 ret 还保留着上一次阶乘的结果,然后就出现了问题。在这里,就向大家演示了一次调试代码和找 BUG 的过程,希望大家能学会。
    在这里插入图片描述
    代码修改:

    #include 
    int main()
    {
    	int i = 0;
    	int sum = 0;//保存最终结果
    	int n = 0;
    	scanf("%d", &n);
    	for (i = 1; i <= n; i++)
    	{
    		int ret = 1;//保存n的阶乘
    		int j = 0;
    		for (j = 1; j <= i; j++)
    		{
    			ret *= j;
    		}
    		sum += ret;
    	}
    	printf("%d\n", sum);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述

    实例二

    #include 
    int main()
    {
    int i = 0;
    int arr[10] = {0};
    for(i=0; i<=12; i++)
    {
    arr[i] = 0;
    printf("hehe\n");
    }
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    上面代码的输出结果会是什么呢?我相信很多人,会说程序崩溃了,因为数组越界了。但是其实不是,而是死循环地打印 hehe。那为什么是这样子呢?请看下图:
    在这里插入图片描述

    上面的代码是有意为之的,大家平时一定不要这么写代码,避免数组越界。

    👉如何写出好(易于调试)的代码👈

    优秀的代码:

    1. 代码运行正常
    2. bug很少
    3. 效率高
    4. 可读性高
    5. 可维护性高
    6. 注释清晰
    7. 文档齐全

    常见的coding技巧:

    1. 使用assert
    2. 尽量使用const
    3. 养成良好的编码风格
    4. 添加必要的注释
    5. 避免编码的陷阱

    一个漂亮的示例

    模拟实现strcpy函数

    strcpy函数的原型如下:

    char * strcpy ( char * destination, const char * source );
    
    • 1

    在这里插入图片描述
    代码示例:

    
    #include 
    #include 
    char* my_strcpy(char* dst, const char* src)
    {
    	assert(dst && src);
    	char* ret = dst;//借助ret记住dst的首元素地址
    	while (*dst++ = *src++)
    	{
    		;
    	}
    	return ret;
    }
    
    int main()
    {
    	char arr1[] = "******************";
    	char arr2[] = "hello Joy";
    	printf("%s\n", my_strcpy(arr1, arr2));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    1.assert的作用

    assert是断言的意思,如果assert括号内的表达式为真,就什么事都不会发生。但是如果assert括号内的表达式为假,那么就会直接给你报错,哪行哪行出现了上面错误。见下图示例:
    在这里插入图片描述

    2.函数返回值类型的设计

    其实上面的 my_strcpy 函数的返回值类型也可以是 void 类型,那为什么我们不把 my_strcpy 函数的返回值类型设置为 void 类型呢?因为将返回值类型设置为 char* 类型,可以实现函数的嵌套调用。

    比如上面的printf("%s\n", my_strcpy(arr1, arr2)); 语句,我可以直接将my_strcpy(arr1, arr2)的结果打印在屏幕上。

    3.’ \0 ’ 的拷贝

    上面的 my_strcpy 函数还有几个需要注意的点,第一,就是 ‘\0’ 的拷贝。当数组arr2的内容全部拷贝到数组arr1后,my_strcpy 函数会自动在后面加上 ‘\0’,作为字符串结束的标志。那现在就来调试起来看一下,是不是这样子的。

    演示代码:

    #include 
    #include 
    char* my_strcpy(char* dst, const char* src)
    {
    	assert(dst && src);
    	char* ret = dst;//借助ret记住dst的首元素地址
    	while (*dst++ = *src++)
    	{
    		;
    	}
    	return ret;
    }
    
    int main()
    {
    	char arr1[] = "******************";
    	char arr2[] = "hello Joy";
    	my_strcpy(arr1, arr2);
    	printf("%s\n", arr1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述
    可以看到,my_strcpy将 ‘\0’ 也拷贝过去了,其实库函数strcpy也会将 ‘\0’ 拷贝过去。

    第二个需要注意的点就是,使用 strcpy 和 my_strcpy 函数一定要确定目的数组的空间足以容纳源头数组的内容,否则将会出现一些意想不到的问题。

    4.const的作用

    我们可以看到,上面 my_strcpy 的第二个参数 char* src 用了 const 来修饰。那么用 const 来修饰指针变量有什么意义呢?先告诉大家结论。

    结论:

    conts修饰指针变量

    • const 如果放在 * 的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本身的内容可变。
    • const 如果放在 * 的右边,修饰的是指针变量本身,保证指针变量的内容不能修改,但是指针指向的内容可以通过指针来改变。

    举个例子:

    #include 
    int main()
    {
    	int n = 10;
    	int m = 20;
    	int const* p = &n;// const int*p = &n 等价于 int const *p = &n
    	
    	//*p = 100; //error,const放在*的左边,不可以改变指针指向的内容
    	p = &m;//可以改变指针变量本身的内容
    	printf("%d\n", *p);
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    #include 
    int main()
    {
    	int a = 10;
    	int b = 20;
    	int* const p = &a;
    	
    	//p = &b; //error,const放在*的右边,不可以改变指针变量本身的内容
    	*p = 100;//可以改变指针指向的内容
    	printf("%d\n", *p);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    👉常见的错误👈

    编译型错误

    直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定,相对来说简单。

    在这里插入图片描述
    这种错误是初学者最容易犯的错误,但是随着代码能力的提升,这种错误也会越来越少。

    链接型错误

    看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误

    在这里插入图片描述

    运行时错误

    运行时错误是最难搞的错误,需要借助调试,逐步定位问题。

    运行时错误可以由很多原因导致的,所以大家写代码的时候要细心一点。在这里就不跟大家讲解了,主要是借助调试来解决运行时错误。

    温馨提示:

    做一个有心人,积累拍错经验。

    在这里插入图片描述

    👉总结👈

    本篇博客主要讲解了调试技巧、如何写出好的代码已经常见的错误等等。以上就是本篇博客的全部内容,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家啦!!!💖💝❣️

  • 相关阅读:
    跑跑飞弹室外跑步AR游戏代码方案设计
    阻抗与导纳的理解
    论文中文翻译——A deep tree-based model for software defect prediction
    Codeforces Round 439 (Div. 2) C. The Intriguing Obsession
    激光切割机在现代灯具的生产过程中的应用
    Sparse Merkle Tree
    thinkphp 项目报错No input file specified.
    Mac 中 vim 插件配置 —— 以YouCompleteMe 为例
    二进制矩阵(秋季每日一题 2)
    npm 设置取消代理
  • 原文地址:https://blog.csdn.net/m0_63639164/article/details/126141959