C语言初阶——9实用调试技巧


前言

作者写本篇文章旨在复习自己所学知识,并把我在这个过程中遇到的困难的将解决方法和心得分享给大家。由于作者本人还是一个刚入门的菜鸟,不可避免的会出现一些错误和观点片面的地方,非常感谢读者指正!希望大家能一同进步,成为大牛,拿到好offer。
本系列(初识C语言初阶)只是对C语言的基础知识点过一遍,是为了能让读者和自己对C有一个初步了解。


日志

  1. 2024.5.19号首发

1.什么是bug?

计算机史上第一个bug:上世纪的计算机科学家,在写程序的时候,发现计算机发生了故障。那时候的计算机还很大,跟个房间似的,可以进入到里面。程序员就去里面找故障的原因,发现在一个晶体二极管上发现一个飞蛾死在了上面。然后把它清理,并替换了零件,计算机就正常工作了。
在这里插入图片描述
后来科学家把这个事情记录下来,之后就把计算机发生的错误称为bug(臭虫、飞蛾)。bug就是计算机程序或硬件可能存在的缺陷。

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

简单来说就是通过已经发现的bug,逆推回去找出发生bug的原因,解决发生的bug。这个过程就成为调试。

2.1调试是什么?

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

2.2调试的基本步骤

  1. 发现程序错误的的存在
    假设有一天写完代码发给公司的测试人员。测试人员跟你说程序有问题,重新写。而你却觉得想着:我写的程序怎么可能出现问题,是你自己安装的环境错误了、安装包有问题、操作方法不当。这个时候明明程序出问题了,还不承认自己写错了,这就非常尴尬了。要接受自己的错误,才能不断成长。
  2. 以隔离、和消除等方式对错误进行定位
    假设有一天你不是个新手了,写了很多代码,比如一万行。我们就可以先屏蔽一部分你可能觉得会出错的代码,再运行。发现没有问题了,我再把这部分代码放出来。继续找另外可能出现问题的代码,屏蔽掉再尝试运行。
    就这样就能很快找到错误的位置,可不要傻乎乎的从第一行f10,一直调试。这是非常浪费时间的。
  3. 确定错误产生的原因
  4. 提出纠正错误的解决办法
  5. 对程序错误予以改成,重新测试
    可不要想着改完之后,我觉得一定是对的,就压根不看了。改完之后也存在错误的可能,我们需要保持谦虚的心态。

2.3Debug和Release的介绍

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

  1. 在VS的编译器上面也有显示
    在这里插入图片描述

  2. 可以随便写一个代码,用来区别Debug和Release
    在这里插入图片描述

  3. 先来看Debug版本,使用f10调式,直接进入main函数,箭头来到第4行
    在这里插入图片描述
    再按一下f10,走到了第5行
    在这里插入图片描述
    再f10,创建arr,可以从监视窗口看到,arr里面的值全部初始化为0了。并且来到第6行
    在这里插入图片描述
    再f10,创建了i,可以看到i的初始值为0。程序来到第7行
    在这里插入图片描述
    这个时候,i为0,小于10成立。按f10就会进入循环,来到第9行
    在这里插入图片描述
    这时i为0,arr[i] = 10 - i;将10赋值给arr[0],所以这个时候按一下f10,就能发现arr[0]的值变成了10,箭头来到第10行
    在这里插入图片描述
    这次循环就结束了。再按f10就会来到第7行,循环的调整和判断部分
    在这里插入图片描述
    i++,i变成1,1还是小于10。还要进入循环,来到第9行在这里插入图片描述
    之后一直第7行第9行一直循环到第9次循环结束时候,i为9,arr[8]为0的时候
    在这里插入图片描述
    将1赋值给arr[9],之后再来到循环的调整和判断部分
    在这里插入图片描述
    i++,i变成10。10不小于10,判断为假,不再进入循环。再f10,就会来到11行
    在这里插入图片描述
    从这里就可以发现,在Debug版本下,f10能调试每一步。

  4. 来调试一下Release版本,看看是什么现象
    在这里插入图片描述
    此时此刻箭头还在第4行。按一下f10看看
    在这里插入图片描述
    直接来到了第9行,而不是一步步来。再f10
    在这里插入图片描述
    来到11行,第一个for循环都结束了。但是ar都没有完全改完。再f10
    在这里插入图片描述
    又咔咔一下子改完了arr里的值。就这样你会发现,在Release版本下压根就没办法一步一步调试
    所以说Release里面没有包括调试信息,它是不方便调试的。这是因为站在用户的角度看,用户不需要调试,所以Release就没有必要包括调试信息,使得内存更小

  5. 我们可以在这两个版本下,Ctrl+f5分别生成可执行的exe文件。并且来到这个项目底下就能看到它们各自的文件
    在这里插入图片描述

