2010年 01月 04日 的归档
用“函数属性”来避免C中格式化字符串时可能存在的错误
为了说明这个问题,先来看下这个简单的C程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include <stdio.h> #include <stdarg.h> void writeLogInfo(const char *sFormat, ...){ char sOutBuffer[4196]; va_list lvalist; va_start(lvalist, sFormat); vsnprintf(sOutBuffer, sizeof(sOutBuffer)-2, sFormat, lvalist); va_end(lvalist); printf("log: %s\n", sOutBuffer); } int main(){ writeLogInfo("int=%s", 123); return 0; } |
这程序用gcc编译,即使是用 -Wall 打开所有的警告,也是不会有任何报错的。
但是执行结果是什么呢?由于 writeLogInfo 的是一个参数里指定的是 %s ,而第二个参数确是整型数字 123。所以程序义无反顾地出现了“段错误”而崩溃掉。这种问题在项目代码超过万行以后,要debug起来,也是会浪费很多时间的。
有的人会发现,如果把main里的writeLogInfo直接换成printf,那么在编译的时候,gcc会报一个警告:“警告:格式‘%s’需要类型‘char *’,但实参 2 的类型为‘int’”(Gcc4.x默认就会报,Gcc3.x要加 -Wall 选项才报),如果我们自己的定义的writeLogInfo函数也能有这个警告,那么这种bug将在编译的时候就可以完美解决了。
那么具体怎么实现呢?先来看下面这段代码,功能是和上面的完全一样的,连错误都一样,呵呵:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #include <stdio.h> #include <stdarg.h> #ifndef __GNUC__ # define __attribute__(x) /*NOTHING*/ #endif void writeLogInfo(const char *sFormat, ...){ char sOutBuffer[4196]; va_list lvalist; va_start(lvalist, sFormat); vsnprintf(sOutBuffer, sizeof(sOutBuffer)-2, sFormat, lvalist); va_end(lvalist); printf("log: %s\n", sOutBuffer); } void writeLogInfo(const char *sFormat, ...) __attribute__((format(printf,1,2))); int main(){ writeLogInfo("int=%s", 123); return 0; } |
当你尝试用gcc编译这个文件的时候,你就可以看到警告了,哈哈。
可以看到,这里的关键就是“__attribute__((format(printf,1,2)))” ,这句话的作用就是告诉编译器,前面这个函数呢,参数类型是类似printf的,格式化字符串在参数的第1个位置,扩展参数从第2个位置开始,然后编译器就明白了~
然后,上面的4~6行呢,是为了兼容非Gcc的编译器而加的,这样其他的编译器就会直接无视整个 __attribute__ 了,这样至少不会报错。
其实,这个检查格式化字串的功能(format),只是“函数属性”的一个而已,另外还有许多有用又有意思的属性,比如函数的别名啊(alias),是否已经过时啊(deprecated),等等~要了解这些用法的话,建议去看看官方文档。