当前位置: 首页 > news >正文

C语言VS实用调试技巧

文章目录

  • 一、什么是bug?
  • 二、什么是调试?
  • 三、Debug和Release
  • 四、VS调试快捷键
    • 4.1环境准备
    • 4.2调试快捷键
  • 五、监视和内存观察
    • 5.1监视
    • 5.2内存
  • 六、调试举例
  • 七、编程常见错误归类
    • 7.1编译型错误
    • 7.2链接型错误
    • 7.3运行时错误

一、什么是bug?

🍎bug本意是 “昆虫” 或 “虫子”,现在一般是指在电脑系统或程序中,隐藏着的一些未被发现的缺陷或问题,简称程序漏洞

“Bug”的创始人格蕾丝·赫柏(Grace Murray Hopper),她是一位从事美国海军工作的电脑专家,1947年9月9日,格蕾丝·赫柏对Harvard Markll设置好17000个继电器进行编程后,技术人员正在进行整机运行时,它突然停止了工作。于是他们爬上去找原因,发现这台巨大的计算机内部一组继电器的触点之间有一只飞蛾,这显然是由于飞蛾受光和热的吸引,飞到了触点上,然后被高电压击死。所以在报告中,赫柏用胶条贴上飞蛾,并用“bug”来表示“一个在电脑程序里的错误”,“Bug”这个说法一直沿用至今。
在这里插入图片描述

二、什么是调试?

🍋当我们发现程序中存在问题的时候,那下一步就是找到问题,并修复问题。
🍋这个找问题的过程被称为调试,英文叫debug(消灭bug的意思)。
🍋调试一个程序,首先得承认出现了问题,然后通过各种手段去定位问题的位置,可能是逐过程的调试,也可能是隔离和屏蔽代码的方式,找到问题所在的位置,然后确定错误产生的原因,再修复代码重新测试。

三、Debug和Release

在这里插入图片描述在VS上编写代码的时候,就能看到有 debug 和 release 两个选项,分别是什么意思呢?

Debug通常称为调试版本式,它包含调试信息,并且不作任何优化,便于程序员调试程序;程序员在写代码的时候,需要经常性的调试代码,就将这里设置为 debug,这样编译产生的是debug版本的可执行程序,其中包含调试信息,是可以直接调试的。
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。当程序员写完代码之后,再对程序进行测试,直到程序的质量符合交付给用户使用的标准,这个时候就会设置为 release ,编译产生的就是release版本的可执行程序,这个版本是用户使用的,无需包含调试信息。

如果在两个版本下分别调试上面图片中的代码:就会得到Debug和Release两个版本下的可执行程序:(运行程序后,在下面的输出窗口会告诉我们生成的可执行程序(.exe)的文件位置)
在这里插入图片描述在这里插入图片描述在这里插入图片描述可以看到同样的程序在两个版本下生成的可执行程序的大小是有差距的,相比较于Debug版本的可执行程序,Release版本的更小。

四、VS调试快捷键

那程序员怎么调试代码呢?

4.1环境准备

首先是环境的准备,需要一个支持调试的开发环境,我使用的编译器是VS2022,应该把VS上的环境设置为Debug,如图:
在这里插入图片描述

4.2调试快捷键

调试最常使用的几个快捷键
🍅F9创建断点和取消断点
断点的作用是可以在程序的任意位置设置断点,打上断点就可以使得程序执行到想要的位置暂定执行,接下来我们就可以使用F10F11这些快捷键,观察代码的执行细节。
条件断点满足这个条件,才触发断点
🍅F5启动调试,经常用来直接跳到下一个断点处,一般是和F9配合使用。
🍅F10逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
🍅F11逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数的内部。在函数调用的地方,想进入函数观察细节,必须使用F11,如果使用F10,会直接完成函数调用。
🍅Ctrl+F5开始执行不调试,如果你想让程序直接运行起来而不是调试,就可以直接使用Ctrl+F5
VS更多快捷键了解:http://blog.csdn.net/mrlisky/article/details/72622009