所以我们写代码的时候,担心这个程序有问题,就设置成Debug版本。而当你测试过,觉得没有任何问题的时候,就可以设置成Release版本给用户使用。
至于Release版本到底进行了什么优化,后面写的博客介绍

3.Windows环境调试介绍

我们介绍的是Windows环境下的VS
注:linux开发环境调试工具后期是gdb,后期介绍

3.1调试环境的准备

在这里插入图片描述
在环境中选择Debug选项,才能进行正常的调试。因为Release已经进行了各种优化,不包含调试信息。

3.2学会快捷键

在Debug环境下,按下f10,进入调试之后可以看到调试里有很多的快捷键
在这里插入图片描述

f10和f10

f10:逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
f11:逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最长用的)。
在这里插入图片描述
在下面这些普通的雨具f10和f11是没有区别的。都是一条一条代码执行
在这里插入图片描述
我们加上一个test函数来区别f10和f11
我们直接来到代码的第13行,可以看到控制台什么都没有
在这里插入图片描述
这个时候按一下f10,直接来到14行了。没有进入函数,而且屏幕上输出了haha和hello。
在这里插入图片描述
这说明按f10是进入不了函数,观察里面的细节的。整个函数被一下f10就走完了。
重新开启一次调试模式,来到第13行的时候按一下f11
在这里插入图片描述
可以看到箭头来到了第5行,进入了test函数。说明f11能够进入函数,观察函数内部的细节。
再按一下f11,什么都没有发生,这是因为还没有执行第6行的打印。
在这里插入图片描述
再按一下f11,就可以看到屏幕上打印了haha
在这里插入图片描述
再按下f11,打印hello
在这里插入图片描述
再按下f11,就会跳出函数,回到main里。箭头来到14行
在这里插入图片描述
这里就可以看到f10和f11的区别了
在这里插入图片描述
所以未来在调试的时候遇到函数,按f10还是f11就看你的需求。不能觉得f11更细致,就一直按f11。比如有一天你写了一万行的代码,要调试一下。但是你大概知道第5000行,出现了错误。然后5000之前的代码里有各种各样的函数,函数里面又有非常多的嵌套循环,执行完成这一个循环要100,1000次乃至更多。这个时候你想要来到5000行,f11都会按冒烟的啊。
f11可以进入函数观察细节,但是如果我觉得这个函数没有问题,那么就按f10不进入这个函数。所以说按哪个要根据需求来定

f5和f9

f5:启动调试,经常用来直接跳到下一个断点处
f9:创建断点和取消断点。断点的重要作用,可以在程序的任意位置设置断点。这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。
在这里插入图片描述

  1. f5和f9是配合使用的
    有可能你是直接按f5,结果直接出来了
    在这里插入图片描述
    结果直接出来了,没有调试过程

  2. f5叫做开始调试,如果中间没有断点拦截它,就会直接执行完代码。f9是新建/切换断点。
    比如说现在,你认为20行前面的代码都没有问题,而问题出现在打印上。鼠标点到20行,按一下f9
    在这里插入图片描述

  3. 这个时候和f5配合使用。代码会快速执行,直到来到20行,突然停下来。并且进入调试模式
    在这里插入图片描述
    走到这里因为前面20行的代码已经执行完了,右边监视窗口也可以看到数组arr已经赋值好了

  4. 此时此刻第一次进入循环,i为0。还没有开始打印。
    在这里插入图片描述

  5. 现在可以按f10观察里面的细节。f10走一下,来到21行,打印arr[0],也就是10
    在这里插入图片描述

  6. f10来到18行,循环的调整和判断部分
    在这里插入图片描述

  7. i++,i变成1。i<10成立,再次进入循环来到20行
    在这里插入图片描述

  8. f10再走一下,打印arr[1]
    在这里插入图片描述
    之后一直走完。从上面就可以看出f5和f9配合使用起来了。f9设置一个断点,然后f5快速走完断点前的代码,来到断点处。来到断点处再f10,f10往下走。所以说你需要快速来到你认为出现问题的地方,就可以f9设置一个断点,再f5开始调试。断点前的代码就会快速执行,来到断点处停下来。然后就可以f10观察问题的细节。

  9. 断点是可以设置多个的。比如说你觉得为arr初始化的for循环可能有问题,也可以在16行按f9。再按下第一次f5的时候,就会来到16行,去观察里面的细节。
    在这里插入图片描述

  10. 当你觉得这个for循环观察完之后,想去到下一个断点。直接按f5是不行的,代码还会来到16行。而且i的值变成了1,,arr[0]变成了10。说明执行了一次循环
    在这里插入图片描述
    再f5还是来到16行,循环又执行了一次
    在这里插入图片描述
    之后f5,会一直来到这个断点处。这是因为循环还没有走完,循环再次执行的时候,遇到这个断点,又停下来了。
    在这里插入图片描述
    其实f5已经在执行代码,往下走了,但是这里是循环。代码从16行按f5后,默默的往17行走,17行走了之后,发现循环还没有结束。又来到14行,发现还要进入循环,默默的来到16行。16行发现有断点啊,所以停下来了。
    因此给我们看到的现象是f5一直在16行。所以f5是让我们的代码执行到下一个执行逻辑(实际上要执行的一行的代码的每一次断点处)上的断点上停下来,而不是物理断点(表面看到的断点处)上停下来

  11. 如果说我们觉得第一个断点处,也就是16行,问题已经排查好了。想来到下一个断点处,就可以在16行按一下f9。16行的断点就会取消掉
    在这里插入图片描述

  12. 这个时候如果再按一下f5,又会来到20行的断点处
    在这里插入图片描述
    这样使用f5和f9配合调试的速度就会快很多,否则从main函数开始f11来调试,键盘都会按冒烟啊

  13. f9还有一个技巧。比如说我们想来到16行里的for循环,但是我不想i从0开始,我想让i是5的时候停下来。就可以鼠标右击设置好的断点上,点条件。条件里面就可以加条件
    在这里插入图片描述
    这里的意思是i等于5的时候,才触发这个断点。现在按下f5
    在这里插入图片描述
    当然这里也可以加其他条件,看你自己的需求

