《C语言深度解剖》(8):一篇文章彻底学会Visual Studio 调试技巧,新手必看!

🤡博客主页:醉竺

🥰本文专栏:《C语言深度解剖》

😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!


✨✨💜💛想要学习更多数据结构与算法点击专栏链接查看💛💜✨✨ 


1. 什么是bug?

第一次被发现的导致计算机错误的飞蛾,也是第一个计算机程序错误。 

2. 调试是什么?有多重要? 

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

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

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

我们是如何写代码的? 

又是如何排查出现的问题的呢? 

下面进入正题! 

2.1 调试是什么? 

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

2.2 调试的基本步骤

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

2.3 Debug和Release的介绍 

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

代码:

#include <stdio.h>
int main()
{
	char* p = "hello bit.";
	printf("%s\n", p);
	return 0;
}

上述代码在Debug环境的结果展示:

上述代码在Release环境的结果展示:

Debug和Release反汇编展示对比:

所以我们说调试就是在Debug版本的环境中,找代码中潜伏的问题的一个过程。 

那编译器进行了哪些优化呢? 请看如下代码:

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

如果是 debug 模式去编译,程序的结果是死循环。

如果是 release 模式去编译,程序没有死循环。

那他们之间有什么区别呢? 就是因为release模式优化导致的。 

变量在内存中开辟的顺序发生了变化,影响到了程序执行的结果。 

上述代码在下面调试案例中会详细讲解,这里只是简单说一些该程序在Debug模式和Release模式下的区别。

3. Windows环境调试介绍

注:linux开发环境调试工具是gdb,后面我会单独开一个专栏进行学习。

3.1 调试环境的准备 

在环境中选择 debug 选项,才能使代码正常调试。

3.2 学会快捷键 

最常用的调试快捷键其实就5个: 

  • F10 逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
  • F11 逐语句,就是每次都执行一条语句,这个快捷键可以使我们的执行逻辑进入函数内部(这是最常用的)。
  • F9 创建断点和取消断点 ,可以在程序的任意位置设置断点。
  • F5 启动调试,经常用来直接跳到下一个断点处。
  • F9和F5这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。
  • CTRL + F5 开始执行程序,如果你想让程序直接运行起来而不调试就可以直接使用。

3.2.1 演示CTRL + F5

3.2.2. 演示F10和F11 

在着手调试程序时,开发者通常会依赖于F10和F11这两个关键的快捷键。这两个键的主要区别在于它们对函数调用的处理方式。

  1. 当你按下F11时,调试器会进入函数的内部,允许你逐行执行函数中的代码,这被称为“单步执行进入”。这种方式非常适合于深入理解函数的具体行为和逻辑。
  2. 相对地,F10键用于“单步执行”,当遇到函数调用时,它不会进入函数内部,而是将整个函数视为一个步骤来执行。这意味着它会直接跳过函数的内部细节,只显示函数调用的结果。这种方法在你知道函数已经正常工作,或者不关心函数内部细节时非常有用。
  3. 总结来说,F11用于深入函数内部进行详细调试,而F10则用于快速遍历代码而不深入每个函数的细节。合理使用这两个快捷键可以大大提高调试效率。

接下来演示F10和F11实际中的区别:

  • 按下 F10 或者 F11进入调试过程

  • 按F10,执行 test() 函数

我们发现,按F10逐过程遇到test()函数时,会直接显示该函数运行的最终结果并不会进入test()函数的内部细节。

  

我们看看如果运行到test()函数时,按F11是怎样的 

  • 按F11,执行 test() 函数

我们发现,按F11逐语句遇到test()函数时,调试器箭头会进入该函数的内部,允许我们逐条执行并函数内部的代码细节。直到该函数执行完毕,然后箭头跳转到main()函数中该函数调用语句的下一条指令。


3.2.3 演示F5和F9

F5和F9通常是配合着使用的。 

在处理较长的程序时,手动逐句执行到特定代码行会非常耗时。为了提高效率,我们通常会使用断点来标记我们关心的代码行。

  1. 按下F9键,这样可以在特定的代码行上设置一个标记,指示调试器在该行暂停执行。如果需要取消断点,再次按下F9键即可。 
  2. 一旦断点设置完毕,我们可以通过按下F5键启动调试。程序将开始执行,直到遇到第一个断点。
  3. 如果程序中有多个断点,我们可以继续按下F5键,让程序从一个断点跳转到下一个断点
  4. 这时,执行将暂停,允许我们检查当前状态、变量值以及其他相关的调试信息。
  5. 这样就可以只关注那些我们怀疑可能存在问题或者想要深入了解的代码区域。

