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函数解决的问题,都应该避免写成宏(这点也应该写到编码规范里头)。
混合声明(mix declarations and code)
其实也就是解除了原先必须在block的第一条语句之前声明变量的限制:现在C99也和C++一样,可以在代码中随时声明变量了。
不过我看很多人尤其是初学者,可能压根不晓得原来C99之前是不能在代码里混着声明变量啊。。。因为大部分C编译器其实都扩展支持了这个特性(比较严格的也最多打个warning罢了),而N多菜鸟用C垒代码,都是觉得缺了个变量就顺手定义一下然后就继续往下垒了,能编译过就算胜利~~不过现在倒是好了。。如果有老师傅鄙视你随地声明变量,你可以理直气壮地告诉他:这是C99标准支持的!
其实混合声明对于编码的重要意义在于它对提高代码的可读性方面帮助很大。在C89里面,稍微复杂一点的函数中通常会看到这样的壮观景象:
int load_elf_file(const char *filename, int argc, int argv, int *retcode)
{
int elf_exec_fileno;
int i, retval;
uint k, elf_bss, elf_brk, elf_entry;
uint start_code, end_code, end_data;
struct elf_phdr *elf_ppnt, *elf_phdata;
struct elfhdr elf_ex;
struct file *file;
...;
}
代码还没开始就先被一堆变量砸晕,这个就是C89必须在Block第一条语句之前声明变量带来的后果~~不管一个变量是否一直到Block的最后一行才被用到,你也不得不在一开始就声明它。而根据某研究(记的是在一本关于产生式系统的书里看来的),人的脑袋瓜子里面同一时刻只能暂存5~9个临时记忆块。而变量就是你在看代码的时候不得不在它的整个生命周期中都必须费劲跟踪的东西。所以同一时刻要关注的变量越多,你的脑子就越忙不过来,就不得不经常在代码里头跳来跳去。
而在C99支持混合声明以后,你就可以游刃有余地在编码时淋漓尽致地运用变量引用局部化原则(详见神书CC2e)化解这些可读性问题。不然如果非得把变量都堆前头声明的话,什么缩短变量的跨度、减小变量攻击窗口、最小化变量的生存时间之类的技巧都很难充分实施。而另外CC2e中提到的利用中间变量来提高代码可读性,实现代码自注释的技巧在C99里面实施起来也会容易很多——因为变量定义起来方便多了,而且即使定义了太多变量也不会把每个Block开头的变量列表给弄得太夸张。所以从这个角度来看,这也是一个强烈推荐使用,应该写入编码规范的特性。
// 行注释
//行注释也是从C++过来的东西。实际上在C99出来之前用//写注释和随处定义变量基本上是C菜鸟的标准特征了……至少会被人鄙视为“不专业”。所以老手们要开始在代码里面接受这两个东西弄不好反倒会有些心理障碍~~比如我至今还不太习惯在C代码里面敲//~~
不过//做行注释也有不少好处。一个自然是输入方便——但是也和你用的工具有关了~~如果你的代码编辑器足够高级(如SlickEdit),用/* */写注释也不会有啥不爽的~~SE的注释自动补全和格式化能力强到变态。即使是你用/* */写了一大堆洋洋洒洒的、左边带有漂亮的前导“ * ”边界的文档化注释……也只有开头的“/*”这两个字符是要你自个敲的~~不过用SourceInsight这种弱智玩意的人就比较悲催了,想“//”想得最凶的一定是他们(# ̄▽ ̄#)。
//注释另外一个最大的好处还是在于排版方便:在为常量或变量写Trailing Comments的时候,用//写注释你只要维护左侧的对齐就能有很好的观感。而用/* */,则不得不首位兼顾。作为一个完美主义者,小心地维护一大堆/* */首尾的精美对齐是一件挺烦人的事情。比如这样代码,我就是忍受不了的:
#define PID_OFF 0x10 /* TCB->pid */
#define USER_CONTEXT_OFF 0x14 /* TCB->user_context */
#define MM_OFF 0x50 /* TCB->mm */
#define UPFLAGS_OFF 0x54 /* TCB->usr_proc_flags */
#define SIGNAL_OFF 0x58 /* TCB->signal */
#define EX_FIXUP_OFF 0x5C /* TCB->ex_fixup */
非得把它搞成这样才爽:
#define PID_OFF 0x10 /* TCB->pid */
#define USER_CONTEXT_OFF 0x14 /* TCB->user_context */
#define MM_OFF 0x50 /* TCB->mm */
#define UPFLAGS_OFF 0x54 /* TCB->usr_proc_flags */
#define SIGNAL_OFF 0x58 /* TCB->signal */
#define EX_FIXUP_OFF 0x5C /* TCB->ex_fixup */
即使有SlickEdit强大的Block功能的帮助,维护这么个两头对齐的排版还是很麻烦的。最烦的就是要是哪天又加了点注释结果长度捅出去了,有排版洁癖的我又会忍不住把所有注释的*/都往后调整以便对齐。。。而用了//以后,只管一边自然容易许多,而且看上去也还整齐:
#define PID_OFF 0x10 // TCB->pid
#define USER_CONTEXT_OFF 0x14 // TCB->user_context
#define MM_OFF 0x50 // TCB->mm
#define UPFLAGS_OFF 0x54 // TCB->usr_proc_flags
#define SIGNAL_OFF 0x58 // TCB->signal
#define EX_FIXUP_OFF 0x5C // TCB->ex_fixup
所以对于这类Trailing Comments来说,用//写比较合适。
//还有一个好处就是没有/* */的嵌套问题。/* */注释是不支持嵌套的,所以只要里头出现一个*/就会导致注释结束。不过这样也带来了一个有点可怕的陷阱,比如原来用/* */注释的宏:
#define macro(arg1, arg2) \
func(arg1, /* xxxxxxx */ \
arg2 /* xxxxxxx */ \
)
你要是改成这样:
#define macro(arg1, arg2) \
func(arg1, // xxxxxxx \
arg2 // xxxxxxx \
)
这就杯具了~~因为这样等价于:
#define macro(arg1, arg2) func(arg1, // xxxxxxx arg2 // xxxxxxx )
arg2就落入第一个//的魔掌,变成注释了~~
总得来说,用//写注释肯定是比较方便的~~只要能保证别在一个文件里面一会儿用/* */做行注释,一会儿用//做行注释就行了。至于具体的用法,个人建议所有的单行注释都可以用//搞。而文档化注释(函数头部、文件头部或者重要数据结构头部的那些注释),还是建议用正统的格式来写:
/**
* xxxxxxx
*/
另外最好别用//来垒多行注释。
for循环变量初始化(for loop intializers)
一个只在for循环内部使用的变量,却非得在循环体外面定义,并且在循环体外也可见——这个事情太没道理。于是C99终于也忍不住引入了C++中广为人知的for循环变量初始化方式:
for (int i = 0; i < n; i++) {
...;
}
除了写起来方便以外,循环变量的生存周期也被最小化了。这也顺便杜绝了那种把循环变量带到循环外头继续用的恶习(有人称之为“技巧”)。C99是鼓励在for循环中使用这种写法的,所以赶快把这条也写到你的编码规范里去吧。
变参宏(Variadic Macros)
变参宏的最大好处是可以让你很容易地用宏来封装一些带变参的函数(主要是打印函数),如可以这样写一个输出到stderr的宏:
#define print_err(...) fprintf(stderr, __VA_ARGS__)
宏参数里面“...”的部份会展开到__VA_ARGS__处。如果在__VA_ARGS__前面加上##,就可以写出允许变参部份为空的变参宏。比如我自己常用的调试信息打印宏:
#define debug(fmt, ...) \
printf("[DEBUG] %s:%d <%s>: " fmt, \
__FILE__, __LINE__, __func__, ##__VA_ARGS__)
##__VA_ARGS__表示变参“...”部份允许为空。当变参部份为空时__VA_ARGS__会展开成空字符串,并且##前面那个逗号也会在展开时去掉。于是你可以这样调用这个宏:debug("Hello");
__func__常量
提到了我的debug()宏就顺便说一下:__func__也是C99标准了~~功能是会被展开为当前函数的名字,用法见上。
复合常量(Compound Literals)
简单来说复合常量就是允许你定义一个匿名的结构体或数组变量。如:
const int *p_array = (const int []) {1, 2};
这样就得到了一个已经被初始化为指向一个整型数组的指针了。
复合常量比较有用的地方在于其可以方便传参。如之前我们通常这样写带超时的select:
struct timeval wait = {.tv_sec = timeout, .tv_usec = 0};
select(sd+1, &sready, NULL, NULL, &wait);
有了复合常量以后可以这么写了(你可以省得专为传参而另外定义一个结构体变量):
select(sd+1, &sready, NULL, NULL,
&((struct timeval) {.tv_sec = timeout, .tv_usec = 0}));
同样,参数是个数组的函数也可以这么搞:
func((int []) {1, 2, 3, 4});
或者,可以利用复合常量给一个结构体变量整体赋值:
point = (point_t) {.x = 1, .y = 2};
这个写法等价于:
point_t tmp = {.x = 1, .y = 2};
point = tmp;
复合常量乍看起来不错,其实仔细想了以后觉得它实在是个可有可无的东西——没有很给力的实用价值。从效率上看用复合常量不像能有什么提升,而从提高可读性的角度看其效果也很有限:比如前面那个select的例子用复合常量改写后可读性实际上并没有提升,反而有些下降——因为这个结构体并非没意义到实在连个名字都不知起啥好的地步。所以除非是这个结构体实在是像路人甲一样没有任何起名价值的时候(比如我上面举的第二个例子),你才可以考虑用复合常量来写它。
btw:其实举的第二个例子也很牵强:我想大家应该还是甘愿用代码来分别为各成员赋值吧。毕竟这是一种习惯的写法,而且在大部分情况下可以只对少数必要的成员赋值(直接对结构体变量做整体赋值编译器实际上是编译成两个结构体之间做memcpy()的)。所以关于复合常量我真是没想出什么特别合适的例子来,而且在我的编码实践中也从来没遇到过什么非用不可的时候。建议各位也不要只是为了用复合常量而用复合常量——否则看来实在是有点卖弄技巧之嫌。但如果你确实找到了不用复合常量就很难把代码写得优雅的例子,麻烦给post在评论里。
复数(Complex Numbers)
C99开始支持complex float/double/long double类型了,这样做信号分析或时域频域变换之类的处理会方便许多。
可以这样在代码里面以a+bi的形式表示一个复数:
#include <complex.h>
double complex cmpl = 1.0 + 2.3 * I; // 1+2.3i
printf("%f + %fi", creal(cmpl), cimag(cmpl));
当然,复数变量之间的加减乘除自然也是照着复数的运算法则来的了。另外<complex.h>里面还包含了不少支持复数的数学函数(c打头的就是),比如三角函数、开方之类的。
restrict指针
restrict是对编译器的一个hinting:该指针是访问其所指数据对象的唯一且初始的方式——所以编译器优化起与restrict指针相关的代码来说就更加容易了。在使用效果上restrict和volatile相反:restrict指针的访存操作会被编译器尽量放心大胆地优化。
干说这些还是很模糊,所以我设计了一个实验能让各位直观地看到restrict修饰符对编译器的优化行为带来的影响。这个实验我是在cygwin+gcc 4的环境下做的,优化级别为-O2。
首先看没有加restrict的情况:
func.c:
void func(int *a, int *b)
{
*a += *b;
*a = *b;
}
main.c:
int main(int argc, char *argv[])
{
int n = 1;
func(&n, &n);
printf("n = %d\n", n);
return (0);
}
这个程序执行的结果是:n = 2,与我们的预期一致:因为传给func()的a和b都指向相同的整型变量n,所以在执行*a += *b的时候,实际上就把n的值变成了1+1=2。此时再将*b的值赋给*a,就相当于做了个n=n的赋值了。所以n的最终值就是2。
不过在将func()的a,b改为restrict指针以后,情况就变得有趣了:
void func(int *restrict a, int *restrict b)
{
*a += *b;
*a = *b;
}
修改后的执行结果是:n = 1。我们来看看为啥会出现两个不同的结果,首先看未加restrict时编译器生成的func()汇编码:
push ebp
mov ebp, esp
mov ecx, [ebp+arg_4]
mov edx, [ebp+arg_0]
mov eax, [ecx] ; 这里取了一次*b
add [edx], eax
mov eax, [ecx] ; 这里又取了一次*b
mov [edx], eax
pop ebp
retn
可以发现编译器对*a,*b两个指针的处理非常老实,基本没有做出啥偷懒的行为——因为编译器拿不准这两个指针是否是指向同一个对象,也拿不准指针所指的对象还有没有可能被第三方(别的线程或硬件)并发修改。所以编译器很老实地在每次计算过程中都从内存里取数值。因此这种保守的行为保证了结果的正确。
加了restrict以后,编译器认为开发者可以保证a、b两个指针所指的对象只能通过这两个指针访问,这就意味着:① a、b不可能指向相同的对象,且 ② a、b所指的对象也不存在被第三方并发修改的可能。换句话说在func()函数内掌控了对a、b所指对象的全部读写访问,因此编译器就采取了更为大胆的优化策略生成了这样的汇编码:
push ebp
mov ebp, esp
mov edx, [ebp+arg_4]
mov eax, [ebp+arg_0]
mov edx, [edx] ; 注意,这里只取了一次*b
add [eax], edx ; 遗憾,我还以为这个加法也会被优化掉,没想到还在
mov [eax], edx ; 因为编译器认为*b不会变,所以第二次直接用了寄存器值
pop ebp
retn
由于在func函数中*b是“只读”的,所以编译器就放心地优化掉了对*b的访存操作:*b实际只取了一次值,在之后的计算过程中始终用之前取到寄存器中的值以提高效率。而杯具的是,我们在main()里面调用func()时违反了我们对编译器所做的承诺:a、b实际指向的是同一个变量。所以在执行了*a += *b的时候,*a、*b都变成了2。但编译器仍然使用之前读到的1来做后面的赋值操作,从而得到了错误的结果。
所以从这个简单的实验里面可以看出来:① restrict确实对编译器的优化能力有改善,在承诺属实的前提下,能够最大限度提高编译器生成的目标码质量。② restrict是开发者对编译器做出的承诺,但如果承诺不兑现,后果很严重。
restrict既然是一个对编译器的hinting,所以貌似其主要用于提高编译器生成的目标码效率的——所以工程实践的角度来看能用上的地方实在不多。因为首先以减少访存为手段的优化至少要求①你要优化的这个函数必须是系统的热点,并且②你要优化的这个指针所指向的数据结构也要足够“热”。否则指望加个restrict修饰就能给目标效率带来什么明显的优化是不可能的。而再则:即使加了编译器也可能因为条件所限(如寄存器数量的限制),不一定就能生成明显更快的目标码来(个人推测可能在RISC体系里面restrict的效果相对好些,因为RISC体系的访存操作开销大,而处理器寄存器又多有利于编译器做有效优化)。
但有经验的人应该也都知道,即使在这种热点热数据上简单地加个hinting,其作用恐怕远不如对算法和数据结构的精心调整来得显著。所以从提高效率这个角度看restrict的实用价值实在比较有限。但不能说restrict就是个没什么用的东西——在我的实践中,把restrict放在函数参数的声明里面用,对代码的可读性能有不错的提升。因为就跟const一样,函数参数声明中的restrict能起到很好的对函数的前/后置条件(Pre/Postcondition)自注释的作用。
举个例子,看看C89的memset()和memmove()的声明:
void *memcpy(void *, const void *, size_t);
void *memmove(void *, const void *, size_t);
从这个声明里面你可以很快地看出:第一个参数肯定是拷贝的目的,第二个参数是拷贝的源——因为第二个参数带了const修饰,所以马上就能知道这个指针在函数的处理过程中是只读的,所以它只会是源。因此const在这里起到了两个作用:①自注释:都不用费劲在函数前面写啥注释文档,你也用不着去翻C库手册也能知道两个指针里面到底哪个是源指针那个是目的指针。②做出对调用者的承诺(或者说是自注释了函数的后置条件Postcondition):在函数处理的过程中保证对第二个参数所指内容是只读的。
C99在支持了restrict以后,memset()和memmove()的声明又变成了这样:
void *memcpy(void *restrict, const void *restrict, size_t);
void *memmove(void *, const void *, size_t);
这样代码的自注释能力就更强了:memcpy中的restrict提出了一个由调用者承诺满足的前置条件,即这两个指针所指的内存区域不能重叠(否则restrict就不成立)。所以我们又能很快看出:memcpy不能处理两块存在重叠的内存区间的拷贝,而memmove可以。所以代码里面依然是一行注释也没有,仅仅通过多加了几个修饰符,我们就能从中看出这么多的东西,免去RTFM求证到底是从哪拷到哪或者两块内存能不能重叠之类的烦恼!这就是自注释的力量,也是我认为像restrict这种即使不用也不会产生什么副作用的“可有可无”的修饰符的最大价值之所在。
综上,我对restrict的使用建议是:鼓励用在函数参数的声明中,以提高可读性实现自注释。但在函数内部还是少用,因为在看到一个函数里面用了某些以提高效率为目的的写法时,读者或代码维护者可能会有点紧张:这往往说明这个函数效率攸关十分重要,需要你小心翼翼地去对付。通常看到const令人安心,看到volatile和restrict一般都不是啥好事:要么意味着这里有并发的雷区,要么说明这块代码性能重要到连个访存优化也不能放过——所以尽量不要在代码里面到处狼来了,应该只把它用在确实需要的地方。
最后顺便提一下,想自己尝试这个实验的注意一定要把func和main放在不同的c文件里面编译成obj以后在链接为可执行文件。绝对不能偷懒把两个函数都堆在一个c文件里面编译。因为编译器的编译优化是以文件为单位的。如果你把两个函数堆在一块,编译器立马就发现了你的小把戏,无论加不加restrict,得到的结果都是n = 2——实际上在我反汇编以后,发现一个更杯具的情况:堆在一块编译后,gcc干脆直接把结果直接算好了,func函数尸骨无存:
mov dword ptr [esp+4], 2 ; 耍赖了直接printf最终结果2,func被优化没了
mov dword ptr [esp], offset aND ; "n = %d\n"
call printf
关于C99的扩展的使用心得就到这里结束。以上应该涵盖了绝大部分C99相对C89的扩展,也是我自己有实践过觉得值得拿出来说的。不过还有些很少用的或者我觉得实在是没有太大实用价值的特性(如static array parameters等)没有列出来,欢迎各位留言补充。另外不管你是再哪里看到这篇文章的,一定注意如果要留言的话请到这篇文章在我的网易博客的【原始出处】上留,不然我是看不到也不会回的。