Ctrl+f5

开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用
如果现在你设置了一堆断点的话,按Ctrl+f5也不会在断点处停止。因为断点处要调试才停下来,而Ctrl+f5是开始执行不调试,所以不关心断点
在这里插入图片描述

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

3.3.1查看临时变量的值

在调试开始之后,用于观察变量的值
在这里插入图片描述
这三个窗口都能观察变量的信息

  1. 点开自动窗口之后,刚开始什么都没有
    在这里插入图片描述
    按一下f10来到11行,arr自动出现了。它里面还是随机值,因为在为main函数开辟栈帧的时候,被设置成了CCCCCCCC的随机值。执行完创建代码的时候才会变成0
    在这里插入图片描述
    arr里面的内容变成了0,再f10来到12行,i也自动出现了。i同样是随机值。
    在这里插入图片描述
    再f10来到13行,i的值初始化为了0。同时屏幕上没有打印
    在这里插入图片描述
    再f10直接来到15行,,跳过了14行,而且arr消失了。
    在这里插入图片描述
    再按f10,arr又出现了。而且多了一个arr[i]
    在这里插入图片描述
    这就是自动窗口,它会根据程序上下文的环境中的信息,自动、默认的加上一些编译器认为重要的变量,自动的去掉你可能不需要的变量。
    这个自动窗口不太好,因为如果正在看一个变量,又突然没了,就不方便

  2. 局部变量跟自动窗口很相似,会监视程序执行过程中上下文环境中的局部变量
    在这里插入图片描述
    不需要你输入就自动监视了arr和i。但是有一个不好的点,就是不能自己加上想要观察的变量

  3. 监视窗口,才是我们真正最常用的监视变量的功能。
    在这里插入图片描述
    可以看到有很多的监视窗口,这些监视窗口之间相互独立。当你有很大的屏幕的时候,可以都开启,就可以观察到非常多的数据。
    最开始点进去之后什么都没有,一片空白。
    在这里插入图片描述
    你想监视什么数据,就输入什么数据。而且这些变量并不会随着程序的执行而消失。
    在这里插入图片描述
    但是监视的最好的是合法的,程序存在的。如果随便输入一个程序没有的bbq,监视也不知道这是什么
    在这里插入图片描述
    监视窗口想监视什么就监视什么,不想监视就直接删掉。因此我们用的都应该是监视窗口

3.3.2查看内存信息

在调试开始之后,用于查看内存信息
在这里插入图片描述

  1. 比如现在在监视窗口可以观察到arr,也可以打开内存观察arr
    在这里插入图片描述
    打开之后,会显示很多列,一行里的一列代表一个字节,但是这样不便于观察。可以在内存窗口右上角把它调成4列,也就是一行4个字节,这样就方便观察了
    在这里插入图片描述

  2. 想要观察arr就输入,&arr。这样就能查看arr所在空间的内容了
    在这里插入图片描述
    一行4列,4字节,一个int类型。从arr首元素地址0x0135f75c开始,一共10行0,也就是40字节,即10个整型。说明arr的内容确实被初始化为0了

  3. 按f10,内存窗口的内容也会跟着程序的执行改变的。比如现在第一次进入for循环,来到17行。
    在这里插入图片描述
    只要走完这一行,arr首元素也就是arr[0],它的地址就是0x0135F75C,会被改成10,在内存中数值是倒着放的,也就会被改成0a 00 00 00
    在这里插入图片描述
    至于为什么倒着放,以后介绍

