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

本文主要是介绍《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; // rightp = &m; // right
}//代码2 测试const放在*的左边
void test2()
{//代码2int n = 10;int m = 20;const int* p = &n;*p = 20; // errorp = &m; // right
}//代码3 测试const放在*的右边
void test3()
{int n = 10;int m = 20;int* const p = &n;*p = 20; // rightp = &m;  // error
}//代码4 测试const放在*的两边
void test4()
{int n = 10;int m = 20;const int* const p = &n;*p = 20; // errorp = &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调试的专栏,会有更加高阶的调试技巧!敬请期待!

这篇关于《C语言深度解剖》(8):一篇文章彻底学会Visual Studio 调试技巧,新手必看!的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/927359

相关文章

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

购买磨轮平衡机时应该注意什么问题和技巧

在购买磨轮平衡机时,您应该注意以下几个关键点: 平衡精度 平衡精度是衡量平衡机性能的核心指标,直接影响到不平衡量的检测与校准的准确性,从而决定磨轮的振动和噪声水平。高精度的平衡机能显著减少振动和噪声,提高磨削加工的精度。 转速范围 宽广的转速范围意味着平衡机能够处理更多种类的磨轮,适应不同的工作条件和规格要求。 振动监测能力 振动监测能力是评估平衡机性能的重要因素。通过传感器实时监

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

ASIO网络调试助手之一:简介

多年前,写过几篇《Boost.Asio C++网络编程》的学习文章,一直没机会实践。最近项目中用到了Asio,于是抽空写了个网络调试助手。 开发环境: Win10 Qt5.12.6 + Asio(standalone) + spdlog 支持协议: UDP + TCP Client + TCP Server 独立的Asio(http://www.think-async.com)只包含了头文件,不依

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

滚雪球学Java(87):Java事务处理:JDBC的ACID属性与实战技巧!真有两下子!

咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE啦,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~ 🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,助你一臂之力,带你早日登顶🚀,欢迎大家关注&&收藏!持续更新中,up!up!up!! 环境说明:Windows 10

计算机毕业设计 大学志愿填报系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

🍊作者:计算机编程-吉哥 🍊简介:专业从事JavaWeb程序开发,微信小程序开发,定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事,生活就是快乐的。 🍊心愿:点赞 👍 收藏 ⭐评论 📝 🍅 文末获取源码联系 👇🏻 精彩专栏推荐订阅 👇🏻 不然下次找不到哟~Java毕业设计项目~热门选题推荐《1000套》 目录 1.技术选型 2.开发工具 3.功能

基于UE5和ROS2的激光雷达+深度RGBD相机小车的仿真指南(五):Blender锥桶建模

前言 本系列教程旨在使用UE5配置一个具备激光雷达+深度摄像机的仿真小车,并使用通过跨平台的方式进行ROS2和UE5仿真的通讯,达到小车自主导航的目的。本教程默认有ROS2导航及其gazebo仿真相关方面基础,Nav2相关的学习教程可以参考本人的其他博客Nav2代价地图实现和原理–Nav2源码解读之CostMap2D(上)-CSDN博客往期教程: 第一期:基于UE5和ROS2的激光雷达+深度RG