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

news2024/9/24 18:31:40

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

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

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

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

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/710117.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

找不到msvcp140.dll无法继续执行代码,请重新安装软件MSVCP140.dll,怎么解决?

计算机在运行软件程序或者游戏的时候&#xff0c;提示“找不到msvcp140.dll无法继续执行代码,请重新安装软件”&#xff0c;无法正常启动运行。这个是因为电脑系统中的msvcp140.dll文件丢失或者损坏了&#xff0c;msvcp140.dll是一种动态链接库文件&#xff0c;它是由Microsoft…

显卡资源使用

1.首先&#xff0c;使用校园网访问http://202.206.212.218:8000/&#xff0c;&#xff08;测试用的ip&#xff09;&#xff0c;部署后为http://59.67.235.242:8000/&#xff0c;出现如下登录界面。 2.目前有9个用户&#xff0c;用户名和密码设置如下&#xff1a; UsernamePass…

最强优化指令大全 | 【Linux技术专题】「系统性能调优实战」终极关注应用系统性能调优及原理剖析(下册)

Linux命令相关查看指标 CPU 指标 vmstat指令 vmstat -n m该命令用于每隔n秒采集系统的性能统计信息&#xff0c;共采集m次。 [rootsvr01]$ vmstat 1 3procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----r b swpd free buff cache …

基于JSP+Servlet的文件上传与下载

基于JSPServlet的文件上传与下载 一、系统介绍二、功能展示1.项目骨架2.单文件上传3.多文件上传4.下载文件1.其他系统实现五.获取源码 一、系统介绍 项目类型&#xff1a;Java web项目 项目名称&#xff1a;基于JSPServlet的文件上传与下载案例 项目架构&#xff1a;B/S架构…

基于单片机智能洗衣机设计与实现

功能介绍 以51单片机作为主控系统&#xff1b;利用STC89C52单片机进行数据处理&#xff1b; 通过2路继电器分别控制洗衣机进水、出水相关逻辑运算&#xff1b;采用L298去掉直流电机实现滚筒正反转&#xff1b;通过单片机进行处理数据&#xff0c;把采集到的数据通过LCD液晶显示…

由于找不到msvcr110.dll,无法继续执行的三个可行修复方案

MSVCR110.dll是一种动态链接库文件&#xff0c;它是由Microsoft Visual Studio 2012的C运行时库的一部分。该文件主要负责提供C代码在Windows操作系统上运行所需的运行时支持。是Windows操作系统中非常重要的文件&#xff0c;如果文件出现损坏或者丢失&#xff0c;计算机系统就…

springboot+echarts+mysql制作数据可视化大屏(滑动大屏)

作者水平低&#xff0c;如有错误&#xff0c;恳请指正&#xff01;谢谢&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 项目简单&#xff0c;适合大学生参考 分类专栏还有其它的可视化博客哦&#xff01; 专栏地址&#xff1a;https://blog.csdn.net/qq_559…

【C】初步认识

目录 【1】什么是C语言 【2】第一个C程序解读 【3】数据类型 【4】变量常量 【4.1】定义变量的方法 【4.2】变量的分类 【4.3】变量的使用 【4.4】变量的作用域和生命周期 【4.5】常量分类 【5】字符串 【6】转义字符 【7】注释 【8】选择语句 【9】循环语句 【…

牛客网Java面试题及答案整理( 2023最新版)

大家从 Boss 直聘上或者其他招聘网站上都可以看到 Java 岗位众多&#xff0c;Java 岗位的招聘薪酬天差地别&#xff0c;人才要求也是五花八门。而很多 Java 工程师求职过程中&#xff0c;也是冷暖自知。很多时候技术有&#xff0c;但是面试的时候就是过不了&#xff01; 为了帮…

SpringSecurity对CSRF的支持实践

【1】什么是CSRF 跨站请求伪造&#xff08;英语&#xff1a;Cross-site request forgery&#xff09;&#xff0c;也被称为 one-click attack 或者 session riding&#xff0c;通常缩写为 CSRF 或者 XSRF&#xff0c; 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操…