3.3.3查看调用堆栈

  1. 现在重新写过一份代码,来解释什么是调用堆栈
    在这里插入图片描述
    这个时候,我们想去看他们的调用关系的时候,可以打开调试窗口的调用堆栈
    在这里插入图片描述
    点开之后,默认是这样子的界面
    在这里插入图片描述
    我们按f10来到42行之后,再按下f11进入test函数。调用堆栈窗口又多出来一行
    在这里插入图片描述
    之后一直f11走到test2里,调用堆栈窗口这里就在反馈一个函数调用关系
    在这里插入图片描述
    当代码来到31行的时候,马上就要结束test2
    在这里插入图片描述
    此时按一下f11,test2函数调用结束。调用堆栈的test2就消失了
    在这里插入图片描述
    之后test1函数结束,test1的调用堆栈消失。test结束,test的调用堆栈消失。回到main函数
    在这里插入图片描述
    当main函数结束后,main函数的调用堆栈也会消失
    这个调用堆栈窗口,就反映了程序在执行过程中函数调用的逻辑

  2. 什么是调用堆栈
    先来看数据结构,数据结构就是数据在内存存储的结构,有栈、队列列表和顺序表等。主要讲的是栈和队列,只讲概念,不讲细节
    队列就好比在食堂买饭吃,要排队
    在这里插入图片描述
    只能从队尾进,队头出。这种维护数据的方式就叫做队列
    数据结构还有一种叫栈
    在这里插入图片描述
    比如你要存个1,再存个2,再存3,再存4。就先从栈顶放进去1,再放2,再放3,再放4。就像是往空弹夹里填充子弹
    在这里插入图片描述
    删的时候,也不能先把1删了,只能先删4,再删3,2,1。就像满弹夹,总是打最上面的子弹。这种数据结果叫做栈

  3. 函数的调用逻辑也是采用了栈的结构,函数从栈顶放进去,栈顶出。所以这个东西叫做调用堆栈

  4. 之前在函数栈帧的创建和销毁那节课,也说过main函数也是被别的函数调用的。可以在调用堆栈右击,勾选显示外部代码
    在这里插入图片描述
    在这里插入图片描述
    这些东西都是隐藏起来了,我们关注到的只有main函数。(不同的编译器调用逻辑可能不一样)

3.3.4查看汇编信息

任何一段C语言代码调试起来后,都可以观察汇编代码
在这里插入图片描述
就可以看到C语言所对应的汇编代码
在这里插入图片描述
也可以在调试窗口打开汇编代码
在这里插入图片描述

3.3.5查看寄存器信息

CPU上还集成有寄存器,如果想去观察这些寄存器,就可以再调试模式下,打开寄存器窗口
在这里插入图片描述
点击去之后,就能看到一堆的寄存器
在这里插入图片描述
红色是说明这些寄存器的值在变化。而如果记住寄存器的名字,在监视窗口,也可以通过输入寄存器的名字来观察寄存器的变化
在这里插入图片描述

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

当我们学了这些快捷键和他们的使用方法之后,我们一定要多动手去调试,你会调试的过程中不断进步,并且加深自己对代码的理解。
我们初学者当前大部分的时间都是在写代码,很少调试。但越到后期,各种语法问题对我们来说会越简单,只需要很少的时间就能写完代码了,而大部分的时间都是在调试。
因此我们要熟悉快捷键,提升效率

5.一些调试的示例

