Answers:
函数指针。您可以使用函数指针表来实现,例如,快速间接线程代码解释器(FORTH)或字节码分派器,或模拟类似于OO的虚拟方法。
然后在标准库中有隐藏的gems,例如qsort(),bsearch(),strpbrk(),strcspn()[后两者对于实现strtok()替换很有用]。
C的一个缺点是有符号算术溢出是未定义行为(UB)。因此,每当看到x + y之类的表达式都被签名为整数时,它可能会溢出并导致UB。
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
等等。这使我想拔头发。 只需使用怪异的标准整数typedefs!
逗号运算符没有广泛使用。它当然可以被滥用,但它也非常有用。这种用法是最常见的一种:
for (int i=0; i<10; i++, doSomethingElse())
{
/* whatever */
}
但是您可以在任何地方使用此运算符。观察:
int j = (printf("Assigning variable j\n"), getValueFromSomewhere());
每个语句都会被求值,但是表达式的值将是最后一个语句的值。
将结构初始化为零
struct mystruct a = {0};
这将所有结构元素清零。
memset
/ calloc
做“所有字节为零”(即物理零),的确不是所有类型都定义。{ 0 }
保证使用适当的逻辑零值对所有内容进行初始化。例如,即使给定平台上的null值为,也保证指针会获得其正确的null值。0xBAADFOOD
memset
(0
作为第二个参数)。在源代码中初始化/分配(或)该对象时,将得到逻辑零。这两种零不一定会产生相同的结果。如带有指针的示例。当你在一个指针,你会得到一个指针。但是,当您分配一个指针时,您将获得null指针值,它在物理级别可能是或其他任何值。0
{ 0 }
memset
0x0000
0
0xBAADF00D
double
。通常,它是根据IEEE-754标准实现的,其中逻辑零和物理零是相同的。但是该语言不需要IEEE-754。因此,可能会发生这样的情况,当您这样做时double d = 0;
(逻辑零),物理上内存所占用的某些位d
将不会为零。
多字符常量:
int x = 'ABCD';
设置x
为0x41424344
(或0x44434241
,取决于体系结构)。
编辑:此技术不可移植,特别是如果您序列化int。但是,创建自记录枚举可能非常有用。例如
enum state {
stopped = 'STOP',
running = 'RUN!',
waiting = 'WAIT',
};
如果您正在查看原始内存转储并且需要确定枚举的值而不必查找它,则这将变得更加简单。
我从未使用过位字段,但对于超低级的东西,它们听起来很酷。
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)
。
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示例) 。
像Duff's Device这样的交错结构:
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中长期支持):
#define FOO 16
#define BAR 3
myStructType_t myStuff[] = {
[FOO] = { foo1, foo2, foo3 },
[BAR] = { bar1, bar2, bar3 },
...
数组初始化不再依赖于位置。如果更改FOO或BAR的值,则数组初始化将自动对应于它们的新值。
匿名结构和数组是我最喜欢的一种。(请参阅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代码尽可能标准且可移植。
常量字符串串联
我很惊讶没有看到答案,因为我所知道的所有编译器都支持它,但是许多程序员似乎都忽略了它。有时,它非常方便,不仅在编写宏时。
我目前的代码中有一个用例:我#define PATH "/some/path/"
在配置文件中有一个(确实是由makefile设置的)。现在,我想构建包括文件名的完整路径以打开资源。它只是去:
fd = open(PATH "/file", flags);
而不是可怕的,但很常见:
char buffer[256];
snprintf(buffer, 256, "%s/file", PATH);
fd = open(buffer, flags);
请注意,常见的可怕解决方案是:
初始化数组或枚举时,可以在初始化列表中的最后一项后面加上逗号。例如:
int x[] = { 1, 2, 3, };
enum foo { bar, baz, boom, };
这样做是为了使您在自动生成代码时不必担心消除最后一个逗号。
结构分配很酷。许多人似乎都没有意识到结构也是值,并且可以通过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;
}
这是安全的,只要(当然)只要使用结构赋值按值复制返回值即可:
Point origin;
origin = point_new(0, 0);
这样,您可以使用纯标准C编写相当干净且面向对象的代码。
C编译器实现了几种标准之一。但是,拥有标准并不意味着已定义语言的所有方面。 例如,达夫(Duff)的设备已成为最受欢迎的“隐藏”功能,以至于现代编译器都具有特殊目的的识别代码,以确保优化技术不会掩盖这种常用模式的预期效果。
通常,当您在编译器使用的任何C标准的严格边缘上运行时,都不建议使用隐藏功能或语言技巧。从一个编译器到另一个编译器,许多这样的技巧不起作用,并且从给定制造商的一个编译器套件版本到另一个版本,这些功能通常会失效。
破坏了C代码的各种技巧包括:
每当程序员对所有C标准中都指定为“依赖于编译器”的行为的执行模型进行假设时,还会出现其他问题。
使用sscanf时,您可以使用%n找出您应该继续阅读的位置:
sscanf ( string, "%d%n", &number, &length );
string += length;
显然,您无法添加其他答案,因此我将在此处添加第二个答案,您可以使用“ &&”和“ ||” 作为条件:
#include <stdio.h>
#include <stdlib.h>
int main()
{
1 || puts("Hello\n");
0 || puts("Hi\n");
1 && puts("ROFL\n");
0 && puts("LOL\n");
exit( 0 );
}
此代码将输出:
你好 劳氏
我一直以来最喜欢使用INT(3)在代码处设置断点
我最喜欢的C“隐藏”功能是在printf中使用%n来写回堆栈。通常,printf根据格式字符串从堆栈中弹出参数值,但是%n可以将其写回。
在此处查看第3.4.2节。可以导致很多讨厌的漏洞。
使用枚举进行编译时假设检查:这是一个愚蠢的示例,但对于具有编译时可配置常量的库而言确实有用。
#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)
};
#define CompilerAssert(exp) extern char _CompilerAssert[(exp)?1:-1]
)
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或任何其他内存分配技术之前,您必须精打细算检查参数,因此这并非罕见。