我知道所有C编译器实现背后都有一个标准,所以应该没有隐藏的功能.尽管如此,我确信所有C开发人员都有他们一直使用的隐藏/秘密技巧.
更多GCC编译器的技巧,但您可以给编译器提供分支指示提示(在Linux内核中很常见)
#define likely(x) __builtin_expect((x),1) #define unlikely(x) __builtin_expect((x),0)
见:http://kerneltrap.org/node/4705
我喜欢这个,它也增加了一些功能的表现力.
void foo(int arg) { if (unlikely(arg == 0)) { do_this(); return; } do_that(); ... }
int8_t int16_t int32_t uint8_t uint16_t uint32_t
这些是标准中的可选项,但它必须是隐藏的功能,因为人们不断重新定义它们.我曾经使用过的一个代码库(目前仍在使用)具有多个重定义,所有重定义都具有不同的标识符.大部分时间都是使用预处理器宏:
#define INT16 short #define INT32 long
等等.这让我想把头发拉出来. 只需使用怪异的标准整数typedef!
逗号运算符未被广泛使用.它当然可以被滥用,但它也非常有用.这种用法是最常见的:
for (int i=0; i<10; i++, doSomethingElse()) { /* whatever */ }
但您可以在任何地方使用此运算符.注意:
int j = (printf("Assigning variable j\n"), getValueFromSomewhere());
将评估每个语句,但表达式的值将是最后一个语句的值.
将结构初始化为零
struct mystruct a = {0};
这将使所有结构元素归零.
函数指针.您可以使用函数指针表来实现,例如,快速间接线程代码解释器(FORTH)或字节码调度程序,或模拟类似OO的虚拟方法.
然后标准库中有隐藏的宝石,例如qsort(),bsearch(),strpbrk(),strcspn()[后两者对于实现strtok()替换很有用].
C的错误特征是带符号的算术溢出是未定义的行为(UB).因此,每当您看到一个表达式(如x + y)都是有符号的整数时,它可能会溢出并导致UB.
多字符常量:
int x = 'ABCD';
这设置x
为0x41424344
(或0x44434241
取决于架构).
编辑:这种技术不可移植,特别是如果你序列化int.但是,创建自记录枚举功能非常有用.例如
enum state { stopped = 'STOP', running = 'RUN!', waiting = 'WAIT', };
如果你正在查看原始内存转储并且需要确定枚举的值而不必查找它,这会使它变得更加简单.
我从来没有使用过bit字段,但是对于超低级的东西来说它们听起来很酷.
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; }
这意味着sizeof(cat)
可以小到sizeof(char)
.
感谢Aaron和leppie的评论,谢谢你们.
交错结构,如Duff的设备:
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); } }
C有一个标准,但不是所有的C编译器都完全兼容(我还没有看到任何完全兼容的C99编译器!).
也就是说,我更喜欢的技巧是那些非常明显且可以跨平台移植的技巧,因为它们依赖于C语义.它们通常是关于宏或位算术的.
例如:在不使用临时变量的情况下交换两个无符号整数:
... a ^= b ; b ^= a; a ^=b; ...
或"扩展C"代表有限状态机,如:
FSM { STATE(x) { ... NEXTSTATE(y); } STATE(y) { ... if (x == 0) NEXTSTATE(y); else NEXTSTATE(x); } }
可以使用以下宏实现:
#define FSM #define STATE(x) s_##x : #define NEXTSTATE(x) goto s_##x
但总的来说,我不喜欢聪明的技巧,但是让代码不必要地复杂化(作为交换示例),我喜欢使代码更清晰并直接传达意图的代码(如FSM示例) .
我非常喜欢指定的初始化程序,在C99中添加(并且在gcc中支持了很长时间):
#define FOO 16 #define BAR 3 myStructType_t myStuff[] = { [FOO] = { foo1, foo2, foo3 }, [BAR] = { bar1, bar2, bar3 }, ...
数组初始化不再依赖于位置.如果更改FOO或BAR的值,阵列初始化将自动对应于其新值.
C99有一些很棒的任意顺序结构初始化.
struct foo{ int x; int y; char* name; }; void main(){ struct foo f = { .y = 23, .name = "awesome", .x = -38 }; }
匿名结构和数组是我最喜欢的.(参见http://www.run.montefiore.ulg.ac.be/~martin/resources/kung-f00.html)
setsockopt(yourSocket, SOL_SOCKET, SO_REUSEADDR, (int[]){1}, sizeof(int));
要么
void myFunction(type* values) { while(*values) x=*values++; } myFunction((type[]){val1,val2,val3,val4,0});
它甚至可以用来实现链表...
好吧......我认为C语言的一个优点是它的可移植性和标准性,所以每当我在我正在使用的实现中找到一些"隐藏的技巧"时,我尽量不使用它,因为我试图保持我的C代码尽可能标准和便携.
gcc有很多我喜欢的C语言扩展,可以在这里找到.我最喜欢的一些是功能属性.一个非常有用的例子是format属性.如果您定义一个采用printf格式字符串的自定义函数,则可以使用此方法.如果启用此函数属性,gcc将对您的参数进行检查,以确保您的格式字符串和参数匹配,并在适当时生成警告或错误.
int my_printf (void *my_object, const char *my_format, ...) __attribute__ ((format (printf, 2, 3)));
我第一次见到时"震惊"我的(隐藏)功能是关于printf.此功能允许您使用变量格式化格式说明符本身.寻找代码,你会看到更好的:
#includeint main() { int a = 3; float b = 6.412355; printf("%.*f\n",a,b); return 0; }
*角色达到了这个效果.
编译时断言,如此处所述.
//--- 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);
常量字符串连接
我很惊讶没有在答案中看到它,因为我所知道的所有编译器都支持它,但许多程序员似乎忽略了它.有时它非常方便,而且不仅仅是在编写宏时.
我在当前代码中使用的用例:我#define PATH "/some/path/"
在配置文件中有一个(实际上它是由makefile设置的).现在我想构建包含文件名的完整路径来打开资源.它只是去:
fd = open(PATH "/file", flags);
而不是可怕的,但很常见的:
char buffer[256]; snprintf(buffer, 256, "%s/file", PATH); fd = open(buffer, flags);
请注意,常见的可怕解决方案是:
三倍的时间
更不容易阅读
慢得多
设置为任意缓冲区大小限制的强大功能(但你必须使用更长的代码来避免没有常量字符串连接).
使用更多的堆栈空间
好吧,我从来没有用它,而且我不确定我是否会向任何人推荐它,但我觉得这个问题在不提及Simon Tatham的常规技巧的情况下是不完整的.
结构分配很酷.许多人似乎并没有意识到结构也是值,并且可以被分配,没有必要使用memcpy()
,当一个简单的赋值就可以了.
例如,考虑一些虚构的2D图形库,它可能会定义一个表示(整数)屏幕坐标的类型:
typedef struct { int x; int y; } Point;
现在,你做的事情可能看起来"错误",比如编写一个创建从函数参数初始化的点的函数,并返回它,如下所示:
Point point_new(int x, int y) { Point p; p.x = x; p.y = y; return p; }
这是安全的,当然很长(因为)使用struct赋值通过值复制返回值:
Point origin; origin = point_new(0, 0);
通过这种方式,您可以编写非常干净且面向对象的代码,所有代码都采用简单的标准C.
初始化数组或枚举时,可以在初始化列表中的最后一项之后放置逗号.例如:
int x[] = { 1, 2, 3, }; enum foo { bar, baz, boom, };
这样做是为了让你自动生成代码,你不必担心删除最后一个逗号.
奇怪的矢量索引:
int v[100]; int index = 10; /* v[index] it's the same thing as index[v] */
C编译器实现了几个标准之一.但是,拥有标准并不意味着语言的所有方面都已定义. 例如,Duff的设备是最受欢迎的"隐藏"功能,它已经变得如此受欢迎,以至于现代编译器具有特殊用途的识别代码,以确保优化技术不会破坏这种常用模式的预期效果.
一般情况下,当您在编译器使用的任何C标准的剃刀边缘上运行时,不鼓励隐藏功能或语言技巧.许多这样的技巧从一个编译器到另一个编译器不起作用,并且这些类型的功能通常会从给定制造商的一个版本的编译器套件失败到另一个版本.
破坏C代码的各种技巧包括:
依赖于编译器如何在内存中布局结构.
关于整数/浮点数的字节顺序的假设.
功能ABI的假设.
关于堆栈帧增长方向的假设.
关于语句内执行顺序的假设.
关于函数参数中语句执行顺序的假设.
关于short,int,long,float和double类型的位大小或精度的假设.
当程序员对大多数C标准中指定为"编译器相关"行为的执行模型做出假设时,会出现其他问题和问题.
使用sscanf时,您可以使用%n找出您应该继续阅读的位置:
sscanf ( string, "%d%n", &number, &length ); string += length;
显然,你不能添加另一个答案,所以我会在这里添加第二个答案,你可以使用"&&"和"||" 作为条件:
#include#include int main() { 1 || puts("Hello\n"); 0 || puts("Hi\n"); 1 && puts("ROFL\n"); 0 && puts("LOL\n"); exit( 0 ); }
此代码将输出:
Hi ROFL
使用INT(3)在代码中设置断点是我一直以来的最爱
我最喜欢的C"隐藏"功能是在printf中使用%n来写回堆栈.通常,printf会根据格式字符串从堆栈中弹出参数值,但%n可以将它们写回.
在这里查看3.4.2节.可能会导致很多令人讨厌的漏洞.
Gcc(c)有一些你可以启用的有趣功能,比如嵌套函数声明,以及?:b形式的?:运算符,如果a不是false,则返回a.
使用枚举进行编译时假设检查:愚蠢的例子,但对于具有编译时可配置常量的库非常有用.
#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) };
我最近发现了0个位域.
struct { int a:3; int b:2; int :0; int c:4; int d:3; };
这将给出一个布局
000aaabb 0ccccddd
而不是没有:0;
0000aaab bccccddd
0宽度字段表示应在下一个原子实体(char
)上设置以下位域
C99风格的可变参数宏,又名
#define ERR(name, fmt, ...) fprintf(stderr, "ERROR " #name ": " fmt "\n", \ __VAR_ARGS__)
哪个会像
ERR(errCantOpen, "File %s cannot be opened", filename);
在这里我还使用stringize运算符和字符串常量连接,其他功能我真的很喜欢.
在某些情况下,可变大小的自动变量也很有用.这些是在nC99中添加的,并且已经在gcc中支持了很长时间.
void foo(uint32_t extraPadding) { uint8_t commBuffer[sizeof(myProtocol_t) + extraPadding];
最终在堆栈上有一个缓冲区,其中包含固定大小的协议头和可变大小数据的空间.您可以使用alloca()获得相同的效果,但此语法更紧凑.
在调用此例程之前,您必须确保extraPadding是一个合理的值,否则您最终会破坏堆栈.你必须在调用malloc或任何其他内存分配技术之前检查参数是否健全,所以这并不是很不寻常.
我喜欢你可以制作的可变大小的结构:
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宏,现在是ANSI C,但是在我第一次看到它的时候是一段魔法.它基本上使用address-of运算符(&)将空指针重铸作为结构变量.
GCC中的Lambda(例如匿名函数):
#define lambda(return_type, function_body) \ ({ return_type fn function_body fn })
这可以用作:
lambda (int, (int x, int y) { return x > y; })(1, 2)
这扩展到:
({ int fn (int x, int y) { return x > y } fn; })(1, 2)
我喜欢__LINE__
和__FILE__
.请参见:http: //gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html