作者:到处挖坑蒋玉成
链接:https://zhuanlan.zhihu.com/p/24128782
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
“每次改bug都只靠蒙”这个是很多编程入门的初学者都会遇到的问题——这篇文章将会对如何进行调试,做一个比较浅显的入门介绍。虽然你看完这篇文章之后,不见得就能一下子找到bug在哪,但是至少可以做到有的放矢,不至于完全只能随缘。
对程序进行调试,一个最基本的思路,就是二分法——类似于算法中的二分查找,对于一个程序来说,其bug大多数情况下都只是集中在一个子模块里,我们通过二分法,
不断地把程序一分为二,排查缩小可能有问题的地方,这样一来调试的效率就比起“随缘法”和遍历法就高多了。
这里使用Visual Studio 2015调试一个C++程序作为例子——其他语言和环境的方法是差不多的。如图所示:
然后这个程序运行的时候会炸:
这个时候我们就需要使用分治法来判断,程序到底是哪个部分出了问题了——这里我们采用一种虽然看起来“大力出奇迹”,十分简单粗暴,但是实际上却很有效,适用范围极广的方法:输出法。
比如对于上面这个程序,我们要判断程序里的两个for循环,到底是哪个出了问题——这个时候我们就可以通过添加一些额外的输出语句,作为标记。
比如对于上面的程序,我们可以在第16行添加以下代码:
cout<<"part 1 finished"<<endl;
然后运行的结果就是这样:
我们可以看到,在炸之前,程序成功输出了这行标记——这就意味着,仅针对这个例子来看,第十六行之前的代码是没有问题的。进一步讲,如果程序逻辑更加复杂的话,你还可以输出特定的某些变量的值,看看这个变量的值是不是符合预期的。
这个时候,我们把注意力放在16行之后的代码上——对于如此小的代码块,我们已经可以直接用肉眼进行查错了。C++基础比较好的同学可以直接看出,这是一个典型的访问越界错误,向量v1现在只有n个元素,因此v1[n]是不存在的(大多数语言的数组索引都是左闭右开的,v1的最后一个元素索引是n-1),访问这个元素自然就会报错。
把i<=n的等号去掉,这个时候程序便能正确运行了:
这种方法,不光适用于CLI程序,GUI程序也可以。比如用C#写WinForm的话,就是这样:
这样就可以输出你要的内容了——可以是一个标记,也可以是特定变量的值。
当然,这种“大力出奇迹”的办法,只是权宜之计——接下来将会介绍如何使用调试器。
首先让我们明确一个概念——“断点”。所谓“断点”是调试器提供的一个功能之一,它可以让你的程序执行到某个步骤的时候停下来,进入中断模式。此时程序既不会被终止,也不会继续执行——接下来你就可以通过调试器,控制程序的执行过程了。
这里我们还是以C++和Visual Studio 2015为例进行说明——
其他语言和其他环境都是类似的(虽然调试器可能不怎么好用),万变不离其宗。
还是上面那个程序,在第十六行代码左边点一下,如图所示:
点击运行,然后输入数字,接下来你就会看到程序停在了断点处:
于是我们可以得出跟之前一样的结论——第十六行之前的程序是没有问题的。
接下来,点击“继续”,就可以让程序直接执行到下一个断点处(断点可以加不止一个),或者直接结束——同时,工具栏上有两个按钮,可以控制程序一步一步地往下运行,这样就可以更加精确地确认哪里有bug。
这篇先讲这些——关于调试器的更多功能,之后再介绍……
调试器一共有三个最为核心的功能:
单步调试、查看变量的值,以及查看调用栈。接下来将会对这三个功能,逐一进行介绍。
首先让我们来看下查看变量的值——还是上面那个程序,点击放大,可以看到我们已经在下方的“局部变量”窗口,看到一些变量的值了。比如说,这里我们能看到n的值为10
不光是简单变量,Visual Studio还允许我们查看更为复杂的成员变量的值——比如说,这里我们建立了一个vector,然后将0-9这10个数字都添加了进去。这个时候,我们直接点击旁边的箭头,就可以展开v1:
这个时候,我们可以看到,展开之后,我们可以看到v1的详细信息——包括容量(capacity,这里我们可以看到STL默认分配了稍微大一点的空间),以及其中每一个元素的值。而我们自己定义的数组arr,因为还没有new,所以还不能访问。
接下来,让我们把注意力放到上方的工具栏:
Visual Studio提供了两个不同的按钮——逐过程(右侧)和逐语句(左侧)。所谓“逐过程”指的是如果接下来要执行的语句是一个函数(或者对象的成员)的话,那就直接执行整个函数,只看返回结果,而“逐语句”指的则是直接进入函数/对象的体内。因此,需要注意的是,
如果某个语句使用了标准库的某些方法或者运算符重载的话,那就不要使用“逐语句”,不然会钻进标准库的代码里,没有意义。
可以看到,这是钻进了VC的库提供的new运算符的定义里。
灵活利用单步调试功能,可以让你即使是不能直接用肉眼看出,也可以用“笨办法”找出bug——比如说,对于这个例子,我们使用单步调试,如下图所示:
现在循环体已经执行完了10轮,此时i=9,我们继续执行:
现在i变成10了,接下来一步就是执行赋值操作了,再往下走一步:
现在我们已经可以精确定位到具体是哪一步出了错,修改起来自然就容易多了。
另外,初学者使用VS做练习的时候,经常会出现一个问题——VS的程序执行完之后,命令行窗口就直接消失了,看不到输出结果。利用调试功能,我们也可以有一个非常简单而又没有额外影响的方法来解决这个问题:把断点打在return 0;上——这样一来,当程序的主体部分完全执行完毕之后,就会自动停在结束前的最后一步(return 0;),然后你就可以看到完整的输出结果了。
最后让我们来看一下函数调用栈查看功能的使用——首先,你要懂得什么是函数调用栈,这里我就不再废话了,不懂的直接去上我的课: C++ 程序设计 - 课程 - 计蒜客
我们直接用这个图来举例——这是一个用递归法求阶乘的函数:
代码如下——我们直接停在fac(1)这一步:
注意看右下角的调用堆栈窗口,我们可以看到现在调用栈里只有一个main()函数。然后继续往下执行,选择“逐语句”:
我们可以看到,有一个fac已经被放进了调用栈里——继续向下走:
又有一个被放了进去——接下来该返回了:
一目了然。
这些就是调试器最为主要的三个功能——对于其他语言和其他环境(例如gdb),虽然具体细节有所不同,但是核心的思路都是一样的,万变不离其宗。
以上就是这篇文章的主要内容——很惭愧,做了一点微小的贡献,谢谢大家。