【C语言】实用调试技巧(vs2019)

简单不先于复杂,而是在复杂之后。

89efcc89ac61428db4d5b6639b2bd948.jpeg

目录

 1. 什么是bug?

2. 调试是什么?

 2.1 调试定义

 2.2 调试的基本步骤

2.3 Debug 和 Release 的介绍 

3. Windows 环境调试介绍 

3.1 调试环境的准备 

3.2 学会快捷键 

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

3.3.1 查看临时变量的值 

3.3.2 查看内存信息

3.3.3 调用堆栈 

3.3.4 查看汇编信息

3.3.5 查看寄存器信息

4. 一些调试的实例 

4.1 实例一

4.2  实例二

5. 如何写出好(易于调试)的代码

5.1 优秀的代码 

5.2 示范: 

5.3 const 的作用 

5.4 模拟实现 strlen

6. 编程常见错误(语法错误)

6.1  编译型错误(语法错误)

6.2 链接型错误

6.3 运行时错误 

 


 

 1. 什么是bug?

导致计算机不能正常工作的错误叫做bug。

2. 调试是什么?

所有发生的事情都一定有迹可循

如果问心无愧,就不需要掩盖也就没有迹象了

如果问心有愧,那就一定会有迹象

迹象越多越容易顺藤而上,这就是推理的途径

顺着这条途径顺流而下就是犯罪,逆流而上,就是真相

一名优秀的程序员是一名优秀的侦探。

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

一定要拒绝迷信式调试,也就是单纯靠猜去调试,而不去想产生 bug 的前因后果。

 2.1 调试定义

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

 2.2 调试的基本步骤

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

2.3 Debug 和 Release 的介绍 

Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。 

Release 通常称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。

下面是在文件夹中该程序保存路径下的 Debug 版本:

下面是 Release 版本:

其中 Release 版本中没有调试信息。

其实两者之间在其他方面也有差异,在以后的文章中会有体现。

3. Windows 环境调试介绍 

注:Linux 开发环境调试工具是 gdb,在之后会有介绍。 

3.1 调试环境的准备 

我使用的是 vs2019 的编译器,首先把这个地方调成 Debug ,保证是一个可以让我们调试的版本。 

3.2 学会快捷键 

 

下面是最常用的几个快捷键:

F5:

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

注:在 vs2019 中按 F5 直接显示运行结果,并且运行结果的窗格不会出现一闪而过的情况,但是这只是 vs2019 的编译器的特点,不代表在其他的编译器不会出现这种问题,其实是我们没有按对。F5 仅仅是启动调试,代码一行行向下执行,如果没有一个东西拦截它,就会执行到程序结束为止。

所以 F5 并不是单独使用的,要和 F9 配合使用。

F9:

创建断点和取消断点。

断点的重要作用:可以在程序的任意位置设置断点

这样就可以使得程序在想要的位置随意停止执行,进而一步步执行下去。

(比如当我们觉得程序的错误在后面的部分,就可以借助打断点,在可能的错误部分前停止执行,然后一步一步执行下去,进行调试)

F10

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

在调试时,点出了这样的断点,按下 F5 ,箭头并不会从第一个断点处直接跳到下一个断点,而是进入逻辑上的下一个断点,因为循环要执行10次,逻辑上还要再进入循环,所以按 F5 只会执行循环一次,所以 i 会 +1。

小技巧:如果想保留断点,但是不想让它起作用,可以禁用断点。

禁用断点之后断点就会变成空心的,如下图:

如果我们怀疑在循环里的第某次循环后有问题,可以将断点设置为调试断点。

右键断点选择条件,然后输出条件表达式,比如我怀疑该循环的第五次循环之后有问题,就写成下面的样子:

 

当我们按 F5 启动调试时,会自动执行到 i == 5 的地方

下附代码供大家亲手实践:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int main()
{
	int i = 0;
	int arr[10] = { 0 };
	//赋值
	for (i = 0; i < 10; i++)
	{
		scanf("%d", &arr[i]);
	}

	//打印
	for (i = 0; i < 10; i++)
	{
		printf("%d ", i);
	}
	
	for (i = 0; i < 10; i++)
	{
		printf("%d ", i);
	}

	return 0;
}

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

3.3.1 查看临时变量的值 

在调试开始之后,我们可以用下面的方法观察变量的值。

 以上都是我们在调试过程中可以观察的东西。