下面这张图是一个断点的情况:

下面这张图是多个断点的情况: 

1.分别在13行和22行按 F9 设置两个断点

2.按下F5键,程序跳转到第一个断点处,并且断点之前的代码都已执行 

3.继续按下F5,程序从第一个断点处跳到第二个断点处,第二个断点之前的程序都已执行。

补充1:条件断点 

在调试过程中,我们可以通过为断点添加条件来进一步细化控制程序执行的行为。这样,即使程序运行到该断点,也不会立即暂停,除非满足我们设定的条件。设置条件断点的方法通常是在断点属性中进行配置,这可以通过右键点击断点并选择“条件”来设置。 

例如,假设我们有一个循环,我们只想在循环变量达到特定值时才暂停执行。我们可以在循环内的代码行上设置一个断点,并为其添加一个条件,比如“当循环变量等于某个值时暂停”。这样,当我们按下F5键启动调试时,程序将正常运行,直到循环变量满足我们设定的条件,此时程序会在该断点处暂停。 

下面将演示条件断点的设置:在for循环中,我们在设置i==5时,程序会停下来。

具体步骤:1.按F9设置断点 —>2.鼠标右键断点—>3.点击条件并设置—>4.按F5执行调试—>5.执行到条件断点处程序停下。

补充2:查看断点的数量和信息 

当程序特别长有设置了特别多的断点时,我们想查看所有断点的数量和信息可以按照以下步骤:

点击 1.调试 —> 2.窗口—>3.断点 

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

调试的时候除了上述的几个快捷键,最重要的就是使用调试窗口中的几个功能。如下图所示:

注意:下面窗口的功能,首先进入调试中才会有,正常运行程序是不会有的。 

3.3.1 自动窗口和临时变量窗口(了解)

点击自动窗口或者局部变量窗口,逐步调试时,自动窗口和局部窗口中会短暂的自动展示一些变量的值,自动窗口中包括全局变量以及局部变量;局部变量窗口则只展示局部变量的值。但是继续调试过程中,两个窗口编译器就不再显示变量值,我们就无法观察了。所以说这两个窗口并不实用也不常用,这里只是简单提一下。

后续主要用的就是监视窗口。

1.自动变量窗口 

2.局部变量窗口 

3.3.2 查看变量的值 

点击“监视”,可以打开好几个窗口,同时窗口的位置可以长按拖动。 

监视窗口可以添加你想观察的成员信息,包括并不限于各种变量,数组,结构体,指针等,以及它们的地址。如下图所示:

3.3.2 查看内存信息 

内存窗口显示的内容所代表的具体含义:

3.3.3 查看调用堆栈  

当多个函数嵌套调用的时候,如果想理清每个函数之间的关系,可以查看调用堆栈窗口 

下面请看函数栈的堆叠以及释放:

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

3.3.4 查看汇编信息 

在调试开始之后,有两种方式转到汇编: 

(1) 第一种方式:右击鼠标,选择【转到反汇编】: 

就能看到汇编代码了:

(2)第二种方式:

3.3.5 查看寄存器信息  

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

3.3.6 小细节补充

在C或C++等编程语言中,数组作为函数参数传递时,它们会退化为指针。这意味着数组不再携带其原始的长度信息,因此在调试器的监视窗口中查看这样的参数时,我们无法直接看到整个数组的细节。

解决方法: arr,number

4.多动手,尝试调试,才能进步

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

5. 调试实战 

5.1 实例一 

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

下面代码语法没问题,但是逻辑有问题,我们需要调试查出来: 

这时候我们如果3,期待输出9,但实际输出的是15。 why? 这里我们就得找我们问题。 

1. 首先推测问题出现的原因。初步确定问题可能的原因最好。

2. 实际上手调试很有必要。

3. 调试的时候一些中间过程和结果我们至少是心里有数的。 

所以正确代码应该是:

5.2 实例二 

看下面这段代码有没有什么问题?运行结果是什么? 

运行结果:程序陷入了死循环!

这是怎么回事?

