C99出来很久了,好像现在还在谈论一个10多年前的标准显得有点过时。不过现实是:关于C99新增的特性,许多用C用了N年的人其实还都不清楚。而在一些能找到的相关文章里面,基本上都是一些对标准的简单翻译,看那种玩意还不如直接去翻标准。所以我主要从自己的使用体验的角度来分享一下我用C99的一些心得。
这篇文章并非要做个大而全的C99相对C89的扩展列表,我只挑我自己觉得可能用上或者有必要说的来讨论。另外这个简单的讨论不能免除你读标准的苦差——对于大多数特性我都只是点到为止不会面面俱到,要了解其完整的用法。。。要么就等我哪天突然闲得蛋疼像之前写过的那篇《volatile的陷阱》那样再来几个专题节目,要么就只能劳您自己去翻ISO C99标准了~~
另外,用gcc并且关注过gcc扩展的人肯定会发现下面介绍的很多C99特性在gcc里早就有了。不过现在它已经成为了标准,你可以放心地在任何一个宣称支持C99的编译器里面用它。这些特性的排列没啥规律,基本上我是想到啥就写啥。不过先被我想到的,理论上就是我觉得相对比较好用的吧。
指定初始化(Designated Initializers)
简单来说,就是在初始化结构体和数组时,可以通过指定具体成员名或数组下标来赋初值。如:
static struct usb_driver usb_storage_driver = {
.owner = THIS_MODULE,
.name = "usb-storage",
.probe = storage_probe,
.disconnect = storage_disconnect,
.id_table = storage_usb_ids,
};
很明显,使用指定初始化的好处很多。首先这让对结构体的部份初始化成为了可能,再也不用仅仅为了初始化其中一两个成员而费劲地将所有不关心的成员都填上0;其次让初始化代码变得可读,一眼就可以看出成员名和初值之间的对应关系;再则可以让初始化不受结构体定义的影响。因为在初始化中指定了成员名,所以结构体定义中成员顺序的调整或增删都不会影响初始化代码。
另外在数组初始化时指定初始化也非常好用,如:
static bool mac_splitter[UCHAR_MAX] = {
[':'] = true, ['-'] = true,
['.'] = true, [' '] = true
};
我在编码时喜欢大量使用表驱动法(table-driven),所以初始化各种查找表、状态表、分发表是家常便饭。利用指定初始化可以让这些数据表的初始化变得方便,易读。如这样定义一张函数分发表:
typedef enum {
LogTypeUnknown = 0,
LogTypeSecurity,
LogTypeUrl,
LogTypeSession,
LogTypeIpfix,
LogTypeMax
} log_type_t;
static evt_export_func evt_handler_tbl[LogTypeMax] = {
[LogTypeUnknown] = event_unknown_export,
[LogTypeSecurity] = event_security_export,
[LogTypeUrl] = event_url_export,
[LogTypeSession] = event_sess_export,
[LogTypeIpfix] = event_ipfix_export
};
可读性和可维护性显然要比这种写法好得多:
static evt_export_func evt_handler_tbl[LogTypeMax] = {
event_unknown_export, event_security_export,
event_url_export, event_sess_export, event_ipfix_export
};
其实个人觉得指定初始化带来的最大好处还是让初始化代码的可读性极大地提高了——这从上面的例子中应该也能很直观地看出来。所以无论你想初始化的是结构体的部份还是全部,我都强烈建议你用指定初始化来写这部分代码。
变长数组(Variable Length Arrays)
VLA变长数组是个方便但不太常用的特性。C99允许可以定义一个长度为变量的数组(当然,这不是说你随时可以去通过改变这个变量的值来动态修改数组的长度——其实仅仅是这个数组的长度可以到运行时才决定而已):
void func(int n)
{
int vla[n];
printf("%d\n", sizeof(vla));
}
在我的实际开发过程中,大概也就遇上过2~3次需要用VLA的情况。基本上都是在函数内需要开辟一个与传入的数据(数组或字符串)长度相符的临时缓冲区做某些处理(如快速排序或是做一些字符串转换)时才有这种需要。
比如一个根据数据表名称打开对应的磁盘文件的函数:
FILE *open_output_file(const char *name)
{
#define EXTRA_LEN 11
char fname[strlen(name) + EXTRA_LEN + 1];
snprintf(fname, sizeof(fname), "/tmp/.%s.temp", name);
return (fopen(fname, "a"));
}
在这个函数里面,我们需要一个临时的字符串缓冲区来存放补充了路径和后缀名之后的文件名。在没有VLA的时候,遇到这种情况通常是按照最大长度定义一个定长数组(比如char fname[256]);现在用VLA就轻松搞定了。其实这个例子里面用VLA的主要目的并非是为了节省那点栈空间,关键是可以在很多情况下简化异常处理:用定长缓冲区往往很难妥善处理字符串长度超出预期的尴尬问题。
总的说来,VLA是个很少用,但是能用上时却很好用的功能。而且VLA可以替代非标准的栈分配alloca()——不过讽刺的是,在VC里面你可能反而不得不用alloca()来模拟VLA。。。
伸缩型数组成员(Flexible Array Members)
实际上这就是原先gcc所扩展的0长度数组,在做变长的报文或字符串处理时极为好用,也是一个几乎所有的C码农都应该掌握的技巧。
比如你要做个单词统计程序,显然每个单词的长度都是不同的——这时可以利用伸缩型数组成员来定义一个变长的单词计数器节点:
typedef struct {
int count;
char word[]; // 只能放在末尾,等价于gcc的char word[0]
} word_counter_t;
然后这样为这个节点分配空间:
malloc(sizeof(word_counter_t) + strlen(word_str) + 1);
这样就得到了一个末尾包含了一个char word[strlen(word_str)+1]数组的节点了。可以看到这么写比按照最大长度预留的定长分配要优雅得多,并且也节省内存空间(因为通常节点或者报文都是需要大批量动态分配的数据结构)。另外顺便带来的好处也是错误处理更容易:至少不用考虑单词缓冲区不够长的问题。
bool类型
没啥好说的。反正都C99了你还在自己#define TRUE 1,#define FALSE 0或者enum boolean就老土了~~用标准的bool类型吧(<stdbool.h>)。
另外,应该把禁止自定义形形色色的山寨BOOL/BOOLEAN类型写到你小组的编码规范里面,必须强制大家统一使用标准的bool类型。在C没有将bool标准化之前,代码里见到的最多最乱的就是这些形形色色的个人自定义~~
long long类型
想必不少人会大吃一惊:原来已经用了N久的long long直到C99才成为标准( ̄口 ̄)!!
long long类型可以确保其长度至少为64bit。对应的常量后缀是ll/ull/LL/ULL;格式化输入输出为%lld,%llu,%llx……
inline函数
这里就不多说了,之前写过一整篇关于这个的文章:《C语言的inline》。如果你纠结于gcc和c99的inline不知所措的话,记住始终只写static inline就对了。
在实际使用中inline最大的意义在于static inline能代替绝大部分的函数宏。在用宏封装函数的时候,比较讨厌的是宏本身只是一个简单的字符串展开工具,所以不具备像函数那样完备的参数检查能力。而目前的代码编辑器对宏的支持也不好,参数提示什么的做得都很不足。所以即使是你写的是个所谓“函数宏”,但是在实际调用起来的时候感觉都不如真正的函数来得舒服。
当你在调别人提供的接口时,最讨厌的应该就是遇到这种函数宏了。因为函数宏你基本看不出参数类型,看不出有无返回值,这逼得你非得看代码,查文档,或者大动干戈地跳过去一层一层看到调用的原始函数为止。而且在调用宏的时候还必须小心避免宏展开时的陷阱:比如一个宏参数有可能被展开两次(于是你要是没注意传入一个i++就杯具了)。。。所以我一看到要调宏函数,首先就是觉得嗓子发干有点紧张~~
因此C99也是鼓励用static inline代替宏函数的。所有能通过static inline函数解决的问题,都应该避免写成宏(这点也应该写到编码规范里头)。