自动窗口:就是无需我们手动输入,而是自动地将我们上下文程序中创建的变量罗列到这个窗口供我们观察。

 

但是不够方便,因为如果进入 Add 函数,自动窗口就只会显示 x、y变量,之前的变量不显示,当我们想观察更多的信息的时候就非常不方便,所以不常用。

局部变量:会把程序执行过程中上下环境中的局部变量罗列到窗格中,进入函数时变量会反复切换,所以也不够方便。 

监视: 我们可以输入任何想要观察的变量以及合法的表达式,上下文环境以及之前的变量数据都会保留,便于我们对比调试。

所以监视窗口是我们最常用的。

我们也可以同时打开多个监视窗口,如下图:

下附代码,供大家亲手实践:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int Add(int x, int y)
{
    return x + y;
}

int main()
{
    int a = 10;
    int b = 20;
    int c = Add(a, b);
    printf("%d\n", c);

    return 0;
}

小技巧:

 

我们发现,把数组名传过去,观察形参的数组名 a ,只能观察到数组内的第一个元素,因为实参是数组名,把数组首元素地址传了过去。

要想显示数组中更多的元素,需要用到一个小技巧,比如我们要观察数组的前四个元素,要写成下面的样子:

这样,我们就可以连续看到数组中的一串数据。 

下附代码供大家亲手实践:

#define _CRT_SECURE_NO_WARNINGS 1

void test(int a[])
{
    //
}

int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    test(arr);

    return 0;
}

3.3.2 查看内存信息

3.3.3 调用堆栈 

 

 通过调用堆栈,可以清晰地反映函数的调用关系以及当前调用所处的位置。

下附代码,供大家亲手实践:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

void test2()
{
    printf("hehe\n");
}

void test1()
{
    test2();
}

void test()
{
    test1();
}

int main()
{
    test();

    return 0;
}

3.3.4 查看汇编信息

 有两种方式转到反汇编

第一种:

第二种(直接鼠标右键):

然后我们就i可以切换到汇编代码:

下附代码,供大家亲手实践:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int main()
{
    char arr[] = "abcdef";
    printf("%s\n", arr);

    return 0;
}

3.3.5 查看寄存器信息

可以查看当前运行环境的寄存器的使用信息:

有两种方式可以观察寄存器的信息:

第一种:

第二种:

如果知道这些寄存器的名字,也可以用监视窗口来观察。

寄存器的信息会随着代码一行行的执行而发生变化。

下附代码供大家实践:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int main()
{
    int a = 10;
    int b = 20;
    int c = a + b;
    printf("%d\n", c);

    return 0;
}

4. 一些调试的实例 

4.1 实例一

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

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

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;
}

我们输入3,期待输出9,但实际输出的是15

这个时候就需要我们上手一行一行地逐步调试,经过调试后我们发现,外层的第三次循环出了问题,ret 依次乘以2和3结果不是6,而是12,在监视窗口我们容易看到,ret 变量在每次计算阶乘之前没有重置为 1,所以会导致计算错误。

正确的代码如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

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;
		ret = 1;  // 重置ret为1
		for (j = 1; j <= i; j++)
		{
			ret *= j;
		}
		sum += ret;
	}
	printf("%d\n", sum);

	return 0;
}

4.2  实例二

 研究程序死循环的原因;

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for (i = 0; i <= 12; i++)
	{
		arr[i] = 0;
		printf("hehe\n");
	}

	return 0;
}

 

 

通过调试,我们发现,当 i == 12 时,arr[i]的值也变成了12,在执行 arr[i] = 0; 这条语句时,i 的值也变成了0,造成了死循环。

5. 如何写出好(易于调试)的代码

5.1 优秀的代码 

  • 代码运行正常
  • bug 很少
  • 效率高
  • 可读性高
  • 可维护性高
  • 注释清晰
  • 文档齐全 

常见的 coding 技巧:

1. 使用 assert

2. 尽量使用 const

3. 养成良好的编码风格

4. 添加必要的注释

5. 避免编码的陷阱

5.2 示范: 

模拟实现库函数:strcpy

下面是利用库函数 strcpy 解决问题的一个示例:

通过调试不难发现,在拷贝的时候,会把源字符串中的 \0 也拷贝过去。

下附代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include<string.h>
#include<stdio.h>

int main()
{
    char arr1[20] = "XXXXXXXXXXXXX";
    char arr2[] = "hello,world!";
    //strcpy 在拷贝的时候, 会把源字符串中的\0也拷贝过去
    strcpy(arr1, arr2);

    printf("%s\n", arr1);

    return 0;
} 

