C语言的隐藏特性

141

我知道在所有C编译器实现背后都有一个标准,因此不应该有任何隐藏功能。尽管如此,我相信所有的C开发人员都有他们经常使用的隐藏/秘密技巧。


如果你/有人能够编辑这个“问题”,指出最好的隐藏功能选择,就像在C#和Perl版本的问题中那样,那将是很棒的。 - Donal Fellows
56个回答

115

这更像是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();
     ...
}

2
这个技巧很酷... :) 特别是你定义的宏。 :) - Sundar R

77
int8_t
int16_t
int32_t
uint8_t
uint16_t
uint32_t

这些在标准中是可选项,但肯定是一种隐藏功能,因为人们经常重新定义它们。我曾经工作过的一个代码库(现在仍然在使用)有多个重新定义,每个都有不同的标识符。大多数情况下都是用预处理器宏实现的。

#define INT16 short
#define INT32  long

等等,这让我想把头发都拔光。 就用该死的标准整数类型定义啊!


3
我认为它们是C99或类似的版本。我还没有找到确保这些版本可用的便携方式。 - akauppi
10
在C99中,stdint.h是必需的,但根据某些供应商(咳咳 微软)的说法,遵循C99标准则并非必须。 - Ben Combee
5
@Pete,如果你想要很小心地解释一下:(1)这个帖子与任何微软产品无关。 (2)这个帖子从来都与C++无关。 (3)并不存在所谓的C++97。 - Ben Collins
5
请查看http://www.azillionmonkeys.com/qed/pstdint.h -- 这是一个接近可移植的stdint.h文件。 - gnud
3
据我所知,由于对C99标准的stdint.h库特定功能需求很高,Visual Studio 2010现在已经包含了该库。这一点比之前没有包含它要方便得多。 - Jonathan Sternberg
显示剩余6条评论

72

逗号运算符并不常用。它肯定可以被滥用,但它也可以非常有用。这是最常见的用法:

for (int i=0; i<10; i++, doSomethingElse())
{
  /* whatever */
}

但是你可以在任何地方使用这个运算符。观察:


int j = (printf("Assigning variable j\n"), getValueFromSomewhere());
每个语句都会被评估,但表达式的值将是最后一个被评估的语句的值。

7
我做了20年的编程,从未见过这种情况! - Martin Beckett
11
在C++中,您甚至可以对其进行重载。 - Wouter Lievens
6
当然,"can"并不等同于"should"。过度使用重载的危险在于,内置的已经适用于所有内容,包括void,因此由于缺乏可用的重载而无法编译的情况永远不会发生。也就是说,这给了程序员很大的自由度。 - Aaron
循环内部的int在C中无法使用:这是C++的改进。 ","操作符和for (i=0,j=10;i<j; j--, i++)一样吗? - Aif

63

将结构体初始化为零

struct mystruct a = {0};

这将把所有结构元素清零。


2
然而,它不会将填充清零。 - Mikeage
2
@simonn,如果结构包含非整数类型,它不会执行未定义的行为。使用0对浮点/双精度内存进行memset时,在解释浮点/双精度时仍将为零(浮点/双精度是故意设计成如此)。 - Trevor Boyd Smith
6
@Andrew:memset/ calloc会把“所有字节”都清零(即物理零),这确实并不适用于所有类型。{0}可以保证使用适当的“逻辑零值”初始化所有内容。例如,指针保证可以得到其适当的空值,即使在给定平台上的空值是0xBAADFOOD - AnT stands with Russia
3
实际上,区别通常只是概念上的。但从理论上来说,几乎任何类型都可能有这种情况,例如“double”。通常它是根据 IEEE-754 标准实现的,在该标准中,逻辑零和物理零是相同的。但是,语言并不要求使用 IEEE-754 标准。因此,当您执行“double d = 0;”(逻辑零)时,内存中占用“d”的一些位可能不为零。 - AnT stands with Russia
1
bool值一样,举个例子。如果你执行bool b = false;(或者等价的bool b = 0;),这并不意味着在物理内存中b会被清零(尽管在实践中通常是这样的)。 - AnT stands with Russia
显示剩余9条评论