chatgpt赋能python:如何用Python计算居民用电量

如何用Python计算居民用电量 介绍 居民用电量是一个重要的经济指标。对于一个家庭来说&#xff0c;如果能够掌握自己的用电量情况&#xff0c;不仅可以控制开支&#xff0c;还可以提高用电效率&#xff0c;节约能源。而对于电力公司来说&#xff0c;了解居民用电量的变化规律…

vue 实现色板功能

效果&#xff1a; 动态添加颜色 随机色 代码&#xff1a; <divclass"mt-10 firstTitle"v-show"pictureType ! card && pictureType ! table && pictureType ! inventory"><i:class"[colorSystemShow ? el-icon-com-xia…

关于antd的form表单组件的一个天坑。。。

事情是这样的&#xff0c;项目中遇到了一个问题&#xff0c;用表单包裹着着一个Switch组件&#xff0c;提交表单的时候可以将Switch的值一起提交。 form.setFieldsValue({power:0})<Form.Item label"Switch" name"power"><Switch checked{flag}…

autodl算力租用平台应用于pycharm

一、GPU租用选择 1、创建实例 首先进入算力市场 博客以2080为例&#xff0c;选择计费方式&#xff0c;选择合适的主机&#xff0c;选择要创建实例中的GPU数量&#xff0c;选择镜像&#xff08;内置了不同的深度学习框架&#xff09;&#xff0c;最后创建即可 2、SSH远程连…

第五章 linux编译器——gcc/g++的使用

第五章 linux编译器——gcc/g的使用 一、编辑器与编译器的区别二、gcc/g的编译过程前言1、阶段1&#xff1a;预处理&#xff08;头文件、宏的替换&#xff09;&#xff08;1&#xff09;作用&#xff08;2&#xff09;指令&#xff08;3&#xff09;示例 2、阶段2&#xff1a;编…

Linux--用户身份切换: su

①普通用户切换成超级用户且更改路径&#xff1a;su - ②普通用户切换成超级用户且不更改路径&#xff1a;su root 或者 su ③(由普通用户切换来的)超级用户切换回普通用户&#xff1a;Ctrld ④超级用户切换成普通用户&#xff1a;su 普通用户名 ⑤普通用户a切换成普通用户b…

Jetson Nano Swap交换空间增加

依次输入以下命令&#xff0c;可以使交换空间增加3G&#xff0c;解决一些耗尽内存的程序出错。 sudo fallocate -l 3G /var/swapfile sudo chmod 600 /var/swapfile sudo mkswap /var/swapfile sudo swapon /var/swapfile sudo bash -c echo "/var/swapfile swap swap de…

金九银十1060+ 道 Java面试题及答案整理(2023最新版)

前言 今年的金三银四可是被裁员疫情搞得人心慌慌&#xff0c;由于大厂纷纷裁员&#xff0c;面试的竞争难度又上一层&#xff0c;不知道你是否在金三银四中拿到 offer&#xff1f;不过这些都过去了&#xff0c;现在马上迎来的是金九银十&#xff0c;按照往年来说&#xff0c;秋…

python | 识别项目中的接口并生成接口文档

识别项目中的接口并生成接口文档 前言起点用途使用方法控制台展示文档内容展示代码注意事项 前言 前段时间也是来了一场说走就走的旅行&#xff0c;去看了看祖国的大好河山&#xff0c;不得不说也是一场让我难忘的旅行&#xff0c;可惜钱包太扁了&#xff0c;禁不起我的的折腾…

NOSQL之redis配置和安装

关系数据库与非关系型数据库 ●关系型数据库&#xff1a; 关系型数据库是一个结构化的数据库&#xff0c;创建在关系模型&#xff08;二维表格模型&#xff09;基础上&#xff0c;一般面向于记录。 SQL 语句&#xff08;标准数据查询语言&#xff09;就是一种基于关系型数据库…