我们第一眼观察到这个代码的时候,首先观察到的应该是“数组的越界访问”,因为这类数组只有10个数,下标访问应该到9。一般情况下越界访问应该直接会报错,非法访问,但是这里为什么会陷入死循环呢?

所以我们需要对程序进行调试,研究程序死循环的原因。 

在上面调试过程中,我们发现了一个现象,i 从 1 变成 12 的过程中,arr[12]的数值跟i一模一样!

因此,我猜想 i 和 arr[12] 处于同一块空间?例如下图这样?

为了验证这个猜想,在监视窗口中,添加 “&i” 和 “&arr[]12”,对比一下它们俩的地址。请看下图:

我们发现, i 和 arr[12] 确实处于同一块空间,因此当arr[12] 赋值为0的时候,i 也变成了 0.因此for循环从0又开始继续增长,最后陷入了死循环。

为什么会这样呢?为什么 i 和arr[12] 处于同一块空间? 

拓展: 

上述代码的运行结果其实是跟编译环境有关系的。 

这个例子呢并不是探究编译器的内存分配的,不过这个确实是一个利用调试技巧来探究代码运行结果的好例子。


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

6.1 优秀的代码: 

1. 代码运行正常

2. bug很少

3. 效率高

4. 可读性高

5. 可维护性高

6. 注释清晰

7. 文档齐全 

常见的coding技巧: 

1. 使用assert

2. 尽量使用const

3. 养成良好的编码风格

4. 添加必要的注释

5. 避免编码的陷阱。 

这里先简单介绍一个assert和const的用法:

6.1.1 assert

在C语言中,assert宏定义在头文件assert.h中。要使用它,需要在源文件顶部包含这个头文件: 

#include <assert.h>

assert宏的原型如下: 

void assert(int expression);

它接收一个表达式,并检查表达式的值是否为非零(true)。如果表达式的计算结果为0(即为false),assert将输出一条错误消息到标准错误输出,并终止程序执行。 

6.1.2 const的作用

结论:

const修饰指针变量的时候: 

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

2. const *的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。 

// const修饰指针变量的时候:

//代码1 测试无cosnt的
void test1()
{
    int n = 10;
    int m = 20;
    int* p = &n;
    *p = 20; // right
    p = &m; // right
}

//代码2 测试const放在*的左边
void test2()
{
    //代码2
    int n = 10;
    int m = 20;
    const int* p = &n;
    *p = 20; // error
    p = &m; // right
}

//代码3 测试const放在*的右边
void test3()
{
    int n = 10;
    int m = 20;
    int* const p = &n;
    *p = 20; // right
    p = &m;  // error
}

//代码4 测试const放在*的两边
void test4()
{
    int n = 10;
    int m = 20;
    const int* const p = &n;
    *p = 20; // error
    p = &m;  // error
}

6.2 示范: 

模拟实现库函数:strcpy 

#include<assert.h>

// 原版
char* my_strcpy(char* dest, const char* src) 
{
	assert(dest != NULL);
	assert(src != NULL);
    while (*src != '\0')
    {
        *dest = *src;
        dest++;
        src++;
    }
    *dest = *src; // 拷贝'\0'结束符
    return dest;
}

// 1.优化断言
char* my_strcpy(char* dest, const char* src)
{
    assert(dest && src);
    while (*src != '\0')
    {
        *dest = *src;
        dest++;
        src++;
    }
    *dest = *src; // 拷贝'\0'结束符
    return dest;
}

// 2.优化赋值
char* my_strcpy(char* dest, const char* src)
{
    assert(dest && src);
    while (*src != '\0')
    {
        *dest++ = *src++;
    }
    *dest = *src; // 拷贝'\0'结束符
    return dest;
}

// 3.优化循环和拷贝'\0'结束符
char* my_strcpy(char* dest, const char* src)
{
    assert(dest && src);
    while (*dest++ = *src++)
    {
        ;
    }
    return dest;
}        

// 4.优化返回值
char* my_strcpy(char* dest, const char* src)
{
    assert(dest && src);
    char* ret = dest;
    while (*dest++ = *src++)
    {
        ;
    }
    return ret;
}
int main()
{
    char str1[] = "hello world";
    char str2[100];
    my_strcpy(str2, str1);
    printf("%s\n", str2);
    return 0;
}