62

函数指针。您可以使用函数指针表实现快速间接线程代码解释器(FORTH)或字节码分派程序,或模拟类似面向对象的虚拟方法。

然后标准库中有一些隐藏的宝石,例如qsort(),bsearch(),strpbrk(),strcspn() [后两者用于实现strtok()替代品]。

C语言的一个问题是,有符号算术溢出是未定义的行为(UB)。因此,每当您看到这样的表达式x+y,两个都是有符号整数时,它可能会导致溢出并引起UB。


29
但如果他们对溢出时的行为做了特定规定,那么在不符合该规定的架构上,程序运行速度将会变得非常慢。低运行时开销一直是C语言设计的目标之一,这意味着很多像这样的事情都是未定义的。 - Mark Baker
9
我非常清楚为什么溢出会导致未定义行为。但这仍然是一个设计缺陷,因为标准应该至少提供库例程,可以测试算术溢出(所有基本操作)而不会导致未定义行为。 - zvrba
2
如果您添加了“库例程,可以测试所有基本操作的算术溢出”,则任何整数算术操作都会产生显著的性能损失,@zvrba。 ===== 案例研究Matlab特别添加了控制整数溢出行为到包装或饱和的功能。每当溢出发生时,它还会抛出异常==> Matlab整数操作的性能:非常慢。我的结论是:我认为Matlab是一个引人注目的案例研究,它展示了为什么不希望进行整数溢出检查。 - Trevor Boyd Smith
15
我认为该标准应该提供支持来检查算术溢出。如果你从未使用这个库函数,那么它如何会对性能产生影响?请注意,我的翻译尽可能保留原意,同时使其更通俗易懂。 - zvrba
5
GCC没有标志来捕获有符号整数溢出并抛出运行时异常,这是一个很大的负面因素。虽然x86有用于检测这些情况的标志,但GCC并未使用它们。拥有这样的标志将使非性能关键(特别是遗留)应用程序获得安全好处,而无需进行代码审查和重构。 - Andrew Keeton
显示剩余4条评论

52

多字符常量:

int x = 'ABCD';

这将把x设置为0x41424344(或0x44434241,具体取决于架构)。

编辑:此技术不具有可移植性,特别是如果您对int进行序列化。但是,它可以非常有用地创建自文档化的枚举。例如:

enum state {
    stopped = 'STOP',
    running = 'RUN!',
    waiting = 'WAIT',
};

如果您查看原始内存转储并需要确定枚举值的价值而无需查找它,则这样做会更加简单。


如果有人尝试这样做,请在“WAIT”后面删除逗号。 - blak3r
8
“不具可移植性”的评论完全漏掉了重点。这就像因为INT_MAX“不具可移植性”而批评一个程序一样 :) 这个特性的可移植性已经够用了。多字符常量是一个非常有用的特性,提供了一种可读的方式生成唯一的整数ID。 - AnT stands with Russia
1
@Chris Lutz - 我非常确定尾随逗号一直延续到K&R。它在第二版(1988年)中有描述。 - Ferruccio
1
@Ferruccio:你可能在考虑聚合初始化器列表中的尾逗号。至于枚举声明中的尾逗号-它是一个近期的添加,C99。 - AnT stands with Russia
3
你忘了加上“死机”或者“蓝屏” :-) - JBRWilkinson
显示剩余9条评论

44

我从未使用过位域,但它们听起来很酷,适用于超低级别的东西。

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) 一样小。


感谢 Aaronleppie 提供的评论。