5.1实例1

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

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
在这里插入图片描述
这就发现代码有问题了,就叫做发现问题的存在。接下来就要找问题
我们先要有一个清楚的思路,对代码心中有数。
在这里插入图片描述
我们现在是这样想,去调试的时候,看代码是不是这样走。如果不是就能找打问题。

  1. f10调试,直接来到scanf输入
    在这里插入图片描述
    输入3。因为输入3,结果已经错了。再用太大的数据去调试,不好观察
    在这里插入图片描述
    发现n果然是3,和我们预想的一样。说明前面的代码都没有什么问题。

  2. f10进入循环,循环最开始i为1,再监视窗口看到i确实为1,没有问题
    在这里插入图片描述
    j初始化为0,来到第二层for循环
    在这里插入图片描述
    在59行这里,j又被赋值为1。此时的i为1。所以j<=i成立,进入循环。
    在这里插入图片描述
    进入之后ret\*\=j;,ret最开始为1,所以是1*1,ret得到的是1。监视一看ret果然是1
    在这里插入图片描述
    之后来到59行,循环的调整和判断部分。j++,j变成了2。j<=i不成立,为假,不再进入循环。之后sum+\=ret;,sum最开始是0,ret为1。所以加上去之后sum为1
    在这里插入图片描述

  3. 此时第一层的for结束了,来到56行,i++,i变成2
    在这里插入图片描述
    走下去,j又创建为0。来到59行,j被赋值为1,i此时是2,j<=i成立。进入循环。ret *= j;,ret最开始得是1才是计算阶乘啊,一看监视,ret果然是1
    在这里插入图片描述
    之后1*1,ret还是等于1。这时第二层for循环的第一次循环结束,来到59行,循环的调整和判断部分。
    在这里插入图片描述
    j++,j变成2。此时i还是2,所以j<=i成立。进入循环
    在这里插入图片描述
    此时ret为1,j为2,所以走完61行,ret为2
    在这里插入图片描述
    之后来到59行
    在这里插入图片描述
    j++,j变成了3,i此时为2。所以j<=i不成立,为假,不再进入循环。来到63行
    在这里插入图片描述
    sum为1,ret为2,所以sum+ret之后为3。并且来到56行
    在这里插入图片描述
    发现sum果然还是3,也没有问题。

  4. i++,i变成3。j又初始化为0。
    在这里插入图片描述
    来到59行,j被赋值为1,j<=i成立。
    在这里插入图片描述
    ret此时此刻应该是1,第二层for计算的结果才是i的阶乘。但是此时去瞄了一眼ret,居然是2啊。和我们预想的不一样,所以问题出现在ret身上。这就是对错误进行定位
    我们接着调试下去,ret为2,j为1。ret\*\=j;ret为2
    在这里插入图片描述
    再来到59行,j++,j变成2。此时i为3,j<=i成立,进入。
    进入之后ret为2,j为2,所以走完61行,ret为4
    在这里插入图片描述
    再次来到59行,j++,j变成3,i此时还是3。j<=i成立,进入
    在这里插入图片描述
    ret为4,j为3,所以61行之后,ret为12
    在这里插入图片描述
    再来到59行,j++,j变成4,j<=i不成立,不进入循环。第二层for结束,来到63行。
    在这里插入图片描述
    sum此时为3,ret为12,sum+ret后变成15。并且来到56行
    在这里插入图片描述

  5. i++,i变成4,n是3。i<=n不成立,不再执行循环,至此两层for执行完毕,结束,最后输入sum的值15

  6. 最后我们发现,程序出错的原因是因为ret在第二层for计算阶乘开始的值,不是我们预想的1,导致后面3!的阶乘被错位计算成了12,使得我们看到的sum是15。因此可以发现到问题是ret的值在计算完2!阶乘后,ret的值是2!,也就是2,被保留了下来。等到计算3!的时候,因为ret的初始值不是1,这就确定了错误产生的原因

  7. 因此通过上面的分析,我们可以在进入第一层for循环之后,把ret的值设置成1,使得上一次计算出的阶乘的值不会影响,下一次的阶乘。这就是纠正错误的解决办法

  8. 此时此刻再次去运行我们的代码,发现程序正常输出了
    在这里插入图片描述
    这就是对程序错误予以改成,重新测试。测试结果通过,说明我们就对不过完成了了一次调试

5.2实例二(【nice公司的笔试题)

注意此代码是在VS2022的Debug版本下的x86环境调试的,不同编译环境下发生的结果可能不同

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

在这里插入图片描述