接下来我们模拟实现 strcpy

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<string.h>
#include<stdio.h>

void my_strcpy(char* dest, char* src)
{
    while (*src != '\0')
    {
        *dest = *src;
        dest++;
        src++;
    }
    *dest = *src;
}

int main()
{
    char arr1[20] = "XXXXXXXXXXXXX";
    char arr2[] = "hello,world!";
    //strcpy 在拷贝的时候, 会把源字符串中的\0也拷贝过去
    my_strcpy(arr1, arr2);

    printf("%s\n", arr1);

    return 0;
}

这个代码其实不算好,还可以改进优化:

#define _CRT_SECURE_NO_WARNINGS 1
#include<string.h>
#include<stdio.h>

void my_strcpy(char* dest, char* src)
{
    while (*dest++ = *src++)
    {
        ;
    }
    
}

int main()
{
    char arr1[20] = "XXXXXXXXXXXXX";
    char arr2[] = "hello,world!";
    //strcpy 在拷贝的时候, 会把源字符串中的\0也拷贝过去
    my_strcpy(arr1, arr2);

    printf("%s\n", arr1);

    return 0;
}

如果传到函数中的指针是空指针也会出现问题,所以我们使用断言,如果传了空指针就会报错。

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<string.h>
#include<stdio.h>
#include<assert.h>

void my_strcpy(char* dest, char* src)
{
    //断言
    assert(src != NULL);
    assert(dest != NULL);
    while (*dest++ = *src++)
    {
        ;
    }
    
}

int main()
{
    char arr1[20] = "XXXXXXXXXXXXX";
    char arr2[] = "hello,world!";
    //strcpy 在拷贝的时候, 会把源字符串中的\0也拷贝过去
    my_strcpy(arr1, arr2);

    printf("%s\n", arr1);

    return 0;
}

strcpy 是把源字符串的字符拷贝到目标字符串,如果 while 循环中二者写反了也会出现问题,所以我们使用 const 令 src 所指向的对象不可修改:

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<string.h>
#include<stdio.h>
#include<assert.h>

void my_strcpy(char* dest, const char* src)
{
    //断言
    assert((dest && src) != NULL);
    while (*dest++ = *src++)
    {
        ;
    }
    
}

int main()
{
    char arr1[20] = "XXXXXXXXXXXXX";
    char arr2[] = "hello,world!";
    //strcpy 在拷贝的时候, 会把源字符串中的\0也拷贝过去
    my_strcpy(arr1, arr2);

    printf("%s\n", arr1);

    return 0;
}

如果写代码是二者颠倒会出现这样的情况,const 对源字符串就起到了一个很好的保护作用。

 如果还要继续追究的话,我们可以把函数返回值改为 char*

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<string.h>
#include<stdio.h>
#include<assert.h>


//返回 char* 是为了实现链式访问
//也就是把这个函数的返回值作为另一个函数的参数
//strcpy函数返回的是目标空间的起始地址

char* my_strcpy(char* dest, const char* src)
{
    char* ret = dest;
    //断言
    assert((dest && src) != NULL);
    while (*dest++ = *src++)
    {
        ;
    }
    return ret;
}

int main()
{
    char arr1[20] = "XXXXXXXXXXXXX";
    char arr2[] = "hello,world!";
    //strcpy 在拷贝的时候, 会把源字符串中的\0也拷贝过去
    my_strcpy(arr1, arr2);

    printf("%s\n", arr1);

    return 0;
}

5.3 const 的作用 

 

const 修饰指针变量

1. const 放在 * 的左边
const int* p = &num;//一般用这种
int const* p;
p指向的对象不能通过p来改变了,但是p变量本身的值是可以改变的

2. const 放在* 的右边
int* const p = &num;
p指向的对象是可以哦通过p来改变的,但是不能来修改p本身的值

5.4 模拟实现 strlen

 

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
//求字符串长度

int my_strlen(const char* str)
{
    int count = 0;
    assert(str);
    while (*str != '\0')
    {
        count++;
        str++;
    }
    return count;
}

int main()
{
    char arr[] = "hello,world";
    int len = my_strlen(arr);
    printf("%d\n", len);

    return 0;
}

6. 编程常见错误(语法错误)

6.1  编译型错误(语法错误)

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