(1) F9设置的断点如下图,光标在第13行的语句处按一下F9就会出现一个红色的点,就是断点:(再按一下F9取消断点)
在这里插入图片描述这个时候,再按F5调试起来后,程序就会直接来到断点处,在监视窗口里可以看到,在断点前面的程序其实已经执行了,然后程序就停在断点处。
在这里插入图片描述在这里插入图片描述

如果一个程序中设置有多个断点呢?那按F5会执行到哪呢?
在这里插入图片描述在这里插入图片描述可以看到如果程序中有多个断点时,那第一次按F5会先执行到最前面的断点处,此时在监视窗口可以看到变量i的值是0,如果在第一次按F5的基础上,再按一次F5,变量i的值变为:
在这里插入图片描述在第二次按F5的基础上,再按第三次F5,i的值变为:
在这里插入图片描述再在监视窗口查看arr数组:
在这里插入图片描述通过上面的测试我们可以看出,在设置断点后,再配合使用F5,那程序停顿的断点处其实是一个逻辑断点,意思是第一次执行到断点处,比如上面第9行的断点。第一次按F5调试起来,程序会停在这一行,断点前面的程序其实已经运行了,然后停在断点所在的语句行,当第二次按F5调试后,因为是一个for循环,所以在执行了arr[i]=i这条语句后,会返回上到上面的循环调整部分,也就是i++部分,然后来到循环条件的判断部分,再然后就第二次遇到断点,又停在断点处等待下一次的F5调试。所以为什么称之为逻辑断点就是这个意思。(注意:当F5执行到断点所在的行时只是停在这一行,但这一行的语句是不运行的)

(2) 条件断点:就拿上面的程序来说明什么是条件断点:在第9行设置条件断点,如下图鼠标放在断点上,单击鼠标右键:
在这里插入图片描述在这里插入图片描述条件断点就是当满足某种条件时(才会触发这个断点),程序才会停在断点处。上面设置了当i==5时,程序才会停顿在第9行:
在这里插入图片描述满足条件的断点,不是说程序只能执行到断点处而不能再调试啦。就像上面,如果第二次按F5调试,由于i在等于5后,for循环会使i往上递增,i==5的条件就失效了,所以第二次按F5程序就一次性运行完了。

(3) F10是逐过程的执行语句(也就是一条语句一条语句的执行),但是当遇到调用函数时,不会进入到函数的内部,而是直接调用完函数。F10也可以配合F9、F5一起使用,如果在程序中发现有一段代码出现了bug,那可以在该处设置断点,然后再按F10逐语句的进行调试找问题。
在这里插入图片描述由上图,如果确定是第二个for循环部分出现了bug,那在13行设置断点,之后先按F5调试停在断点处,接下来就按F10一步一步的执行来找bug。这样做的好处是,如果一开始就按F10,一个语句一个语句的执行,由于第一个for循环的循环条件部分太长了,那只按F10来执行程序就太费时了。而设置了断点后,F5调试起来就可以直接来到第二个for循环处,并且第二个for循环的循环条件部分很短。所以这就是F9、F5、F10配合使用的好处,当程序的语句(逻辑上)很短时,用F10执行就很好。

(4)F11与F10的区别就是:在遇到函数调用的时候,按F11能够进到函数内部,执行函数内部的每条语句,这样就能更细致地观察函数内部的代码是怎么运行的。所以比起F10,F11其实能更细致入微地执行程序中的语句。

比如我们定义了一个test函数,在main函数中调用test函数,我们先在调用test函数的地方设置一个断点,然后按F5调试起来后,来到test函数处,我们再按F11进入函数内部,进入函数内部后就可以按F10或者F11来执行语句都可以:
在这里插入图片描述当然还有一个ctrl+F5快捷键,意思是开始执行但不调试。ctrl+F5和F5是有区别的:如果按ctrl+F5执行程序,那不管程序中有没有设置断点程序都会一次性运行完。所以小伙伴们要熟练掌握这些调试快捷键,学会了这些调试快捷键就可以更好的调试程序,找程序中的bug。(注意:在笔记本上电脑上,如果单独按这些快捷键不起作用,那就按Fn+这些快捷键)