研究程序死循环的原因

  1. 先来简单分析一下这个代码
    在这里插入图片描述
    所以这个代码就是从首元素开始,循环将数组元素赋值成0,并且每次循环打印一次hehe。但是数组arr的下标最大时9,而循环的条件是i<=12。因此循环里面的arr[i]=0;肯定会发生数组越界

  2. f10调试代码,直接来到75行
    在这里插入图片描述

  3. 在第一次来到75行的时候,i被赋值为0,i<=12成立,进入循环
    在这里插入图片描述

  4. 执行arr[0]=0;,arr首元素被改0。并且这个时候的屏幕上没有打印
    在这里插入图片描述

  5. 执行78行的打印之后,屏幕输出了hehe。
    在这里插入图片描述

  6. 来到75行,i++,i变成1。i<=12成立,再次进入循环
    在这里插入图片描述

  7. 之后arr[1]被改成0,打印hehe
    在这里插入图片描述

  8. 就这样一直修改arr的值,再打印。直到i为9,数组内所有元素都被改成了0,打印10次hehe
    在这里插入图片描述

  9. 来到75行,i++,i变成10。i<=12成立,还要进入循环
    在这里插入图片描述

  10. 当i等于10的时候,已经不再arr的下标范围内了。此时arr[i]=0;必然越界访问,我们可以在监视加上arr[10],是个CCCCCCCC的随机值
    在这里插入图片描述

  11. 执行75后,发现arr[10]的值被修改成了0了
    在这里插入图片描述
    此时的arr[10]这块空间,不是我们申请的变量创建的空间,越界访问了。这是非法的

  12. 再走78行,发现也会打印hehe
    在这里插入图片描述

  13. 再次来到75行,i++,i变成11。进入循环,看看是否会访问下标11的元素。同时在监视里加上arr[11],发现还是个随机值
    在这里插入图片描述

  14. 走完77行和78行,arr[11]的值确实被修改成了0,也打印了一个hehe
    在这里插入图片描述

  15. 来到75行,监视再加上一个arr[12]
    在这里插入图片描述
    这个时候往右边一看,arr[12]的值不是随机值了,而是11。i此时还没有i++,因此i还是11。为什么这两个的值一样?难道是同一块空间吗?接下来再看看

  16. i++,i变成12,i<=12成立,进入循环。而arr[12]的值也跟着变成了12
    在这里插入图片描述

  17. i此时为12,arr[i]=0;也就是arr[12]会被改成0。执行完之后一看果然改成了0,但是i的值也变成了0
    在这里插入图片描述
    从这里我们就可以推测出,i和arr[12]好像真的是同一块空间。

  18. 在监视观察i和arr[12]的地址
    在这里插入图片描述
    地址相同说明,它们是同一块空间。所以说当我们越界访问,走着走着就访问到i去了。而我们不小心把i改了,后果非常严重。
    i改成0,来到75行i++,i变成1,i<=12成立,又会进入循环,之后i++又走了一遍,打印hehe。在i加到12,进入循环又把i改成了0。再次回到起点,i最大就是12,永远没有机会大于12。循环的判断部分一直成立,这就是死循环打印hehe的原因

  19. i和arr[12]为什么地址一样?这是因为内存布局的原因,这些变量在创建的时候就是这样放的
    在这里插入图片描述

  20. 我们刚刚创建的这些i、arr都是局部变量,局部变量是在栈区创建的。所以i和arr都放在栈区
    栈区内存的使用习惯是先使用高地址的空间,再使用低地址的空间(不一定上面就是高地址,下面就是低地址。反着写也可以,只要保证从高地址到低地址使用就可以了)
    在这里插入图片描述

  21. i是先开辟的,所以i在上面,地址是0x00f3fa28。而arr是后开辟,而栈区的使用习惯,先使用高地址,再使用低地址。所以arr的空间要在i的下面,地址是0x00f3f9f8
    在这里插入图片描述

  22. 当我们把下标为i的元素改成0的时候,从下面开始往上改
    在这里插入图片描述
    在这里插入图片描述

  23. 假设下标为9后面的空间的下标是10、11、12
    在这里插入图片描述
    继续把下标为10和11的空间改了
    在这里插入图片描述

  24. 当来到下标为12的空间的时候,12和i就恰好遇到了一起。改arr[12]的时候,其实就是在操作0x00f3fa28这块空间,i也是这块空间。因此改arr[12]也就改了i。
    在这里插入图片描述

  25. 总结来说就是因为栈区的使用习惯,使得变量i的地址大于arr[9],又因为数组随着下标的增长,地址由低到高。当arr向后越界访问的时候,就有可能遇到i

  26. 注意这是针对VS2022的Debug版本下的x86环境特地设计出来的,有些编译器可能i和arr[9]之间空一个整型,或者没空。但原理都是一样的。至于为什么空,我也不知道。

5.3Release版本会对代码进行优化

用实例二的代码说明这个问题

  1. 在Debug版本下的x86下
    在这里插入图片描述
    上面就说过了,i的地址要大于数组arr每个元素的地址。而数组随着下标的增长,地址从低到高,所以随着数组向后访问的时候,是有可能访问到i的。

  2. 我们换成Release版本,把下面的代码放开
    在这里插入图片描述
    并没有死循环,这是因为编译器帮我们做了优化处理

  3. 可以通过地址看到i和arr的顺序发生了变化
    在这里插入图片描述

  4. 说明Release版本下,编译器对我们的代码做过优化,内存布局都变了。编译器认为应该把i和arr的顺序颠倒过来。具体优化我也不知道,也不需要纠结。

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

我们作为程序员,不能指望说写出一个有bug的代码,然后再去调试改bug。这是不合适的,我们应该写出好的代码,易于调试的代码

6.1优秀的代码

  1. 代码运行正常
  2. bug很少
  3. 效率高
  4. 可读性高
  5. 可维护性高
  6. 注释清晰
  7. 文档齐全

6.2常见的coding(编码)技巧

  1. 使用assert
  2. 尽量使用const
  3. 养成良好的代码风格
  4. 添加必要的注释
  5. 避免编码的陷阱

6.3示范

模拟实现strcpy库函数

6.3.1strcpy函数的用法

  1. strcpy的介绍
    在这里插入图片描述
  2. 用两个数组来演示一下(world单词不会,懒得改了。就当时这个吧)
    在这里插入图片描述
  3. strcpy返回的是目的地的地址,在这里也就是返回arr2的地址,而打印字符串需要的就是地址。因此我们可以这样打印
    在这里插入图片描述
  4. strcpy会把源字符串的\0也拷贝过去
    在这里插入图片描述

6.3.2模拟strcpy