6.2 链接型错误

 出现在链接期间

看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。

一般是标识符名不存在或者拼写错误。

 而且这种错误无法通过双击错误信息来解决,可以用 Ctrl + F 来查找无法解析的符号来确定位置。

 

6.3 运行时错误 

没有报错,但是结果没有符合预期。是最难解决的问题。

我们要借助调试,逐步定位问题。

 

 

 

  • 7
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: C语言编程技巧CHM,即C语言编程技巧帮助文档(Compiled HTML Help),是针对C语言编程的技巧和知识进行整理和总结形成的帮助文档。 C语言是一种广泛应用于系统软件和应用软件开发的高级编程语言。在C语言编程中,掌握一些技巧和知识可以提高代码的效率和可读性。编程技巧CHM可以对这些技巧进行详细讲解和演示,帮助初学者快速入门,同时也为有经验的开发者提供更深入的理解和应用。 编程技巧CHM中可能包含的内容有:常用的编码规范和命名规则、优化和调试技巧、常用算法和数据结构等。通过学习和掌握这些技巧开发者可以更好地组织和设计程序,提高代码的质量和可维护性。 CHM格式的帮助文档具有良好的交互性和可扩展性,可以方便地进行搜索和索引,同时也可以方便地添加自定义的内容和链接。使用CHM格式的编程技巧文档,开发者可以在需要时快速查询和查找相关的内容,提高学习和开发效率。 总之,C语言编程技巧CHM是一种在C语言编程中非常有用的资源,能够帮助开发者更好地理解和应用C语言技巧和知识,提高代码的质量和效率。无论是初学者还是有经验的开发者,都可以从中受益。 ### 回答2: C语言编程技巧CHM是一本关于C语言编程技巧的电子书,CHM是一种帮助文件格式。本书主要介绍了C语言编程中一些常用的技巧和方法,旨在帮助读者提高编程效率和代码质量。 首先,本书介绍了一些常用的C语言编程技巧,例如如何优化代码、避免一些常见的编程陷阱等。这些技巧可以帮助读者更加熟悉C语言的特性,以及掌握一些高效而安全的编程方法。 其次,本书还介绍了一些与C语言编程相关的工具和库。例如,介绍了如何使用C语言的标准库函数和数据结构,如何使用常用的开发工具,如编译器和调试器等。这些工具和库可以帮助读者更好地进行C语言编程,并提高开发效率。 此外,本书还讲解了一些常用的设计模式和算法。这些设计模式和算法是C语言编程中的一些常用技巧,可以帮助读者更好地解决实际编程中的问题。例如,介绍了常用的排序算法、搜索算法以及一些常用的数据结构等。 总而言之,C语言编程技巧CHM是一本对C语言编程非常实用的电子书。通过阅读本书,读者可以学习和掌握一些高效的C语言编程技巧和方法,提高自己的编程水平和能力。无论是初学者还是有一定编程经验的人都可以从本书中受益,为自己的C语言编程之路打下坚实的基础。 ### 回答3: C语言编程技巧是指在使用C语言进行编程时,运用一些技巧和方法来提高代码的效率和可读性。下面简要介绍几个常用的C语言编程技巧。 首先是代码注释。良好的代码注释能够提高代码可读性,便于他人理解和维护代码。在编写C语言代码时,应当养成良好的注释习惯,对于每个函数、变量以及复杂的操作,都应有相应的注释来解释其功能和作用。 其次是错误处理和异常处理。在编写代码时,应养成及时捕捉和处理错误的习惯,避免因漏掉错误处理而导致代码的异常运行或崩溃。对于可能出现异常的情况,可以使用条件判断语句来捕捉并处理错误,保证程序的稳定性和健壮性。 另外,利用函数和模块化编程也是提高C语言编程效率的重要技巧。将代码逻辑分成多个函数,使得每个函数只完成特定的功能,降低代码的复杂性,提高代码的可维护性。可以将一些常用的功能封装成独立的模块,以便在其他项目中复用。 此外,还有一些常用的编程技巧,如避免全局变量的使用,使用宏定义提高代码的可读性,合理选择循环结构和条件语句等等。 总结来说,C语言编程技巧是一种提高代码效率和可读性的方法和技巧,包括良好的注释习惯、错误处理、模块化编程以及其他一些常用的编程技巧。这些技巧的有效运用可以提高代码的质量和开发效率,是每个C语言程序员需要掌握的基本技能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

2024_极限年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值