五、监视和内存观察

在调试的过程中,如果想观察代码执行过程中,上下文环境中的变量的值,有哪些方法呢? 这些观察的前提条件一定是开始调试后才能观察,比如按F10调试下面的代码:

#include<stdio.h>
int main()
{int arr[10] = { 0 };int num = 100;char c = 'w';int i = 0;for (i = 0; i < 10; i++){arr[i] = i;}return 0;
}

5.1监视

上面的代码按F10调试起来,就可以在菜单栏中的【调试】->【窗口】->【监视】,打开任意一个监视窗口,输入想要观察的对象即可:
在这里插入图片描述在这里插入图片描述监视窗口如下:(按F10调试完程序后的变量i和数组arr的值)
在这里插入图片描述

5.2内存

如果在监视窗口看上面代码中的变量的值看得不够仔细,也可以在内存中观察变量的存储情况。在【调试】->【窗口】->【内存】中,任意选择一个内存窗口:
在这里插入图片描述在这里插入图片描述在内存窗口观察数据:(边按F10边调试看内存中变量的值的存储情况)
在这里插入图片描述在这里插入图片描述在打开内存窗口后,要在地址栏输入:arr(数组名),&num,&c,这类地址,就能观察到该地址处的数据。如下图,当F10调试到i==9的时候的内存和监视:
在这里插入图片描述在这里插入图片描述除此之外,在调试的窗口中还有:自动窗口,局部变量,反汇编、寄存器等窗口,请自行验证使用。

六、调试举例

在VS2022、X86、Debug的环境下,编译器不做任何优化的话,下面代码执行的结果是啥?

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

运行的结果是死循环的打印"hehe",原因是什么呢?
可以在调试中,从内存里面查看数组arr和变量i的存储情况:

在这里插入图片描述
在这里插入图片描述当F10调试到for循环的部分,进入循环就会把数组arr中的元素全部改成0。但是你注意到数组的元素个数只有10个,而for循环已经越界使用内存中的空间啦,当for循环的循环变量i超过9增加到12时,从上面的内存中可以看到其实已经访问到变量i的空间了,当arr[12]=0执行完毕,其实就是将变量i的值改为了0。这样就让变量i就又回到了0,又从0开始往上遍历。所以造成了死循环。

那为什么会出现这种情况呢?为什么变量i在内存中的地址就刚好在数组arr的后面(高地址处)?解释一下:
🥥1.栈区内存的使用习惯是从高地址向低地址使用的,所以变量i的地址是较大的。arr数组的地址整体是小于i的地址的。
🥥2.数组在内存中的存放是:随着下标的增长,地址是由低到高变化的。
因为是先创建的变量i,然后才创建的arr数组,所以在栈区中变量i的地址相较于数组arr是高地址。所以是这种内存布局,那随着数组下标的增长,往后越界就有可能覆盖到i,这样就可能造成死循环。

在这里插入图片描述至于为什么变量i和arr数组之间恰好会空出来2个整型的空间? 这里确实是巧合,在不同的编译器下可能中间空出的空间大小是不一样的,代码中这些变量的内存分配和地址分配是编译器指定的,所以不同的编译器之间就有差异了。这个题目是和环境相关的。

从这个例子中我们能够体会到调试的重要性,只有调试才能观察到程序内部执行的细节,就像医生给病人做B超、CT一样。
🥑注意:栈区的默认使用习惯是先使用高地址,再使用低地址的空间,但是这个具体还是要看编译器的实现。比如:在VS上切换到X64,内存空间的使用顺序就是相反的,在Release版本的程序中,这个使用的顺序也是相反的。

七、编程常见错误归类

7.1编译型错误

编译型错误一般都是语法错误,这类错误一般看错误信息就能找到一些蛛丝马迹,双击错误信息也能初步的跳转到代码错误的地方或者附近。编译型错误,随着语言的熟练掌握,会越来越少,也容易解决。
在这里插入图片描述

7.2链接型错误