模拟实现一个strlen函数
参考代码: 

#include<assert.h>

size_t my_strlen(const char* str)
{
    assert(str != NULL);
    size_t len = 0;
    while (*str) // 判断字符串是否结束
    {
        len++;
        str++;
    }
    return len;
}

int main()
{
    char str[] = "hello world";
    size_t len = my_strlen(str);
    printf("len = %d\n", len);
    return 0;
}

7. 编程常见的错误

7.1 编译型错误

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

7.2 链接型错误

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

7.3 运行时错误

借助调试,逐步定位问题。最难搞。

温馨提示: 做一个有心人,积累排错经验。 

以上就是visual stdio 进行调试的教程了,后续会专门出一个visual 调试的专栏,以及Linux环境下GDB调试的专栏,会有更加高阶的调试技巧!敬请期待!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

创建电商产品说明书的这些雷,你踩了几条

现如今电商的流行&#xff0c;让电商产品说明书不仅是产品的“身份证”&#xff0c;更是商家与消费者沟通的桥梁。但是&#xff0c;在创建电商产品说明书时&#xff0c;稍不注意就可能踩到“雷区”&#xff0c;给消费者留下不好的印象&#xff0c;甚至影响销量。今天&#xff0…

【计算机2区】毕业快刊 —— 非黑!非预警!各指标优异!

No.1 工程综合类SCIE 【期刊简介】IF&#xff1a;6.0-7.0&#xff0c;JCR1区&#xff0c;中科院2区 【版面类型】纯正刊&#xff0c;仅10篇版面 【自引率】13.30%&#xff08;位于安全阈值内&#xff09; 【年发文量】400篇左右&#xff08;发文量稳定&#xff09; 【国人…

单机三pxc节点集群,+docker-haproxy2.0负载均衡实现

一.下载 https://www.haproxy.org/download/2.0/src/haproxy-2.0.5.tar.gz 或者在这里下载&#xff08;下面需要的各个配置文件都有&#xff09;&#xff1a; https://download.csdn.net/download/cyw8998/89170129 二.编写文件&#xff0c;制作docker镜像 1.Dockerfile&a…

信创产业发展迅速,信创测试需要伴随

信创产业的发展现状呈现出蓬勃的生机与活力。这一领域不仅构成了数据安全、网络安全的基石&#xff0c;更是新型基础设施建设的重要一环。信创产业涵盖了众多关键领域&#xff0c;如云计算、软件&#xff08;包括操作系统、中间件、数据库及应用软件&#xff09;、硬件&#xf…

Android studio配置Flutter(看这一篇就够了)

Flutter 是 Google 推出并开源的移动应用开发框架&#xff0c;主打跨平台、高保真、高性能。开发者可以通过 Dart 语言开发 App&#xff0c;一套代码同时运行在 iOS 和 Android平台。 Flutter 提供了丰富的组件、接口&#xff0c;开发者可以很快地为 Flutter 添加 Native&#…

#vscode | poetry | 虚拟环境 | Interpreter# 使用Poetry进行Python项目依赖管理和VSCode环境配置

系统安装poetry curl -sSL https://install.python-poetry.org | python3 - 安装 poetry --version 验证安装是否成功 项目安装poetry poetry install install 命令从当前项目中读取 pyproject.toml 文件&#xff0c;解析依赖项并安装它们。 Vscode配置 对应虚拟环境的in…

攻防打点|Shiro漏洞利用大全【附工具】

Shiro反序列化漏洞在目前攻防打点中仍然可以使用,如一些废弃的忘记关掉的旁站之类的。。。 「手工如何判断是否存在shiro」 特征码为响应包存在rememberMe=deleteMe 打开burp进行抓包,在请求包中添加Cookie: rememberMe=me,查看返回包中是否存在rememberMe=deleteMe。 「工…

可视化大屏在政务领域应用非常普遍,带你看看

可视化大屏在政务领域的应用非常普遍&#xff0c;政务领域需要处理大量的数据和信息&#xff0c;通过可视化大屏可以将这些数据以直观、易懂的方式展示出来&#xff0c;帮助政府决策者和工作人员更好地了解和分析数据&#xff0c;从而做出更准确、科学的决策。 在政务领域&…

API接口新探索:一键获取商品标题、分类与店铺名称

