河南视频网站建设公司,响应式模板网站模板下载,新手做地方门户网站,深圳燃气公司招聘信息前言#xff1a;
对万千程序猿来说#xff0c;在这个世界上如果有比写程序更痛苦的事情#xff0c;那一定是亲手找出自己编写的程序中的bug#xff08;漏洞#xff09;。作为新手在我们日常写代码中#xff0c;经常会出现报错的情况#xff08;好的程序员只是比我们见过…前言
对万千程序猿来说在这个世界上如果有比写程序更痛苦的事情那一定是亲手找出自己编写的程序中的bug漏洞。作为新手在我们日常写代码中经常会出现报错的情况好的程序员只是比我们见过的bug多从而减少出错但当我们遇到报错时大家可能都会出现看不懂的情况以至于在那里捣鼓半天最后还是当上了“C/V”工程师。本期基于vs环境下我将带领大家去搞懂代码的调试的小技巧。 文章目录1. 什么是bug2. 调试是什么有多重要2.1 调试是什么2.2 调试的基本步骤2.3 Debug和Release的介绍3. Windows环境调试介绍3.1 调试环境的准备3.2 学会快捷键4.实例演示4.1实例一阶乘之和5.2 实例二死循环问题1. 什么是bug 首先当我们想要去战胜它时我们先要了解它。就像打战一样知己知彼方能百战不殆。 大概由来就是有一次在编写程序计算机发生故障经过排查在计算机的继电器触电里找到了一只被夹扁的小飞蛾这只小虫子卡住了机器的运行并诙谐的把程序故障称为“bug”。这就是我们今天最爱说的“bug”的由来。它的意思和原身一致真就是“一只臭虫”。 具体原因可以了解 bug的由来 2. 调试是什么有多重要
就像警察办案根据线索一步步的推理和考察最后得出最后的真相。或许我们最有印象的就是我们看过的【名侦探柯南】。一名优秀的程序员是一名出色的侦探每一次调试都是尝试破案的过程*
对于绝大多数的新手玩家而言我们写代码就是“三下五除二”管它三七二十一一上来就是一顿猛敲但是到最后的看着密密麻麻的报错人都要麻了。 又是如何排查出现的问题的呢 调试错误时或许像这样 这样无脑的去进行增删查改到最后忙活半天可能都还在原地踏步。因此掌握好调试就显得十分重要。 2.1 调试是什么 调试英语Debugging / Debug又称除错是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。 2.2 调试的基本步骤
a.发现程序错误的存在 b.以隔离、消除等方式对错误进行定位 c.确定错误产生的原因 d.提出纠正错误的解决办法 e.对程序错误予以改正重新测试 2.3 Debug和Release的介绍 紧接着我们再来看一下VS下的两种版本即-----Debug和Release a Debug通常称为调试版本通过一系列编译选项的配合编译的结果通常包含调试信息而且不做任何优化以为开发人员提供强大的应用程序调试能力便于程序员调试程序。 b 而Release通常称为发布版本是为用户使用的一般客户不允许在发布版本上进行调试。所以不保存调试信 息同时它往往进行了各种优化以期达到代码最小和速度最优。为用户的使用提供便利。
我们还是通过代码来展示
#includestdio.hint main()
{char* p hello world;printf(%s\n, p);return 0;
}当我们写出以上代码并且放在【Debug】版本下运行
当我们去文件查看在【debug】下的信息时我们看到结果如下图所示
而当我们的代码在【Release】版本下运行的时候 我们可以看到同样的程序在两个版本下的文件大小是不同的。 所以我们说调试就是在Debug版本的环境中找代码中潜伏的问题的一个过程。 那编译器进行了哪些优化呢 请看如下代码
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 】模式去编译程序没有死循环。 那他们之间有区别就是因为优化导致的。 3. Windows环境调试介绍 注linux开发环境调试工具是gdb后期课程会介绍 3.1 调试环境的准备 在环境中选择 debug 选项才能使代码正常调试。
3.2 学会快捷键 上图我打钩的就是平时经常用得到的一些快捷键记住快捷键将大大提高我们调试的效率接下来我将具体介绍
F5 启动调试经常用来直接跳到下一个断点处。 F9 创建断点和取消断点 断点的重要作用可以在程序的任意位置设置断点。 这样就可以使得程序在想要的位置随意停止执行继而一步步执行下去。 F10 逐过程通常用来处理一个过程一个过程可以是一次函数调用或者是一条语句。 F11 逐语句就是每次都执行一条语句但是这个快捷键可以使我们的执行逻辑进入函数内部这是最长用的。 CTRL F5 开始执行不调试如果你想让程序直接运行起来而不调试就可以直接使用。 更多的快捷键可以通过如下查看 https://blog.csdn.net/mrlisky/article/details/72622009 4.实例演示
说了那么多终究全是书面上的东西接下来我们通过具体的实例带大家感受一下。
4.1实例一阶乘之和
代码思维 在我们开始写代码之前一定要想好n的阶乘和是什么 当我们脑海中有了思路之后写起来就会很快而不是直接就上手逻辑很简答首先输入n表示n的阶乘之和最后在进行求和操作即可 紧接着想每一步需要用到的知识具体如下
1、阶乘1x2x3…xn 用到了循环语句
2、求和还是用到了循环
最后就是打印出来即可
代码如下
int main()
{int i 0;int sum 0;//保存最终结果int n 0;int ret 1;//保存n的阶乘(乘法的话一定要初始化为1)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】为例。在我们的想象中【3】的阶乘之后就是 第一步1的阶乘即1 第二步2的阶乘即122 第三步3的阶乘即12*36 第四步三数相加即1269即最终结果为9 那么真的是这样吗接下来我们就通过一步步的调试去看看结果。
首先看到我们进入了内存循环此时i 1, j 0循环会开始执行一次因此可以求出此时【1】的阶乘为 1! 1 当第一次跳出内层循环之后我们就可以求出【1】因此可以看到【sum】此时为1 紧接着我们再去计算【2】此时在我们函数的内部会进行两次的循环操作执行完毕后此时各个值如下表所示 此时将我们【2】计算完毕之后可以得到【ret】的值最后在进行累加的操作即【12】的操作所以此时的【sum】应该为3结果如下 前两次执行完之后紧接着就回去执行【3】的操作在内部进行三次循环操作我们已经知道【3】的结果为6我们在继续【F10】看结果 第一次内部循环开始此时【j1】【ret2】 第二次循环完毕之后此时【j2】【ret4】 第三次循环完毕之后我们可以发现此时【j3】,【ret12】 最后一步在循环完之后我们就需要计算累加和此时我们可以发现当【j4】时我们跳出循环操作而最终的结果显示的是【15】明明应该是【9】啊.咦…怎么会是【15】呢此时各位好奇的小脑袋瓜就开始躁动起来了 遇到程序出错了不要害怕我们仔细分析一下。 我们来整理一下这样写的思路 a n1时我们进入第一个循环然后并没有发生什么进入第二个循环【ret1*1】【sum01】此时是没有问题的 b n2时我们在进入在第一个循环没有发生什么进入第二个循环【ret1 * 11】但是请注意在这之后并不会计算【sumsumret】而是继续在第二个循环中没有跳出来因为第二个循环的条件是【i2】此时仍然是真的所以第二个循环继续【ret1 *22】在跳出第二个循环【sum02】但是请注意此时第一个循环没有结束对于第一个循环此时【n1】还要继续【n2】的情况所以最终结果是【4】因此在这一步就出现了错误。
因此我们可以这样改每次重置【ret】的值此时运行结果就正确了 还有一种方法就是我们可以不使用两层嵌套来进行封装我们只定义一层循环具体代码如下 int main()
{int n 1;scanf(%d, n);int ret 1;//保存n的阶乘(乘法的话一定要初始化为1)int i 1;int sum 0;//保存最终结果for (i 1; i n; i){ret * i;sum ret;}printf(%d\n, sum);return 0;
}5.2 实例二死循环问题
首先我给出一段代码大家可以猜猜程序最后将会输出什么
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;
}
相信大多数的小伙伴看到这个程序的第一印象就是数组下标为【0-9】而这里却是【12】很显然的问题就是数组访问越界 然而真的是这样的吗?老规矩直接运行程序看最终是不是我们想的那样。 咦…我们发现怎么结果会是死循环打印呢接下来你如果要搞清楚这个问题你能肉眼分析出来是因为什么吗这时候就需要用到调试了 开始时我们进入循环对数组进行初始化操作得到以下结果 紧接着我们进入循环里面去对数组元素都改为【0】一直进行到对【arr[9]】的操作到这个元素都是在我们的正常范围之内进行的。 那么接下来的【arr[10]】呢【arr[11]】和【arr[12]】呢它们是什么样的呢我们继续调试下去
从上图 我们可以发现它依然对其进行操作那么为什么对【arr[10]】这个位置还能访问得到呢接下来我给大家解释解释 因为当我们的数组存放在内存中的时候是有一片连续存储的空间而数组之后也存在一定的空间而这一片连续的地址空间都存在于【main】函数的栈帧空间下在它看来存储空间之间都是连续的因此数组之后的空间也是可以访问到的。
除了这个问题之外还有一个明显的问题不知道大家有没有发现就是当我们执行到最后时【i】和【arr[12]】两个值竟然同时变为了0这又是为什么呢 接下来我们我们分别对两个进行取地址的操作就可以发现这两个竟然是指向的同一片地址空间
到了这我们就可以想到对于变量【i】它应该是位于整个数组结束位置的后两位上只有这样才会在数组越界访问的情况下改变【i】的值最后在修改这块块空间中的值时将循环变量【i】的值做了修改因此使得【i】的值永远不可能到达13因此才会出现死循环打印的情况。 接下来我们通过内存布局来进行进一步的了解。 首先我们知道【i】和【arr】是局部变量元素而局部变量是放在内存中的栈区上的而栈区的使用习惯是先用高地址空间再用低地址空间这点非常重要可以发现变量a的地址是比变量b的地址来得大的。
那么在内存中栈区究竟是怎么样的呢我们通过以下图片为例。 程序一进到【main】函数的函数栈帧中时就会先为变量【i】开辟一块空间接着可能就会空出几个位置再为【arr】数组开辟十个元素的空间根据上述我们可以发现空出来了几个位置那么为什么要空出来呢这里面可是有大学问的 这并不是我规定的或者谁规定的中间的大小而是取决于编译器 1.VC6.0编译器下中间就没有多余的空间 2.在gcc这个Linux环境下的编译器中创建的局部变量之间会空出一个整型也就是4个字节 3.在VS 2013/2019/2022这些编辑器中中间都会空出两个整型也就是8个字节 因此在不同的编译器下去运行这段代码虽然得到的都是死循环这个现象但是底层的实现是有些区别的。
紧接着我们可以知道数组在平时的使用过程中都是从低到高的但是数组的地址是否也是这样的呢我们进行一下测试。 从上图我们可以发现数组的每个元素地址都是从低到高进行一个变化的。
有了这些知识储备之后我们在回过头去看最开始的问题这时就可以很好的回答了 程序开始的时候变量【i】先创建出来在内存中先开辟的地址空间而【arr】数组的地址空间是后开辟出来的。但是刚才我们已经知道数组的下标和数组元素的地址变化顺序都是从低到高而内存中的堆栈则是先使用高地址再使用低地址因此当数组进行向后访问时就有可能找到变量【i】并且把其覆盖掉因此就有可能把循环变量的值改为其他的情况从而导致循环结束条件不能达到就导致了死循环打印的现象。到此一切就讲通了 因此正确的解决方案还是改我们的循环结束条件。 总结 通过本期的学习我相信大家以后在遇到程序出现报错的时候就不会无脑的直接去程序里面增删查改了大家可能会说这个很难但是俗话说得好呀害怕恐惧的最好办法就是战胜恐惧 以上便是本期的所有内容啦感谢您的观看如果对你有帮助的话记得三连支持一下哟