看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是因为:
🍓标识符名不存在
🍓拼写错误
🍓头文件没包含
🍓引用的库不存在

在这里插入图片描述

7.3运行时错误

运行时错误,是千变万化的,需要借助调试,逐步定位问题,调试解决的是运行时问题。

在这里插入图片描述

相关文章:

  • 隧道灯光远程控制系统的设计与实现(论文+源码)_kaic
  • Spring Boot 进阶-Spring Boot 开发第一个Web接口
  • Java集合框架与Lambda表达式实践指南(小白速看)
  • 大模型Agent开发框架对比:LangGraph/LlamaIndex/DIY
  • 7-1.Android SQLite 之 SQLiteDatabase 简单编码模板(SQLiteDatabase 使用、SQL 语句编写)
  • 工业缺陷检测——Windows 10本地部署AnomalyGPT工业缺陷检测大模型
  • 综合题第二题(路由器的配置)
  • 高考技术——pandas使用
  • FastGPT大模型介绍
  • Python面向对象基础:类属性
  • Vscode把全部‘def‘都收起来的快捷键
  • Mybatis中 #{} 和 ${} 的区别是什么?
  • 【RabbitMQ】RabbitMq消息丢失、重复消费以及消费顺序性的解决方案
  • 媒界:助力民生保障 长城“消防炮”即将批量交付硬核守护万家灯火
  • 【hot100-java】【最长公共子序列】
  • iOS 系统授权开发
  • JavaScript的使用你知道几种?(上)
  • magento 货币换算
  • Mysql优化
  • ⭐ Unity + OpenCV 实现实时图像识别与叠加效果
  • 记录:CentOS7.2配置LNMP环境记录
  • 漂亮刷新控件-iOS
  • 前端路由实现-history
  • 如何编写一个可升级的智能合约
  • 使用 Node.js 的 nodemailer 模块发送邮件(支持 QQ、163 等、支持附件)
  • 通过来模仿稀土掘金个人页面的布局来学习使用CoordinatorLayout
  • 微服务框架lagom
  • 积累各种好的链接
  • ​Base64转换成图片,android studio build乱码,找不到okio.ByteString接腾讯人脸识别
  • ​linux启动进程的方式
  • ​经​纬​恒​润​二​面​​三​七​互​娱​一​面​​元​象​二​面​
  • #define、const、typedef的差别
  • #微信小程序:微信小程序常见的配置传值
  • (2015)JS ES6 必知的十个 特性
  • (6)【Python/机器学习/深度学习】Machine-Learning模型与算法应用—使用Adaboost建模及工作环境下的数据分析整理
  • (Arcgis)Python编程批量将HDF5文件转换为TIFF格式并应用地理转换和投影信息
  • (C++17) std算法之执行策略 execution
  • (附源码)SSM环卫人员管理平台 计算机毕设36412
  • (机器学习-深度学习快速入门)第三章机器学习-第二节:机器学习模型之线性回归
  • (理论篇)httpmoudle和httphandler一览
  • (每日一问)操作系统:常见的 Linux 指令详解
  • (每日一问)基础知识:堆与栈的区别
  • (亲测成功)在centos7.5上安装kvm,通过VNC远程连接并创建多台ubuntu虚拟机(ubuntu server版本)...
  • (四) 虚拟摄像头vivi体验
  • (转)关于如何学好游戏3D引擎编程的一些经验
  • (自用)learnOpenGL学习总结-高级OpenGL-抗锯齿
  • .mkp勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复
  • .NET 4.0中使用内存映射文件实现进程通讯
  • .Net 转战 Android 4.4 日常笔记(4)--按钮事件和国际化
  • .NET/C# 利用 Walterlv.WeakEvents 高性能地定义和使用弱事件
  • .NetCore Flurl.Http 升级到4.0后 https 无法建立SSL连接
  • .NET开发不可不知、不可不用的辅助类(三)(报表导出---终结版)
  • .NET框架类在ASP.NET中的使用(2) ——QA
  • /usr/lib/mysql/plugin权限_给数据库增加密码策略遇到的权限问题
  • @selector(..)警告提示