Hidden features of C
我知道所有C编译器实现背后都有一个标准,所以不应该有隐藏的特性。尽管如此,我确信所有的C开发人员都有他们一直使用的隐藏/秘密技巧。
更多的是gcc编译器的一个技巧,但是您可以向编译器提供分支指示提示(在Linux内核中很常见)
1 2 | #define likely(x) __builtin_expect((x),1) #define unlikely(x) __builtin_expect((x),0) |
参见:http://kerneltrap.org/node/4705
我喜欢的是它还为一些函数添加了一些表达性。
1 2 3 4 5 6 7 8 9 | void foo(int arg) { if (unlikely(arg == 0)) { do_this(); return; } do_that(); ... } |
1 2 3 4 5 6 | int8_t int16_t int32_t uint8_t uint16_t uint32_t |
这些是标准中的可选项目,但必须是隐藏的特性,因为人们不断地重新定义它们。我研究过的一个代码基(现在仍然如此)有多个重新定义,所有的重新定义都有不同的标识符。大多数情况下,使用预处理器宏时:
1 2 | #define INT16 short #define INT32 long |
等等。它让我想拔出我的头发。只需使用异常的标准整数typedef!
逗号运算符没有被广泛使用。它当然可以被滥用,但也非常有用。这种用法最常见:
1 2 3 4 | for (int i=0; i<10; i++, doSomethingElse()) { /* whatever */ } |
但是你可以在任何地方使用这个接线员。观察:
1 2 |
每个语句都会被计算,但表达式的值将是最后一个被计算语句的值。
正在将结构初始化为零
1 | struct mystruct a = {0}; |
这将使所有结构元素归零。
函数指针。您可以使用函数指针表来实现,例如,快速间接线程代码解释程序(forth)或字节代码调度程序,或者模拟类OO的虚拟方法。
然后在标准库中有隐藏的gem,例如qsort()、bsearch()、strpbrk()、strcspn()[后两个对于实现strtok()替换很有用]。
C的一个错误特征是有符号算术溢出是未定义的行为(ub)。因此,每当您看到一个表达式(如x+y)都是有符号整数时,它可能会溢出并导致ub。
多字符常量:
1 | int x = 'ABCD'; |
这会将
编辑:此技术不可移植,尤其是在序列化int时。但是,创建自文档枚举非常有用。例如
1 2 3 4 5 | enum state { stopped = 'STOP', running = 'RUN!', waiting = 'WAIT', }; |
如果您正在查看原始内存转储,并且需要在不必查找枚举的情况下确定枚举的值,那么这样做就简单多了。
我从来没有用过位场,但它们听起来很酷,超低水平的东西。
1 2 3 4 5 6 7 8 9 10 11 12 13 | struct cat { unsigned int legs:3; // 3 bits for legs (0-4 fit in 3 bits) unsigned int lives:4; // 4 bits for lives (0-9 fit in 4 bits) // ... }; cat make_cat() { cat kitty; kitty.legs = 4; kitty.lives = 9; return kitty; } |
这意味着
包括亚伦和麻风病人的评论,谢谢大家。
C有一个标准,但不是所有的C编译器都是完全兼容的(我还没有看到任何完全兼容的C99编译器!).
也就是说,我喜欢的技巧是那些不明显的,跨平台可移植的,因为它们依赖于C语义。它们通常是关于宏或位算术的。
例如:在不使用临时变量的情况下交换两个无符号整数:
1 2 3 | ... a ^= b ; b ^= a; a ^=b; ... |
或"扩展c"来表示有限状态机,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | FSM { STATE(x) { ... NEXTSTATE(y); } STATE(y) { ... if (x == 0) NEXTSTATE(y); else NEXTSTATE(x); } } |
可通过以下宏实现:
1 2 3 | #define FSM #define STATE(x) s_##x : #define NEXTSTATE(x) goto s_##x |
不过,总的来说,我不喜欢那些巧妙的技巧,但会使代码变得不必要的复杂,难以阅读(作为交换示例),我喜欢那些使代码更清晰并直接传达意图的技巧(如FSM示例)。
像达夫装置这样的交错结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | strncpy(to, from, count) char *to, *from; int count; { int n = (count + 7) / 8; switch (count % 8) { case 0: do { *to = *from++; case 7: *to = *from++; case 6: *to = *from++; case 5: *to = *from++; case 4: *to = *from++; case 3: *to = *from++; case 2: *to = *from++; case 1: *to = *from++; } while (--n > 0); } } |
我非常喜欢指定的初始值设定项,在C99中添加(并在GCC中长期支持):
1 2 3 4 5 6 7 | #define FOO 16 #define BAR 3 myStructType_t myStuff[] = { [FOO] = { foo1, foo2, foo3 }, [BAR] = { bar1, bar2, bar3 }, ... |
数组初始化不再依赖于位置。如果更改foo或bar的值,数组初始化将自动对应其新值。
C99有一些很棒的任何订单结构初始化。
ZZU1〔11〕
匿名结构和数组是我最喜欢的。(参见http://www.run.montefiore.ulg.ac.be/~martin/resources/kung-f00.html)
1 | setsockopt(yourSocket, SOL_SOCKET, SO_REUSEADDR, (int[]){1}, sizeof(int)); |
或
1 2 3 4 | void myFunction(type* values) { while(*values) x=*values++; } myFunction((type[]){val1,val2,val3,val4,0}); |
它甚至可以用来显示链接列表…
好。。。我认为C语言的一个优点是它的可移植性和标准性,所以每当我在当前使用的实现中发现一些"隐藏的技巧"时,我就尽量不使用它,因为我尽量保持C代码的标准性和可移植性。
当我第一次看到"震惊"我的(隐藏的)特征是关于printf的。此功能允许您使用变量来格式化格式说明符本身。寻找代码,你会看到更好的:
1 2 3 4 5 6 7 8 9 |
*字符达到这个效果。
GCC有许多我喜欢的C语言扩展,可以在这里找到。我最喜欢的是函数属性。一个非常有用的例子是格式属性。如果定义一个采用printf格式字符串的自定义函数,则可以使用此函数。如果启用此函数属性,gcc将检查参数,以确保格式字符串和参数匹配,并根据需要生成警告或错误。
1 2 | int my_printf (void *my_object, const char *my_format, ...) __attribute__ ((format (printf, 2, 3))); |
编译时断言,这里已经讨论过了。
1 2 3 4 5 6 7 8 | //--- size of static_assertion array is negative if condition is not met #define STATIC_ASSERT(condition) \ typedef struct { \ char static_assertion[condition ? 1 : -1]; \ } static_assertion_t //--- ensure structure fits in STATIC_ASSERT(sizeof(mystruct_t) <= 4096); |
常量字符串连接
我很惊讶没有在答案中看到它全部就绪,因为我所知道的所有编译器都支持它,但是许多程序员似乎忽略了它。有时它真的很方便,而且不仅仅是在编写宏时。
我当前代码中的用例:我在配置文件中有一个
1 | fd = open(PATH"/file", flags); |
而不是可怕的,但很常见的:
1 2 3 |
请注意,常见的可怕解决方案是:
- 三倍长
- 更不容易阅读
- 慢得多
- 在设置为任意缓冲区大小限制时功率不足(但如果不包含常量字符串,则必须使用更长的代码来避免这种情况)。
- 使用更多堆栈空间
嗯,我从来没有用过它,我也不确定我是否会向任何人推荐它,但我觉得这个问题不完整,没有提到西蒙·塔瑟姆的共同常规技巧。
初始化数组或枚举时,可以在初始值设定项列表中的最后一项后加逗号。例如:
1 2 3 | int x[] = { 1, 2, 3, }; enum foo { bar, baz, boom, }; |
这样做的目的是,如果您自动生成代码,则不必担心删除最后一个逗号。
结构分配很酷。许多人似乎没有意识到结构也是一种价值观,并且可以被分配,当一个简单的分配完成这个技巧时,不需要使用
例如,考虑一些假想的二维图形库,它可以定义一个表示(整数)屏幕坐标的类型:
1 2 3 4 | typedef struct { int x; int y; } Point; |
现在,您可以做一些看起来"错误"的事情,比如编写一个函数,创建一个从函数参数初始化的点,然后返回它,如下所示:
1 2 3 4 5 6 7 | Point point_new(int x, int y) { Point p; p.x = x; p.y = y; return p; } |
这是安全的,只要(当然)返回值是通过使用结构赋值的值复制的:
1 2 | Point origin; origin = point_new(0, 0); |
通过这种方式,您可以编写非常干净和面向对象的ISH代码,所有代码都是纯标准的C。
奇异向量索引:
1 2 | int v[100]; int index = 10; /* v[index] it's the same thing as index[v] */ |
使用SScanf时,可以使用%n查找应继续阅读的位置:
1 2 |
显然,您不能添加其他答案,因此我将在这里包括第二个答案,您可以使用"&;&;"和""作为条件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
此代码将输出:
1 2 | Hi ROFL |
C编译器实现了几个标准中的一个。然而,有一个标准并不意味着语言的所有方面都被定义。例如,达夫的设备是一个最受欢迎的"隐藏"功能,它已经非常流行,以至于现代编译器都有特殊用途的识别代码,以确保优化技术不会破坏这种常用模式的预期效果。
一般来说,当您在编译器使用的任何C标准的边缘上运行时,隐藏的特性或语言技巧是不鼓励的。许多这样的技巧在一个编译器和另一个编译器之间都不起作用,而且这些类型的特性通常会从给定制造商的编译器套件的一个版本失败到另一个版本。
破坏C代码的各种技巧包括:
每当程序员对大多数C标准中都指定为"依赖编译器"行为的执行模型进行假设时,就会出现其他问题。
我发现最近0位字段。
1 2 3 4 5 6 7 | struct { int a:3; int b:2; int :0; int c:4; int d:3; }; |
它将给出
1 | 000aaabb 0ccccddd |
而不是没有:0;
1 | 0000aaab bccccddd |
0宽度字段表示应在下一个原子实体上设置以下位字段(
使用int(3)在代码处设置断点是我最喜欢的方法
gcc(c)有一些您可以启用的有趣特性,例如嵌套函数声明和a?:b形式?:运算符,如果a不是false,则返回。
使用枚举进行编译时假设检查:这是一个愚蠢的例子,但对于具有编译时可配置常量的库来说确实有用。
1 2 3 4 5 6 7 8 | #define D 1 #define DD 2 enum CompileTimeCheck { MAKE_SURE_DD_IS_TWICE_D = 1/(2*(D) == (DD)), MAKE_SURE_DD_IS_POW2 = 1/((((DD) - 1) & (DD)) == 0) }; |
我最喜欢的C的"隐藏"功能是使用printf中的%n写回堆栈。通常,printf根据格式字符串从堆栈中弹出参数值,但%n可以将其写回。
在这里查看第3.4.2节。会导致很多严重的漏洞。
c99样式变量参数宏,又名
1 2 3 | #define ERR(name, fmt, ...) fprintf(stderr,"ERROR" #name":" fmt" ", \ __VAR_ARGS__) |
它的用法是
1 | ERR(errCantOpen,"File %s cannot be opened", filename); |
在这里,我还使用了Stringize操作符和字符串常量concatation,这是我真正喜欢的其他特性。
可变大小的自动变量在某些情况下也很有用。这些被添加到i nc99中,并在GCC中得到长期支持。
1 2 | void foo(uint32_t extraPadding) { uint8_t commBuffer[sizeof(myProtocol_t) + extraPadding]; |
最后在堆栈上有一个缓冲区,其中有固定大小协议头和可变大小数据的空间。您可以使用alloca()获得相同的效果,但这种语法更紧凑。
在调用这个例程之前,必须确保额外填充是一个合理的值,否则会导致堆栈崩溃。在调用malloc或任何其他内存分配技术之前,必须对参数进行健全的检查,所以这并不罕见。
我喜欢
GCC中的lambda(例如匿名函数):
1 2 | #define lambda(return_type, function_body) \ ({ return_type fn function_body fn }) |
这可用于:
1 | lambda (int, (int x, int y) { return x > y; })(1, 2) |
扩展为:
1 | ({ int fn (int x, int y) { return x > y } fn; })(1, 2) |
我喜欢你可以做的可变大小的结构:
1 2 3 4 5 6 7 8 | typedef struct { unsigned int size; char buffer[1]; } tSizedBuffer; tSizedBuffer *buff = (tSizedBuffer*)(malloc(sizeof(tSizedBuffer) + 99)); // can now refer to buff->buffer[0..99]. |
还有offsetof宏,它现在在ANSIC中,但在我第一次看到它时它是一个奇才。它基本上使用运算符(&;)的地址将空指针重新编译为结构变量。
对于清除输入缓冲区,不能使用
]%*c")
GCC的早期版本尝试在源代码中遇到"pragma"时运行游戏。这里也可以看到。
我是在15年多的C编程之后才发现这一点的:
1 2 3 4 5 6 | struct SomeStruct { unsigned a : 5; unsigned b : 1; unsigned c : 7; }; |
比特场!冒号后面的数字是成员所需的位数,成员被打包到指定的类型中,因此,如果无符号为16位,则上面的内容如下所示:
1 | xxxc cccc ccba aaaa |
斯基兹
通过使用异常的类型转换类型。虽然不是隐藏的功能,但它相当棘手。
例子:
如果需要了解编译器存储浮动的方式,请尝试以下操作:
1 2 3 4 5 6 7 | uint32_t Int; float flt = 10.5; // say Int = *(uint32_t *)&flt; printf ("Float 10.5 is stored internally as %8X ", Int); |
或
1 2 3 4 |
注意打字的巧妙运用。将变量的地址(此处&flt)转换为所需类型(此处(uint32_t*))并提取其内容(应用"*"。
这也适用于表达式的另一面:
1 | *(float *)&Int = flt; |
这也可以通过联合来实现:
1 2 3 4 5 6 | typedef union { uint32_t Int; float flt; } FloatInt_type; |
我有一次用一点代码显示了这一点,然后问它做了什么:
1 | hexDigit ="0123456789abcdef"[someNybble]; |
另一个最喜欢的是:
1 2 3 | unsigned char bar[100]; unsigned char *foo = bar; unsigned char blah = 42[foo]; |
将变量与文字进行比较时,最好将文字放在
1 2 3 | if (0 == count) { ... } |
乍一看可能看起来很奇怪,但它可以减轻一些头痛(比如,如果你不小心碰到了
SteveWebb指出了
我在一个设备上工作,没有端口可用于将日志信息从设备传递到用于调试的PC。可以使用断点停止并使用调试器知道程序的状态,但没有关于系统跟踪的信息。
由于对调试日志的所有调用实际上都是一个全局宏,因此我们将该宏更改为将文件名和行号转储到全局数组。这个数组包含一系列文件名和行号,显示调用了哪些调试调用,给出了执行跟踪的大致概念(但不是实际的日志消息)。可以暂停调试器的执行,将这些字节转储到本地文件,然后使用脚本将这些信息映射到代码库。这之所以成为可能,是因为我们有严格的编码准则,所以我们可以在一个文件中对日志机制进行更改。
用于声明指针类型变量的intptr_t。C99特定并在stdint.h中声明
这不是一个隐藏的特征,但在我看来就像巫毒,我第一次看到这样的东西:
1 2 3 4 5 6 7 8 9 | void callback(const char *msg, void *data) { // do something with msg, e.g. printf("%s ", msg); return; data = NULL; } |
这种构造的原因是,如果您使用-wextra编译它,并且不使用"data=null;"-行,那么gcc将发出一个关于未使用参数的警告。但有了这条无用的线,你就不会得到警告。
编辑:我知道还有其他更好的方法来防止这些警告。我第一次看到这个,就觉得很奇怪。
Excerpt:
In this page, you will find a list of
interesting C programming
questions/puzzles, These programs
listed are the ones which I have
received as e-mail forwards from my
friends, a few I read in some books, a
few from the internet, and a few from
my coding experiences in C.
网址:http://www.gowrikumar.com/c/index.html
注册变量
我曾经用
GCC中有三个不错的:
1 2 3 | __FILE__ __FUNCTION__ __LINE__ |
函数指针的大小不标准。至少在K&R手册中没有。尽管它讨论了其他类型指针的大小,但(我认为)函数指针的
另外,
我看到的一个错误如下(一个简化的例子):
1 2 3 | int j; int i; j = sizeof(i++) |
当编译时评估
C中的运算符优先级控制关联顺序,而不是计算顺序。例如,如果您有三个函数
1 | f() + g() * h() |
C标准没有给出这些功能的评价顺序规则。将
假设您有一个具有相同类型成员的结构:
1 2 3 4 5 | struct Point { float x; float y; float z; }; |
可以将其实例强制转换为浮点指针并使用数组索引:
1 2 3 4 | Point a; int sum = 0, i = 0; for( ; i < 3; i++) sum += ((float*)a)[i]; |
非常基本,但在编写简洁的代码时很有用。
我喜欢typeof()运算符。它的工作方式与sizeof()类似,因为它是在编译时解析的。它不返回字节数,而是返回类型。当需要将变量声明为与其他变量相同的类型时,无论变量的类型是什么,这都很有用。
1 2 | typeof(foo) copy_of_foo; //declare bar to be a variable of the same type as foo copy_of_foo = foo; //now copy_of_foo has a backup of foo, for any type |
这可能只是GCC的扩展,我不确定。
面向对象的C宏:您需要一个构造函数(init)、一个析构函数(dispose)、一个相等的(equal)、一个复制器(copy)和一些用于实例化的原型(prototype)。
通过声明,您需要声明一个常量原型来复制和派生。然后你可以做
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | #define C_copy(to, from) to->copy(to, from) #define true 1 #define false 0 #define C_OO_PROTOTYPE(type)\ void type##_init (struct type##_struct *my);\ void type##_dispose (struct type##_struct *my);\ char type##_equal (struct type##_struct *my, struct type##_struct *yours); \ struct type##_struct * type##_copy (struct type##_struct *my, struct type##_struct *from); \ const type type##__prototype = {type##_init, type##_dispose, type##_equal, type##_copy #define C_OO_OVERHEAD(type)\ void (*init) (struct type##_struct *my);\ void (*dispose) (struct type##_struct *my);\ char (*equal) (struct type##_struct *my, struct type##_struct *yours); \ struct type##_struct *(*copy) (struct type##_struct *my, struct type##_struct *from); #define C_OO_IN(ret, type, function, ...) ret (* function ) (struct type##_struct *my, __VA_ARGS__); #define C_OO_OUT(ret, type, function, ...) ret type##_##function (struct type##_struct *my, __VA_ARGS__); #define C_OO_PNEW(type, instance)\ instance = ( type *) malloc(sizeof( type ));\ memcpy(instance, & type##__prototype, sizeof( type )); #define C_OO_NEW(type, instance)\ type instance;\ memcpy(&instance, & type ## __prototype, sizeof(type)); #define C_OO_DELETE(instance)\ instance->dispose(instance);\ free(instance); #define C_OO_INIT(type) void type##_init (struct type##_struct *my){return;} #define C_OO_DISPOSE(type) void type##_dispose (struct type##_struct *my){return;} #define C_OO_EQUAL(type) char type##_equal (struct type##_struct *my, struct type##_struct *yours){return 0;} #define C_OO_COPY(type) struct type##_struct * type##_copy (struct type##_struct *my, struct type##_struct *from){return 0;} |
我刚刚读了这篇文章。它有一些C语言和其他几种语言的"隐藏特性"。
像这样包装malloc和realloc:
1 2 3 4 5 6 7 8 | #ifdef _DEBUG #define mmalloc(bytes) malloc(bytes);printf("malloc: %d\t<%s@%d> ", bytes, __FILE__, __LINE__); #define mrealloc(pointer, bytes) realloc(pointer, bytes);printf("realloc: %d\t<%s@%d> ", bytes, __FILE__, __LINE__); #else //_DEBUG #define mmalloc(bytes) malloc(bytes) #define mrealloc(pointer, bytes) realloc(pointer, bytes) |
事实上,这是我的全砷化物(法警是为OOC准备的):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #ifdef _DEBUG #define mmalloc(bytes) malloc(bytes);printf("malloc: %d\t<%s@%d> ", bytes, __FILE__, __LINE__); #define mrealloc(pointer, bytes) realloc(pointer, bytes);printf("realloc: %d\t<%s@%d> ", bytes, __FILE__, __LINE__); #define BAILIFNOT(Node, Check) if(Node->type != Check) return 0; #define NULLCHECK(var) if(var == NULL) setError(__FILE__, __LINE__,"Null exception"," var", FATAL); #define ASSERT(n) if( ! ( n ) ) { printf("<ASSERT FAILURE@%s:%d>", __FILE__, __LINE__); fflush(0); __asm("int $0x3"); } #define TRACE(n) printf("trace: %s <%s@%d> ", n, __FILE__, __LINE__);fflush(0); #else //_DEBUG #define mmalloc(bytes) malloc(bytes) #define mrealloc(pointer, bytes) realloc(pointer, bytes) #define BAILIFNOT(Node, Check) {} #define NULLCHECK(var) {} #define ASSERT(n) {} #define TRACE(n) {} #endif //_DEBUG |
下面是一些输出示例:
1 2 3 4 5 6 7 8 9 |
可变大小的结构,常见的冲突解决程序lib和其他位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
在开关内使用while(0)如何才能使用continue语句,如break:-)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
在
1 2 3 4 5 6 7 8 9 10 11 12 | int pos1, pos2; char *string_of_unknown_length ="we don't care about the length of this"; printf("Write text of unknown %n(%s)%n text ", &pos1, string_of_unknown_length, &pos2); printf("%*s\\%*s/ ", pos1,"", pos2-pos1-2,""); printf("%*s", pos1+1,""); for(int i=pos1+1; i<pos2-1; i++) putc('-', stdout); putc(' ', stdout); |
将有以下输出
1 2 3 | Write text of unknown (we don't care about the length of this) text \ / -------------------------------------- |
虽然有点做作,但在制作漂亮的报告时也有一些用处。
使用NaN进行连锁计算/返回错误:
//#include
静态uint64_t in an=0xfff800000000;
const double nan=*(double*)&ina;//安静的nan
内部函数可以返回NaN作为错误标志:它可以安全地用于任何计算,结果始终为NaN。
注:NAN测试很难,因为NAN!=南…使用isnan(x),或自己滚动。
X!=x在数学上是正确的,如果x是NaN,但往往会被一些编译器优化。
在Visual Studio中,可以突出显示自己定义的类型。
为此,在"commom7/ide"文件夹中创建一个名为"usertype.dat"的文件。该文件的内容应为要突出显示的类型。例如:
//usertype.dat的内容
1 2 3 4 5 6 7 8 9 10 11 | int8_t int16_t int32_t int64_t uint8_t uint16_t uint32_t uint64_t float32_t float64_t char_t |