其实我一直想写一篇文章关于调试的(起码在做大一实训TA的时候就有这想法),但是一直未写,原因有二:第一,我不懂怎么组织语言,第二,我懂的调试方法不多,怕写出来被人鄙视。但是经过这个星期的停车场事件,这根导火线啊,让我郁闷不已,很多问题都是调试可以解决了,起码到目前为止我遇到的都是,所以最终决定写了,虽然我不懂组织语言,但我只需要把最基础的、把思想表达就好,虽然我懂的调试方法不多,但足以应对现阶段师妹师弟们遇到的困难。
首先当然要吐槽一下,以后师弟师妹们别一来就弹一句:“豆腐能不能帮我调试一下?”之类的话,并不是所有的师兄都像我这么好人的,再且,你们问人的技巧也应该提高一点,不要一来就把整个project发给我,然后说里面有错误,帮我找找,或者说哪个函数有问题,我更乐意的是你们能写一个测试样例,这个测试样例会产生你刚才说的错误,起码这能在一定程度上减少我的负担,有时候当你写了测试样例,我可以在一分钟内找出错误,这不是夸大。举个例子,有个师妹曾这样问我:
首先她直接发了两张图说:
然后就问我这是什么问题,稍微写过c++的都知道导致这类问题的原因很多,你什么都不发给我就让我诊断出什么问题,当我是神了。
正题了,在这里我介绍三种调试方法,其实思想都差不多的(太久没用C++,写得不好见谅,毕竟用惯java,遇到错误会直接提示哪一行发生错误)。
方法一:单步调试(要Debug而不是release,方法:点击菜单栏生成->配置管理器,然后在你的项目后把配置选择为Debug)。先讲讲一些快捷键,
F5:运行到断点或运行到要用户输入的地方就会停止下来。
F10:就是单步调试,按照当前的程序一行一行执行,把代码中每一行都看成简单语句(遇到函数不会跳进去,直接一步运行到位,该函数里面有断点)
F11:功能跟F10差不多,只是在遇到函数的时候会进入该函数进行调试。
Shift + F5:结束本次调试。
Ctrl + F10:直接运行到光标所在的行,除非过程中遇到断点。
Shift + F11:跳出当前函数(一步把当前调试行所在的函数运行完),除非过程中遇到断点。
断点的设置,把光标定位于要设置断点的行,按一下F9,如果不对断点进行任何的修改,则默认遇到断点就会停止执行,但是断点是可以设置条件的,
就是当满足你设置的条件的时候程序才会停止执行。
此外在调试的过程中可以通过监视窗来看你想知道的变量的值
当你设置了你要监视的变量后,假如当这个变量的值发生改变,则在监视窗口中变量值的颜色会变成红色以突出(比如上面的变量c)。
方法二:利用断言。这需要用到头文件assert.h里面的assert(expr)函数,这个函数会判断表达式expr是否为真,如果expr为假,则程序会终止并输入他运行到哪个文件哪一行。
#include <assert.h>
int main() {
int a = 10;
assert(a ==8);
return 0;
}
比如这个程序,很明显表达式a == 8为false,则控制台上会有这样的输出信息:
Assertion failed: a == 8, filee:\workspace\c++\test\test\test.cpp, line 5
然后程序会终止执行。
方法三,利用输出信息。就比如在关键的地方打印一下信息。比如:
#include <iostream>
int main() {
int a = 10;
std::cout<< a << std::endl;
return 0;
}
我想知道程序运行过程中a的值是多少可以加上这条输出信息,从而判断是否正确。又比如:
#include <iostream>
void fun() {
std::cout<< "fun()" <<std::endl;
}
int main() {
fun();
return 0;
}
想知道fun()函数有没有被调用,则可以通过这样输出信息的方法来判别。
看完上面的基础介绍,也许你会说,这些我都懂啊,可是就是调试半天也找不出错误,别急,慢慢来。假如是写C++控制台的程序,我一般都是方法一加方法三,先通过方法三来快速定位大概的错误位置,然后再用方法一来精确定位错误的位置,但是现在都只用方法三来调试了。举个例子:
比如以下程序:
#include <iostream>
#include <stack>
using namespace std;
stack<int> s;
void fun(){
cout <<"***for test: " << "fun()" << " ***" << endl;
s.pop();
}
void fun1(){
cout <<"***for test: " << "fun1()" << " ***" << endl;
cout <<s.top();
}
int main() {
int a = 1;
int b = 2;
a = b + 1;
s.push(a);
fun();
a++;
fun1();
cout <<a;
cout <<endl;
}
运行过程中发现出现以下错误:
也许你就会很惊讶,不知道哪里有问题,实际上很多人问我都是出现这类问题,这时候你可以注意观察控制台的输出信息,可以看到有输出信息:
***for test: fun() ***
***for test: fun1() ***
这可以说明在调用fun1()函数之前都没问题(至少没有非逻辑错误),于是可以进一步定位这错误是在调用fun1()函数后发生的,于是可以从fun1()开始进行调试,通过这样一次一次逼近,错误的范围就可以收敛了。
也许你定位到是s.top()这句发生问题,对于学过C++的人来说都知道发生这样的错误只能是栈为空,这时候你就在需要查找一下对s在哪里进行了操作(怎么查找很简单,就是通过ctrl+F来查找),如果存在多处操作的话,你可以对每个操作设置一个断点(假如你很清楚你程序的执行过程可以省略这步),然后通过不断使用F5来看对s的操作顺序,这样就可以很快查出错误。比如这个例子,你就可以定位到是由于前面有一个出栈的操作导致栈变为空。其他的情况就类似了。
假如是存在逻辑错误,那可能会比较麻烦,因为程序不会出现运行错误,很难定位在哪里会出现问题,对于这种,我建议是先在一些关键的位置是使用断言或者打印信息。
比如这个例子:
#include <iostream>
using namespace std;
int main() {
int a, b = 1;
a = b++;
cout <<a << endl;
return 0;
}
你期望输出a的值是2,但实际上是1,这样你就可以知道在打印信息之前发生了逻辑错误,然后再继续往上看,也是通过这样的方法来定位错误的。
更一般的,你就单步调试,通过不断监视变量值的变化来找错(通过监视窗口)。
Ps:一般是你自己写的程序,你都应该能够大概知道是在哪里发生错误,这样就能从一开始把范围缩到很小。就正如我刚开始说的那样,我更喜欢你们写一个测试样例会产生你说的错误,因为这样能帮我很快定位哪里有问题,继续往上推敲。
我本人是很喜欢程序出错有异常的,我debug速度如此快就是因为我对上面讲得方法很熟悉,运用得很熟练(特别是方法三,因为我现在写程序能用的工具就只有方法三),所以说下次遇到问题可以先别找人,自己尝试debug。