一、引言 在当今信息化社会&#xff0c;电子商务的蓬勃发展使得各类商品信息浩如烟海。为了高效地获取商品信息&#xff0c;许多开发者选择使用API接口。API&#xff08;Application Programming Interface&#xff0c;应用程序编程接口&#xff09;是一种定义明确的方法&…

玩转压力管理,轻松高效编程

程序员缓解工作压力的小窍门 在当今快速发展的科技时代&#xff0c;程序员作为数字世界的建筑师&#xff0c;面临着高强度、高压力的工作环境。为保持工作效率和创新能力&#xff0c;同时也确保身心健康和个人热情的持久续航&#xff0c;采取科学合理的减压策略至关重要。 方…

Django中的定时任务与后台任务队列的实践

&#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 在Web开发中&#xff0c;处理定时任务和后台任务队列是很常见的需求。Django作为一个功能强…

了解边缘计算,在制造行业使用边缘计算。

边缘计算是一种工业元宇宙技术&#xff0c;可以帮助组织实现其数据的全部潜力。 处理公司的所有数据可能具有挑战性&#xff0c;而边缘计算可以帮助公司更快地处理数据。在制造业中&#xff0c;边缘计算可以帮助进行预测性维护和自动驾驶汽车操作等工作。 什么是边缘计算? …

Spring Boot 自动装配执行流程

Spring Boot 自动装配执行流程 Spring Boot 自动装配执行流程如下&#xff1a; Spring Boot 启动时会创建一个 SpringApplication实例&#xff0c;该实例存储了应用相关信息&#xff0c;它负责启动并运行应用。实例化 SpringApplication 时&#xff0c;会自动装载META-INF/spr…

go语言通过TCP协议实现聊天室样例

1、服务端&#xff1a; package mainimport ("fmt""net""sync" )type ChatServer struct {clients map[string]net.ConnclientsMux sync.Mutex }func NewChatServer() *ChatServer {return &ChatServer{clients: make(map[string]net.Co…

【NoC片上网络 On-Chip Network】应用程序的网络流量 合成网络流量

应用程序的网络流量 and 合成网络流量 1. 应用程序的网络流量 APPLICATION TRAFFIC2. 合成网络流量 SYNTHETIC TRAFFIC3. 合成网络流量的具体介绍 应用程序的网络流量 and 合成网络流量 1. 应用程序的网络流量 APPLICATION TRAFFIC 在 MPSoC(多处理器片上系统) 中&#xff…

书生·浦语大模型第二期实战营(6)作业

1。完成 Lagent Web Demo 使用&#xff0c;并在作业中上传截图。 文档可见 Lagent Web Demo 2、完成 AgentLego 直接使用部分&#xff0c;并在作业中上传截图。 文档可见 直接使用 AgentLego

前端crypto-js, 文件加密,判断相同文件、图片(MD5,SHA256)

文章目录 前情提要应用场景实战解析最后前情提要 大家好,今天我们来接触一个库crypto-js 没错,上面是有道翻译的截图,为了我们得到的信息更权威,这个库是用来加密的,但介绍是说,已经停止维护,但并不影响我们在前端项目中的使用,所以学学也没有坏处 应用场景 判断图片…

成电少年学fpga培训就业班怎么样

成电少年学是专注做FPGA培训的&#xff0c;以就业为导向&#xff0c;学习FPGA还是很有前途的&#xff0c;如果你是像电气、通信、自动化、物联网、集成电路这类专业&#xff0c;又不是名校高学历的&#xff0c;确实有必要可以考虑下校外培训机构。找工作多少会遇到一些问题&…

Linux下SPI设备驱动实验:使用内核提供的读写SPI设备中的数据的函数

一. 简介 前面文章的学习&#xff0c;已经实现了 读写SPI设备中数据的功能。文章如下&#xff1a; Linux下SPI设备驱动实验&#xff1a;验证读写SPI设备中数据的函数功能-CSDN博客 本文来使用内核提供的读写SPI设备中的数据的API函数&#xff0c;来实现读写SPI设备中数据。 …

【机器学习】各大模型原理简介

目录 ⛳️推荐 前言 一、神经网络&#xff08;联结主义&#xff09;类的模型 二、符号主义类的模型 三、决策树类的模型 四、概率类的模型 五、近邻类的模型 六、集成学习类的模型 ⛳️推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风…