5
位域不具备可移植性——编译器可以自由选择在您的示例中将 legs 分配给最高的 3 个位还是最低的 3 个位。 - zvrba
3
位域是一个例子,标准在实现上给予了太多的自由度,以至于在实践中它们几乎没用。如果你关心一个值占用多少位和如何存储它,则最好使用位掩码。 - Mark Bessey
26
只要将位域视为结构元素而非“整数的部分”,它们确实是可移植的。在具有有限内存的嵌入式系统中,大小比位置更重要,因为每个位都很珍贵。但是,大多数今天的程序员太年轻,记不得这一点。 :-) - Adam Liss
5
如果您在嵌入式系统(或其他地方)中依赖于位域在其字节中的位置,则位置可能很重要。使用掩码可以消除任何歧义。同样适用于联合体。 - Steve Melnikoff
@ComSubVie:从这个角度来看,没有什么是真正可移植的 :) - AnT stands with Russia
显示剩余3条评论

37

类似邓夫设备的交错结构:

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);
    }
}

29
@ComSubVie,任何使用Duff's Device的人都是一个看到Duff's Device并认为如果使用Duff's Device他们的代码将显得很1337的脚本小子。(1.)在现代处理器上,Duff's Device不会提供任何性能提升,因为现代处理器具有零开销循环。换句话说,它是过时的代码。(2.)即使您的处理器不提供零开销循环,它也可能有像SSE/altivec/向量处理这样的东西,当您使用memcpy()时,它将使您的Duff's Device相形见绌。(3.)我提到过除了执行memcpy()之外,Duff's并不实用吗? - Trevor Boyd Smith
2
@ComSubVie,请见识一下我的“死亡之拳”(http://en.wikipedia.org/wiki/Alice_(Dilbert_character)#Alice.27s_violent_nature) - Trevor Boyd Smith
12
@Trevor: 那么只有脚本小子才会编写8051和PIC微控制器,对吗? - SF.
6
@Trevor Boyd Smith:虽然Duff的设备看起来已经过时了,但它仍然是一个历史上的奇迹,这证实了ComSubVie的答案。无论如何,引用维基百科的话:“当XFree86 Server版本4.0中删除了许多Duff设备的实例时,性能明显提高了。”… - paercebal
2
在Symbian上,我们曾经评估了各种循环以进行快速像素编码;汇编中的Duff设备是最快的。因此,在今天的智能手机上的主流ARM核心上仍然具有相关性。 - Will
显示剩余5条评论

37

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

总的来说,我不喜欢那些虽然聪明但会使代码变得不必要复杂难懂的技巧(比如交换示例),而我喜欢那些可以让代码更清晰、直接表达意图的技巧(比如有限状态机示例)。


18
C语言支持链式操作,因此可以使用 a ^= b ^= a ^= b; 这样的写法。 - OJ.
4
严格来说,状态示例是预处理器的一个标记,而不是C语言本身——可以在没有C语言的情况下使用前者。 - Greg Whitfield
15
OJ:实际上,你所建议的做法由于序列点规则而是未定义行为。虽然大多数编译器可能能够运行,但这种做法是不正确和不可移植的。 - Evan Teran
5
在空闲寄存器的情况下,Xor交换实际上可能不是最高效的。任何一个好的优化器都会把临时变量分配到一个寄存器中。根据实现(和对并行支持的需求),交换可能会使用真正的内存而不是寄存器(这两者将是相同的)。 - Paul de Vrieze
27
请永远不要真的这样做:http://en.wikipedia.org/wiki/Xor_swap#Reasons_for_avoidance_in_practice - Christian Oudard
显示剩余5条评论

33

我非常喜欢在C99中添加的指定初始化程序(并且长时间支持gcc):

#define FOO 16
#define BAR 3

myStructType_t myStuff[] = {
    [FOO] = { foo1, foo2, foo3 },
    [BAR] = { bar1, bar2, bar3 },
    ...

数组初始化不再依赖位置。如果您更改FOO或BAR的值,则数组初始化将自动对应其新值。


gcc支持的语法已经有很长时间了,但它与标准C99语法不同。 - Mark Baker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接