把复杂的任务拆分成简单的子模块
在C语言程序开发中,程序员常常会把一个复杂的任务拆分成若干个较为简单的子模块,这些子模块可以看做是复杂任务的各个组成部分。因此,程序员将子模块逐个完成后,就可以将其像“积木”一样搭建起来,进而解决复杂任务。
之所以要这么做,是因为若直接编写C语言代码一次性解决复杂任务,往往会让整个代码“揉作一团”,不仅开发时容易引入 bug,而且后期维护起来也比较痛苦。想象一下,若将任务拆分成若干个子模块,以后发现问题时,可能只需要修改某个子模块就可以了。但是若没有这么做,查找和解决问题就必须对全部代码下手了。哪一种开发风格维护起来方便,相信读者自然明白。
将任务拆分成子模块后,每个子模块常常被封装成一个C语言函数,所以,最后的“堆积木”其实就是调用各个C语言函数。不过,每一个子模块都有可能得到正常结果,也有可能得到异常结果,这通常用C语言函数的返回值区分。在“堆积木”阶段调用各个函数时,应该根据被调用函数的返回值做不同的处理。
例如,某个子模块负责计算用户输入数字的 log(对数) 值时,如果用户输入的是正数,则该子模块能够得到正常的结果。但如果用户输入的是负数,子模块显然就无法得到正常结果了。
下面是一个例子
这里的例子代码尽可能的简单,是为了将重心放在讨论主题上:
int cond()
{
static int cnt = 0;
srandom(time(NULL)+ cnt++);
if(random()%10 < 5)
return -1;
return 0;
}
int fun1()
{
/** 其他逻辑 */
int ret = cond();
if(ret == -1)
return -1;
/** 其他逻辑 */
return 0;
}
int fun2()
{
/** 其他逻辑 */
int ret = cond();
if(ret == -1)
return -1;
/** 其他逻辑 */
return 0;
}
int main()
{
if(!fun1() && !fun2())
printf("cond is true\n");
else
printf("cond is false\n");
return 0;
}
cond() 函数产出 0~10 的随机数,如果随机数小于 5 就返回 -1(模拟异常结果),否则返回 0(模拟正常结果)。fun1() 函数和 fun2() 函数都会根据 cond() 函数的返回值做一些进一步的工作(上面的C语言代码略过了“进一步工作”)。在 main() 函数中“堆积木”调用 fun1() 和 fun2() 函数时,使用了 if 语句判断它们的返回值,并且根据返回值做了不同的处理。
现在编译这段C语言程序并执行,得到如下结果:
# gcc t.c -g
# ./a.out
cond is true
# ./a.out
cond is false
从输出结果可以发现,C语言程序输出了“cond is false”(模拟异常)。我们往往不希望程序输出异常结果,所以看到异常结果后,就需要知道为什么会出现这个结果。
适当的输出信息有利于定位异常
查看C语言源代码,发现程序输出异常结果是因为 main() 函数里的 if(!fun1() && !fun2()) 为假,但是无论 fun1() 还是 fun2() 返回 -1,都会导致 if 条件表达式为假,这么看来, main() 函数调用 fun1() 和 fun2() 函数的方式就不太合适了,因为到这里我们已经无法继续追踪异常原因了。似乎 main() 函数这么写更合适,相关C语言代码如下,请看:
int main()
{
int ret1 = fun1();
int ret2 = fun2();
if(!ret1 && !ret2)
printf("cond is true\n");
else if(ret1 && ret2)
printf("cond is false because of fun1 and fun2\n");
else if(ret1)
printf("cond is false because of fun1\n");
else
printf("cond is false because of fun2\n");
return 0;
}
编译修改后的C语言代码并执行,得到如下结果:
# gcc t.c
# ./a.out
cond is false because of fun1 and fun2
# ./a.out
cond is false because of fun1
# ./a.out
cond is false because of fun2
# ./a.out
cond is false because of fun2
# ./a.out
cond is true
这次我们就知道异常输出是哪个函数导致的了,不过仅调用两个函数就写了这么多行极有可能用不到的错误提示代码,太麻烦了,如果其他地方也需要用到类似的调用,就更麻烦了,有没有更方便的方法呢?我们尝试将错误提示信息塞入 fun1() 和 fun2() 函数试试,如下修改 fun1() 和 fun2() 函数的代码:
int fun1()
{
/** 其他逻辑 */
int ret = cond();
if(ret == -1){
printf("fun1 get unexpected cond\n");
return -1;
}
/** 其他逻辑 */
return 0;
}
int fun2()
{
/** 其他逻辑 */
int ret = cond();
if(ret == -1){
printf("fun2 get unexpected cond\n");
return -1;
}
/** 其他逻辑 */
return 0;
}
现在使用修改之前的 main() 函数如下:
编译并执行这段C语言代码并执行,得到如下输出:
# gcc t.c
# ./a.out
fun2 get unexpected cond
cond is false
# ./a.out
fun1 get unexpected cond
cond is false
# ./a.out
cond is true
这样一来,我们既能根据输出推断异常是由哪个函数导致的,也能尽可能的保持C语言代码的简洁性。不过代码还是有一点点啰嗦:
fun1 get unexpected cond
fun2 get unexpected cond
这两句输出仅有 fun1 和 fun2 是不同的,但是我们却需要完整的写两遍几乎一样的语句,而且以后万一需要修改,还需要两处都修改,一来麻烦,二来容易出错。能不能避免这种情况呢?
使用__FUNCTION__
,__LINE__
,__FILE__
等关键字
在C语言程序的编译阶段,编译器会将__FUNCTION__
,__LINE__
,__FILE__
这几个关键字解释为“所在函数名”,“所在行号”,“所在文件名”。所以有了这几个关键字,我们就没有必要再手动输入函数名了,针对本节提到的例子,完全可以使用上一节介绍的 define 宏定义:
#define error_info() printf("%s get unexpected cond (%s line:%d)\n",\
__FUNCTION__, __FILE__, __LINE__)
...
if(ret == -1){
error_info();
return -1;
}
...
编译并执行这段C语言代码,得到如下结果:
# gcc t.c -g
# ./a.out
cond is true
# ./a.out
fun1 get unexpected cond (t.c line:26)
cond is false
# ./a.out
fun2 get unexpected cond (t.c line:41)
cond is false
可以看出,程序不仅把异常的函数名输出了,还把该函数所在的文件名(t.c) 以及行号(line:26, line:41)输出了,这样的调试信息看起来非常舒服,在大型项目开发中,实用性很强。
类似的调试宏还有TIME,DATE等,就不一一演示了。
小结
本节讨论了在C语言程序开发中,复杂任务常被拆分成多个子模块并一一封装为函数,这些函数可能有正常处理结果,也有可能有异常处理结果,所以本节讨论了输出基本调试信息对定位问题的重要性,并在最后介绍了几种C语言程序开发常用的调试宏,这些宏在大型项目开发中实用性很强。