C语言编程竞赛时代码出现漏洞排查教程
运行错误
顾名思义,就是代码运行时出现了错误,错误种类有很多,我们举出比较常见的一部分来说。
除零错误
除零错误必然发生在进行除法运算的地方,这时候我们需要考虑以下几点:
1.是否是程序编写时失误导致除数运算错误而出现了0
2.是否没有考虑某些特殊情况导致除数出现了0
越界访问
越界访问也有两种,即数组越界访问和野指针。
数组越界访问
这个很好理解,就是访问数组时下标超出了数组的范围。(注意:并非所有越界访问都会导致异常。)出现数组越界的大多数情况是遍历数组是循环变量的更新或范围有错误,或者是调用一些系统函数(比如memset)时传入的参数有误。
野指针
野指针就是指针指向的位置是不可知的
这里我们举出一个例子:
int* createPoint() {
int k = 0;
return &k;
}
int main() {
int* point = createPoint();
int* array = malloc(3 * sizeof(int));
free(array);
//do something...
return 0;
}
这里列出了两种比较常见的造成野指针出现的情况,第一种就是函数返回(或通过其它方式传出)了一个指向局部变量的指针,当代码执行到局部变量的作用域之外时变量就会被销毁(内存有可能不会立即销毁,但是该区域内存随时有可能被重新使用),此时这个指针便指向了一个无效的区域。第二种情况就是使用malloc分配了内存,后面又使用了free销毁内存,但是注意,free只会销毁内存而不会销毁指针,也就是说此时指针依旧指向被销毁的内存。
当使用野指针时,能不能计算出正确的结果以及能否不崩溃都是一个完全看脸的问题。
调用栈溢出
递归深度过大导致调用栈溢出
非零返回
按照标准规定,当程序正常结束时main函数应当返回 0,否则返回非0值。如果你的代码种主函数返回了非0值,不论运算结果是否正确,测试平台都会当作你的代码没有正确结束。出现这种情况时一部分平台会直接报“运行错误”,也有一部分平台会单独报出“非零返回”。
浮点错误
浮点错误大多也是浮点运算过程中出现了除零操作
内存超限
这种错误出现的比较少,因为竞赛题给的内存大多比较大,除非用了体积比较庞大的数据结构,不然很难出现这种错误。
时间超限
时间超限也有多种情况,我们列出一部分。
死循环
循环中忘记更新循环变量或更新了错误的值,导致循环永远不会结束,我们给出一个典型的“范例”:
int main() {
for (int i = 0; i != 10; -i) {
//do something...
}
printf("不可达的代码!");
return 0;
}
这段代码除非循环体中使用了break、goto一类的控制流语句,否则不可能跳出循环。
等待输入
使用输入流函数时代码中需要输入的数据比实际应该输入的数据多,导致程序卡在输入流函数中一直等待外部继续输入数据,实际上已经没有任何数据会输入了。常见的情况是题目要求以EOF结束输入但是代码却忘了判断。
int main() {
int n;
while (true) {
scanf("%d", &n);
//do something...
}
return 0;
}
计算时间过长
出现这种问题表明代码的算法不合理或不适用当前情况,运行时间超出题目要求时长。解决方法有这几种:
1.优化算法(适用于算法大体方向正确但有优化余地)
2.使用更优的算法
3.减少数据复制(复制数据不仅消耗内存空间也会占用时间)
4.用高效的函数替代循环(常见的是memset、memmove、memcpy等)
输出超限
程序执行结果过长,出现这种情况答案肯定是错了,仔细读一遍题,看看是不是什么数据不需要输出或者理解错了题意,没有的话就是代码写错了。
答案错误
答案错误是最常见也是最令人头疼的错误,因为大多数情况下,竞赛中提交的代码出现答案错误时,平台不会给出相关数据,只会报出“答案错误”四 个 大 字。
首先我们清除一个点,如果代码的整体思路是对的,除非是手误,基本上只会在特殊数据上出问题,就是某些特殊输入没有考虑到,自己手算以下结果然后和代码运行结果比较一下,检查是否正确是一种比较高效的方法。
如果完全没有头绪,甚至连代码有什么特殊情况会出问题都找不出来,可以采用控制流排除法。例如下面的代码:
int main() {
int n;
scanf("%d", &n);
//code1
if (...) {
//code2
} else if (...) {
if (...) {
//code3
} else {
//code4
}
} else {
//code5
}
return 0;
}
排查错误时我们可以从控制流语句出发,采用从内向外排查的方法。我们先找出深度最深的控制流语句,比如这里的code3和code4,我们算出会让代码进入到这两个语句块的输入,然后输入到程序中检查是否正确。如果发现答案错误,那么说明是到达这段代码的过程中或者是这段代码的计算有问题。如果没有错误就到其它的语句中检查,比如code2和code5,这样就缩小了范围。
在尽可能的缩小范围之后,如果还是看不出错误,可以使用printf一类的输出流函数输出程序的过程量,监视代码运算的中间值是否正确,如果出现错误,只要找到第一个开始错误的点就能更容易的找到问题的根源。
这篇博客提到的方法都是一些辅助排查的方法,使用上述方法也不一定能够找到错误点。想要真正快速地找到错误,首先是必须理解自己写了什么,如果连自己写的是什么都不知道想要排查错误简直是痴人说梦。