关于代码的健壮性,我只是略知皮毛,没有深刻的见解,这里不去讲解那些专业的东西,只是记录我编程的过程中遇到的一些问题及其体会而已。我在此写下这篇博文,原因除了很久没有更新博文之外,也只是希望能给那些刚刚接触编程语言,也许会和我一样忽略了重点的同学们一点小小的启发。
首先我得声明一点就是,我没有看过专门讲解代码健壮性之类的书籍(接下来的时间里我会抽时间仔细地去看),所以也许我所说的可能不是很对,当然也可能根本不属于健壮性这一范畴内,我只把它当作一个小小的开始。如果以后我在这点上有所启发,我会再次更新这篇博文的。
1.类型安全
关于类型安全,相信编写过代码的同学都深有体会,C语言提供了丰富的数据类型及自由的类型转换机制。如果编程的过程中,稍有不慎,用错类型,或者是转换错误,就可能导致程序崩掉或是内存泄漏一系列的问题。这里不想用大篇幅的话语去详细讨论这一话题,只想通过一个小小的例子来说明这一点。所用到的问题代码如下
#define loop_t unsigned int
#define len_t unsigned int
#define bool_t int
#define _true 1
#define _false 0
#define record_t int
typedef struct
{
record_t *record;
len_t length;
}seqlist_t;
bool_t sort_seqlist(seqlist_t *seq)
{
// _ENTER_FUNCTION_
loop_t i, j;
record_t temp;
assert((seq != NULL) && (seq->record != NULL));
if (seq->length <= 1) {
return _true;
}
for (i = 1; i < seq->length; i++) {
j = i - 1;
temp = seq->record[i];
while ((j >= 0) && (temp.key < seq->record[j].key)) {
seq->record[j+1] = seq->record[j];
j--;
}
seq->record[j+1] = temp;
}
return _true;
// _LEAVE_FUNCTION_
}
不知道大家是否看出上面这段代码的问题所在,其实问题就出在数据类型操作失误。
在排序代码中,我用到了语句while((j >= 0).....) j--; 其中j的类型为loop_t, 而loop_t的原型为unsigned int,大家都知道,无符号整型是不可能有负值的,所以当j==0时再对它减1,就变成了unsignde int的最大值,而不是想象中的-1,从而导致了接下来对数组越界操作的问题。解决办法之一就是把上面的unsigned int 换成 int。
上面出现的问题直接导致了程序的崩溃,而有的时候,虽然问题依旧存在,却不会导致明显的错误。所以,在编写代码的过程中,我们一定要注意类型是否匹配以及操作是否正确。
2.内存泄漏的检测
对于一些小程序、运行时间较短的程序来说,内存泄漏之后貌似看不出什么问题,但是对于一些较大的程序,或者需要长期稳定运行的服务程序,一次次的内存泄漏,将会吃掉大部分可用的内存空间,从而导致别的程序无法开启,或者更严重的问题。所以养成良好的编程习惯是非常重要的,对于动态开辟的空间,用完一定要记得释放掉。想是这么想的,如果编写的代码很长的话,动态开辟足够频繁的话,忽略掉那么一两处也是很正常不过的。这里介绍两款很好用的小工具来帮助我们检测我们所编写的程序是否存在内存泄漏的问题(关于工具网上教程多的是,这里不多做介绍)。
1.valgrind
这款工具还是比较好用的,我们只需要正常编写我们的程序,然后用gcc-g *.c编译,然后把执行命令传给它就行,比如valgrind./a.out。它会指出程序中动态开辟空间的数量,以及没有释放掉的空间数量。
2.mtrace
应用这款小工具的时候,需要在代码中包含<mcheck.h>,还要设置环境变量:setenv(“MALLOC_TRACE”,“output_file_name”, 1);然后调用mtrace();函数。编译执行我们程序之后,执行mtraceoutput_file_name即可看到内存泄漏的情况
memwatch是一组c语言代码,包含两个文件:memwatch.c和memwatch.h。只需把这两个文件拷贝到我们的工程目录下,在我们编写的每个代码文件中加入#include“memwatch.h”,在编译的时候定义宏MEMWATCH和MW_STDIO,并且和memwatch.c一起编译就可以了。当我们执行编译后的程序时,就会在所在目录下生成memwatch.log日志文件,里面就是程序的内存使用情况的描述。
我自己还是比较喜欢memwatch的,当需要检测的时候,按照以上方法编译,不需要的时候正常编译即可;调试模式下它会帮助我们释放掉没有释放的空间,测试如下代码(a.c):
#include<stdio.h>
#include<stdlib.h>
#include"memwatch.h"
intmain(int argc, char* argv[])
{
char*p2;
p2= (char*) malloc(sizeof(char) * 128);
return0;
}
执行编译命令:gcc-DMEMWATCH -DMEMWATCH_STDIO a.c memwatch.c执行测试程序:./a.out
查看日志文件:vim memwatch.log
输出结果如下:
=============MEMWATCH 2.71 Copyright (C) 1992-1999 Johan Lindh =============
Startedat Tue Nov 26 10:23:48 2013
Modes:__STDC__ 64-bit mwDWORD==(unsigned int)
mwROUNDALLOC==8sizeof(mwData)==56 mwDataSize==56
Stoppedat Tue Nov 26 10:23:48 2013
unfreed:<1> a.c(9), 128 bytes at 0x1fa8310 {FE FE FE FE FE FE FE FEFE FE FE FE FE FE FE FE ................}
Memoryusage statistics (global):
N)umberof allocations made: 1
L)argestmemory usage : 128
T)otalof all alloc() calls: 128
U)nfreedbytes totals : 128
MEMWATCHdetected 1 anomalies
从上面的输出可以看出,在a.c中第9行动态申请的128bytes没有释放掉,
如果正常编译的话gcca.c调用valgrind./a.out 会显示
totalheap usage: 1 allocs, 0 frees, 128 bytes allocated
…
如果用gcc-DMEMWATCH a.c memwatch.c编译的话,再调用valgrind./a.out就会显示:
All heap blocks were freed – no leaks are possible
证明memwatch帮助我们把没有释放掉的空间释放掉了。
虽然有了工具帮助我们,但是良好的编程习惯是长期养成的,平时编写代码的时候就应该多加注意,一条malloc就该对应一条free,而不是等待问题出现以后再去检测。
3.异常处理
当然,要写出健壮的C代码,完善的异常处理机制也是必要的。C语言中,有很多异常处理的方法,常用的有assert、errorno、exit等。通常以上方法不能满足需求,我们需要自定义异常处理方法。下面自定义一种异常处理机制,以方便我们的编程。
首先,我自建assertex.h和assert.c,用来保存自定义异常处理接口,内容分别如下:
/*******************************************************
* File Name: assertex.h
* Discription: Custom exception interfaces
* Create Date: 11-26-2013 13:09:39
* Version: 1.0
*
* Author: Jensyn
* Email: zhangyongjun369@gmail.com
* BLOG: http://blog.csdn.net/zhangyongjun_2012
*******************************************************/
#ifndef _ASSERTEX_H_
#define _ASSERTEX_H_
void __assertex__(const char *file, unsigned int line, const char *function, const char *expr);
#if defined (NDEBUG)
#define assertex(expr) ((void)0)
//#elif __LINUX__
// #define assertex(expr) {if(!(expr)){__assertex__(__FILE__,__LINE__,__PRETTY_FUNCTION__,#expr);}}
//#elif __WINDOWS__
// #define assertex(expr) ((void)0)
#else
#define assertex(expr) {if(!(expr)){__assertex__(__FILE__,__LINE__,__PRETTY_FUNCTION__,#expr);}}
#endif
#endif
/*******************************************************
* File Name: assertex.c
* Discription:
* Create Date: 11-26-2013 13:23:32
* Version: 1.0
*
* Author: Jensyn
* EMAIL: zhangyongjun369@gmail.com
* BLOG: http://blog.csdn.net/zhangyongjun_2012
*******************************************************/
#include <stdio.h>
#include <stdlib.h>
#include "assertex.h"
void __assertex__(const char *file, unsigned int line, const char *function, const char *expr)
{
char assert_info[1024] = {0};
sprintf(assert_info, "At file [%s] line [%d], In function [%s]: Assertion '%s' failed\n", file, line, function, expr);
printf("%s", assert_info);
// exit(0);
}
这样,我们调试的时候,只需要在程序的源文件中包含上面的头文件,然后把上面的源文件和我们的主程序一块编译就可以实现自定义的异常处理了。当然也可以把以上源文件编译成链接库,再与主程序一块连接;如果不需要进行错误处理,只需要在编译时定义宏NDEBUG即可
上面实现的代码也许在实际中起不到什么作用,实际中需要根据自己的需求去增删一些功能。