6.3.2.1版本1
  1. 我们先只关心将源字符串拷贝到目的地字符串。而不考虑返回目的地地址的问题,返回类型可以设置成void
    在这里插入图片描述
    相应的,我们先用前面的代码来打拷贝
    在这里插入图片描述

  2. arr1和arr2都是一块连续的空间
    在这里插入图片描述

  3. 我们要实现strcpy,那就是arr1内容拷贝到arr2。第一个先拷贝arr1的h,把h拷贝到arr2
    在这里插入图片描述

  4. 拷贝完成了,需要把arr1的第二个字符e拷贝到arr2。所以需要指针src向后移动一步,找到e。同样为了对应起来,dest也需要往后走一步
    在这里插入图片描述

  5. 当src指向e,要拷贝e到arr2的时候,又要指向\*dest=\*src。当拷贝完e之后,src、dest又要往下走。往下走之后又要拷贝内容,所以这是个循环
    在这里插入图片描述

  6. 当拷贝完成\0之后,循环停止
    在这里插入图片描述

  7. 这个时候来写我们的代码
    在这里插入图片描述

  8. 循环拷贝完\0,则停止。那就是找到用*src找到\0。
    在这里插入图片描述

  9. 当src指向\0的时候,循环就没有继续拷贝了。但是\0还要拷贝,因此可以在循环后这么写
    在这里插入图片描述
    因为当src停下来的时候,src指向的就是\0,而dest因为也++了,所以指向的是\0的对应位置

  10. 我们来测试一下
    在这里插入图片描述
    确实做到了拷贝。当我们写到这里的时候,这个函数只是在一定程度上完成了任务。但是还不够好,我们可以优化它

  11. 比如说我们传参的时候,传了一个指针p,但是p恰好被改成了空指针NULL
    在这里插入图片描述
    什么都没发生,但是程序已经崩了。因为把空指针传过去给dest,*dest就对空指针解引用了,就出现问题了。

  12. 如果dest和src这里我们不小心传过来的是NULL,而下面没有做任何防护。这个时候对NULL解引用就很危险。程序崩了的时候,我们都不知道哪里出现了问题,还要一点点去调试。

6.3.2.2版本2
  1. 我们可以做一个断言来优化版本1,用assert这个宏来断言。需要包含<assert.h>
    在这里插入图片描述
    这个时候屏幕上的报错是非常有意义的,直接告诉我们第几行出错。出错的原因是什么,省了调试的时间。

  2. 所以这个断言是一个很好的东西,在里面输入输入期望发生的表达式,表达式为真,就啥也不干。表达式为假,就断言,报错。这相当于预判程序可能会出错的问题
    比如这里我希望dest != NULL,意思就是我希望dest不是NULL。但当dest是NULL的时候就停下来,报错

6.3.2.3版本3
  1. 前面说\0,和hello word!没有放在循环那里拷贝。那就把都放在一起拷贝
    在这里插入图片描述
  2. 当然也可以这样写
    在这里插入图片描述
6.3.2.4版本4
  1. 写到这里的my_strcpy函数,还是和strcpy函数有差别。那就那就是strcpy函数返回了目标空间的的起始地址
    在这里插入图片描述
  2. 我们要返回目标空间的起始地址,也就需要保存dest在++之前的地址。之后返回就行了
    在这里插入图片描述
6.3.3.5版本5
  1. 此时此刻my_strcpy函数和strcpy库函数已经很相似了,但是还是有一点小小的不同
    在这里插入图片描述

  2. 少写const,然后再写代码的时候,又错写把*dest++ = *src++,写成了
    *src++ = *dest++。一去运行就出错了
    在这里插入图片描述
    相当于arr2的内容往里arr1里拷贝,放都放不下

  3. 如果加上了const的话,这里就直接报错了。
    在这里插入图片描述
    这样就逼着你去修改你的错误

6.3.6使用strcpy拷贝要保证目标空间大于或等于源字符串

strcpy函数只关心拷贝,它不管目的地放不放得下来自源字符串的内容。我们作为程序员,在写代码的时候也要保证目的地空间大于或等于源头空间
在这里插入图片描述

6.4const的作用

6.4.1const修饰变量

  1. 我们想改一个变量的值,现在有两种办法
    在这里插入图片描述

  2. 当有这样一个基础后,加上const
    在这里插入图片描述
    我们说const修饰变量n,使变量获得了常属性,不能再被修改

  3. 当直接用变量n改不了的时候,采用指针来改
    在这里插入图片描述
    发现居然能改。我们说const修饰n的时候,语法限制n不能改了,当你这样改了之后语法编不故去。但是可以却可以用指针改,其实这也是有问题的。就像狂飙里高启强不方便做一些事情,就告诉老默我想吃鱼了,老默就去做了这些事。
    这里的n不能改了,高启强不方便出手,就把n的地址交给老默,让老默去改。但是老默做这个事情也是违法的。
    现在n不能改,改就触犯语法规则。但是把n的地址交给p,p就把n改了。p这样改其实本质上也是违背规则的。造出const关键字就是为了不让你改n,而你却千方百计的去找其他方式改,本身就是有为规则的。

  4. 所以我们为了补齐规则的漏洞,给p也加上了const。你想通过指针p改,那我就给p加上限制,加上const。这个时候就编不过去了
    在这里插入图片描述
    这里就可以看出const是可以修饰指针的

6.4.2const修饰指针

  1. 正常情况下
    在这里插入图片描述

  2. const int* p = &n;之后
    在这里插入图片描述
    *p这个动作就非法了

  3. 改p的值,发现能改
    在这里插入图片描述
    所以当const在这个位置的时候,*p改变它所指向的内容不可行,但是改变p的指向可行。

  4. 将const换一个位置
    在这里插入图片描述
    发现还是不能改*p所指向内容,但是能改p的指向

  5. 再换一个位置
    在这里插入图片描述
    发现能改*p所指向内容,但是不能改p的指向

  6. 当const放在*左边的时候,限制的是指针指向的内容,不能通过指针改变指针所指向内容,也就是不能通过p改变m的内容。但是指针变量是能改变的,也就是可以改变p的指向。

  7. 当const在*右边的时候,限制的是指针变量本身,能通过指针改变制作所指向内容,也就是能通过*p改变m。但是不能改变指针变量本身,也就是不能改变p的指向。

  8. 如果在*两边都加上const,指针指向内容和指针本身都被限制了。都不能改变
    在这里插入图片描述

  9. 可以看出const有一种保护的作用。const放在左边保护*p,p指向内容不能被修改。const放在*右边保护p本身,p的指向不能被修改。

  10. 回头看my_strcpy函数
    在这里插入图片描述

6.5模拟实现strlen函数

  1. 之前我们就写过这个函数的循环版本和递归版本
    在这里插入图片描述

  2. 现在用assert断言和const优化它
    在这里插入图片描述
    我们只是数一下这个字符串的长度,并不想这个字符串被修改,所以形参加上const限制
    在这里插入图片描述

  3. 这个str是指针,万一是空指针就很危险,可以断言
    在这里插入图片描述
    当str不是空指针,str!=NULL成立,继续往下走。当str为空指针时,str!=NULL不成立,断言报错

  4. 最后写成这样
    在这里插入图片描述

  5. my_strlen和strlen库函数还是有点差异
    在这里插入图片描述

  6. 我们可以把它的返回类型设置成size_t。紧跟着count,还有接受的len,打印的类型都要变。就严格对应起来了
    在这里插入图片描述

7.编程常见的错误

7.1编译型错误

在编译期间产生的错误。直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单
比如说我们现在忘写分号了
在这里插入图片描述
这些错误信息有提示作用,当然偶尔有可能是没有用的提示信息。而且不管你在哪一行代码,只要双击就会来到错误的地方。这种编译型错误一般找到的都是语法错误。

7.2 链接型错误

链接期间发生的错误。看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误
在这里插入图片描述
而且双击这种错误,是不会跳到错误代码位置的。相对来说难解决,主要是看符号在不在。你调用了,但是定义的时候,实际不是这个符号,就找到了问题所在

7.3运行时错误

代码运行起来了,但结果不是我们想要的。借助调试,逐步定位问题。最难搞
比如说我们这里Add要实现的是相加,但是不小心把+写成了-号就出现问题了。但是代码也能编译链接过去
在这里插入图片描述
这个就是因为疏忽写错了符号,当我们还是新手的时候,需要花大量的时间去调试找问题。

前期我们出现最多的错误就是编译型错误和连接型错误,但我们熟练了各种语法之后,慢慢都会变成运行时错误。

8.总结

还没想好

  • 17
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
sscanf函数是C语言中一个非常常用的函数,它可以将一个字符串按照指定的格式转换成相应的数据类型。在嵌入式开发中,sscanf函数也是非常常见的,因为很多时候需要从串口或者其他外部设备中读取数据,并将其转换成相应的数据类型进行处理。下面是一些sscanf函数的使用技巧: 1. 使用sscanf函数时一定要注意格式字符串的正确性。格式字符串中的占位符必须与待转换的数据类型相对应,否则会发生未知错误。 2. 如果待转换的字符串中包含多个数据,可以使用多个占位符进行转换。例如,如果待转换的字符串为"1,2,3",可以使用" %d,%d,%d"的格式字符串进行转换。 3. 可以使用sscanf函数的返回值来判断转换是否成功。如果返回值等于待转换字符串的长度,则说明转换成功,否则转换失败。 4. 如果待转换的字符串中包含浮点数,可以使用"%f"或者"%lf"的格式字符串进行转换。 5. 如果待转换的字符串中包含十六进制数,可以使用"%x"的格式字符串进行转换。 6. 如果待转换的字符串中包含字符或字符串,可以使用"%c"或者"%s"的格式字符串进行转换。 7. 如果待转换的字符串中包含指针类型的数据,可以使用"%p"的格式字符串进行转换。 总之,在使用sscanf函数时一定要注意格式字符串的正确性,否则很容易出现转换错误的情况。同时,还应该注意sscanf函数返回值的判断,以确保转换的正确性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值