C的隐藏功能

ber*_*rdn 141 c hidden-features

我知道所有C编译器实现背后都有一个标准,所以应该没有隐藏的功能.尽管如此,我确信所有C开发人员都有他们一直使用的隐藏/秘密技巧.

ton*_*ylo 116

更多GCC编译器的技巧,但您可以给编译器提供分支指示提示(在Linux内核中很常见)

#define likely(x)       __builtin_expect((x),1)
#define unlikely(x)     __builtin_expect((x),0)
Run Code Online (Sandbox Code Playgroud)

见:http://kerneltrap.org/node/4705

我喜欢这个,它也增加了一些功能的表现力.

void foo(int arg)
{
     if (unlikely(arg == 0)) {
           do_this();
           return;
     }
     do_that();
     ...
}
Run Code Online (Sandbox Code Playgroud)

  • 这个技巧很酷...... :)特别是你定义的宏.:) (2认同)

Ben*_*ins 77

int8_t
int16_t
int32_t
uint8_t
uint16_t
uint32_t
Run Code Online (Sandbox Code Playgroud)

这些是标准中的可选项,但它必须是隐藏的功能,因为人们不断重新定义它们.我曾经使用过的一个代码库(目前仍在使用)具有多个重定义,所有重定义都具有不同的标识符.大部分时间都是使用预处理器宏:

#define INT16 short
#define INT32  long
Run Code Online (Sandbox Code Playgroud)

等等.这让我想把头发拉出来. 只需使用怪异的标准整数typedef!

  • stdint.h在C99中不是可选的,但遵循C99标准显然适用于某些供应商(*cough*Microsoft). (10认同)
  • @Pete,如果你想成为肛门:(1)此主题与任何Microsoft产品无关.(2)这个主题从来没有与C++有任何关系.(3)没有C++ 97这样的东西. (5认同)
  • 看看http://www.azillionmonkeys.com/qed/pstdint.h - 一个接近便携式的stdint.h (5认同)
  • 我认为他们是C99左右.我没有找到一种可移植的方式来确保它们可以存在. (3认同)
  • 它们是C99的可选部分,但我知道没有编译器供应商没有实现它. (3认同)
  • 据我所知,由于对C99特定功能的高要求,Visual Studio 2010现在具有stdint.h.这是一个皇家PITA,它不包括在那之前. (3认同)

Ben*_*ins 73

逗号运算符未被广泛使用.它当然可以被滥用,但它也非常有用.这种用法是最常见的:

for (int i=0; i<10; i++, doSomethingElse())
{
  /* whatever */
}
Run Code Online (Sandbox Code Playgroud)

但您可以在任何地方使用此运算符.注意:

int j = (printf("Assigning variable j\n"), getValueFromSomewhere());
Run Code Online (Sandbox Code Playgroud)

将评估每个语句,但表达式的值将是最后一个语句的值.

  • 在C++中,你甚至可以超载它. (11认同)
  • 在20年的CI从未见过! (7认同)
  • 当然可以!=!重载它的危险在于内置应用于已经存在的所有内容,包括void,因此由于缺少可用的重载而永远不会编译.即,给程序员很多绳索. (6认同)
  • @Aif:循环内部的`int`将*在C99中工作. (3认同)

mik*_*511 63

将结构初始化为零

struct mystruct a = {0};
Run Code Online (Sandbox Code Playgroud)

这将使所有结构元素归零.

  • @Andrew:`memset` /`calloc` do"all bytes zero"(即物理零),实际上并没有为所有类型定义.保证`{0}`用正确的*逻辑*零值来****.例如,指针可以保证获得正确的空值,即使给定平台上的空值是"0xBAADFOOD". (6认同)
  • @nvl:嗯,实际上差异往往只是概念上的.但理论上,几乎任何类型都可以拥有它.例如,`double`.通常它是根据IEEE-754标准实现的,其中逻辑0和物理零是相同的.但该语言不要求IEEE-754.因此,当你执行`double d = 0;`(逻辑0)时,可能会发生``d`占用的内存中的某些位不会为零. (3认同)
  • 但是,它不会填充填充(如果有的话). (2认同)
  • @simonn,如果结构包含非整数类型,则不会执行未定义的行为.当你解释float/double时,浮点数/双精度值的内存上的0的memset仍然为零(浮点数/双精度就像故意设计的那样). (2认同)

zvr*_*rba 62

函数指针.您可以使用函数指针表来实现,例如,快速间接线程代码解释器(FORTH)或字节码调度程序,或模拟类似OO的虚拟方法.

然后标准库中有隐藏的宝石,例如qsort(),bsearch(),strpbrk(),strcspn()[后两者对于实现strtok()替换很有用].

C的错误特征是带符号的算术溢出是未定义的行为(UB).因此,每当您看到一个表达式(如x + y)都是有符号的整数时,它可能会溢出并导致UB.

  • 但是如果他们在溢出时指定了行为,那么在架构上它会使它非常慢,这不是正常的行为.非常低的运行时开销一直是C的设计目标,这意味着很多这样的事情都是未定义的. (29认同)
  • 我说标准应该提供*library*支持来检查算术溢出.现在,如果你从未使用它,图书馆例程怎么会受到性能影响呢? (15认同)
  • 我非常清楚_why_溢出是UB.它仍然是一个错误,因为标准应该至少提供库程序,可以测试(不是所有基本操作)算术溢出而不会导致UB. (9认同)
  • 一个很大的负面因素是GCC没有标志来捕获有符号整数溢出并抛出运行时异常.虽然有x86标志用于检测此类情况,但GCC不使用它们.拥有这样的标志将允许非性能关键(特别是遗留)应用程序的安全性的好处,最小化到无代码审查和重构. (5认同)
  • @zvrba,"可以测试算术溢出(所有基本操作)的库例程"如果你已经添加了这个,那么你将会因任何整数算术运算而遭受重大的性能损失.=====案例研究Matlab专门将控制整数溢出行为的特性添加到包装或饱和.并且每当发生溢出时它也会抛出异常==> Matlab整数运算的性能:非常慢.我自己的结论:我认为Matlab是一个引人注目的案例研究,它说明了为什么你不想要整数溢出检查. (2认同)

Fer*_*cio 52

多字符常量:

int x = 'ABCD';
Run Code Online (Sandbox Code Playgroud)

这设置x0x41424344(或0x44434241取决于架构).

编辑:这种技术不可移植,特别是如果你序列化int.但是,创建自记录枚举功能非常有用.例如

enum state {
    stopped = 'STOP',
    running = 'RUN!',
    waiting = 'WAIT',
};
Run Code Online (Sandbox Code Playgroud)

如果你正在查看原始内存转储并且需要确定枚举的值而不必查找它,这会使它变得更加简单.

  • "不便携"的评论完全忽略了这一点.这就像批评程序使用INT_MAX只是因为INT_MAX"不可移植":)这个功能就像它需要的那样便携.Multi-char常量是一个非常有用的功能,它提供了生成唯一整数ID的可读方法. (8认同)
  • 你忘了'HANG'或'BSOD':-) (3认同)

Mot*_*tti 44

我从来没有使用过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;
}
Run Code Online (Sandbox Code Playgroud)

这意味着sizeof(cat)可以小到sizeof(char).


感谢Aaronleppie的评论,谢谢你们.

  • Bitfields确实是可移植的,只要你将它们视为结构元素,而不是"整数块".尺寸,而不是位置,在内存有限的嵌入式系统中很重要,因为每个位都很珍贵......但是今天的大多数编码器都太年轻而不记得了.:-) (26认同)
  • 位域不可移植 - 在您的示例中,编译器可以自由选择是否将腿分配最高3位或最低3位. (5认同)
  • @Adam:如果依赖于其字节中位域的位置,位置在嵌入式系统(或其他地方)中可能很重要.使用掩码消除了任何歧义.工会也是如此. (5认同)
  • Bitfields是一个例子,标准为实现提供了如此多的自由度,实际上,它们几乎无用.如果你关心一个值占用了多少位,以及它是如何存储的,那么你最好使用位掩码. (3认同)

Com*_*Vie 37

交错结构,如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);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @ComSubVie,任何使用Duff设备的人都是一个看过Duff设备的脚本小伙子,如果他们使用Duff的设备,他们认为他们的代码看起来是1337.(1.)Duff的设备在现代处理器上没有提供任何性能提升,因为现代处理器具有零开销循环.换句话说,它是一段过时的代码.(2.)即使你的处理器没有提供零开销循环,它也可能会有类似SSE/altivec/vector-processing的东西,这会让你的Duff设备在使用memcpy()时感到羞耻.(3.)我是否提到其他做memcpy()duff的用途没用? (29认同)
  • @Trevor:所以只有脚本小子程序8051和PIC微控制器,对吧? (12认同)
  • @Trevor Boyd Smith:虽然Duff的设备看起来已经过时,但它仍然是一个历史的好奇心,它验证了ComSubVie的答案.无论如何,引用维基百科:*"当版本4.0中从XFree86服务器中删除了大量Duff设备实例时,性能有了显着提高."*... (6认同)
  • @ComSubVie,请见我的死亡之拳(http://en.wikipedia.org/wiki/Alice_(Dilbert_character)#Alice.27s_violent_nature) (2认同)
  • 在Symbian上,我们曾经评估了各种用于快速像素编码的循环; duff的设备,在汇编程序中,是最快的.因此,它仍然与当今智能手机上的主流ARM内核相关. (2认同)

Rem*_*o.D 37

C有一个标准,但不是所有的C编译器都完全兼容(我还没有看到任何完全兼容的C99编译器!).

也就是说,我更喜欢的技巧是那些非常明显且可以跨平台移植的技巧,因为它们依赖于C语义.它们通常是关于宏或位算术的.

例如:在不使用临时变量的情况下交换两个无符号整数:

...
a ^= b ; b ^= a; a ^=b;
...
Run Code Online (Sandbox Code Playgroud)

或"扩展C"代表有限状态机,如:

FSM {
  STATE(x) {
    ...
    NEXTSTATE(y);
  }

  STATE(y) {
    ...
    if (x == 0) 
      NEXTSTATE(y);
    else 
      NEXTSTATE(x);
  }
}
Run Code Online (Sandbox Code Playgroud)

可以使用以下宏实现:

#define FSM
#define STATE(x)      s_##x :
#define NEXTSTATE(x)  goto s_##x
Run Code Online (Sandbox Code Playgroud)

但总的来说,我不喜欢聪明的技巧,但是让代码不必要地复杂化(作为交换示例),我喜欢使代码更清晰并直接传达意图的代码(如FSM示例) .

  • 请不要真的这样做:http://en.wikipedia.org/wiki/Xor_swap#Reasons_for_avoidance_in_practice (27认同)
  • C支持链接,所以你可以做一个^ = b ^ = a ^ = b; (18认同)
  • OJ:实际上你建议的是因为序列点规则而未定义的行为.它可能适用于大多数编译器,但不正确或可移植. (15认同)
  • 在免费注册的情况下,Xor交换实际上可能效率较低.任何体面的优化器都会使temp变量成为寄存器.根据实现(以及并行支持的需要),交换实际上可能使用实际内存而不是寄存器(这将是相同的). (5认同)
  • 严格来说,状态示例是预处理器的刻度,而不是C语言 - 可以使用前者而不使用后者. (4认同)
  • 使用XOR交换通常是一个坏主意,主要是因为它只适用于整数和混叠可能是一个大问题.我很确定大多数编译器在内存成为问题时对其进行优化. (2认同)
  • @OJ:不,这是未定义的行为. (2认同)
  • 如果'a'是'b'的别名,则XOR交换失败 (2认同)
  • OJ:我曾经和GCC一起做过......直到我尝试将一个小例子编译成MIPS并且失去了很多时间调试,因为它编译的方式有所不同.正如他们在这里所说:它是未定义的. (2认同)

DGe*_*try 33

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

#define FOO 16
#define BAR 3

myStructType_t myStuff[] = {
    [FOO] = { foo1, foo2, foo3 },
    [BAR] = { bar1, bar2, bar3 },
    ...
Run Code Online (Sandbox Code Playgroud)

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


小智 28

C99有一些很棒的任意顺序结构初始化.

struct foo{
  int x;
  int y;
  char* name;
};

void main(){
  struct foo f = { .y = 23, .name = "awesome", .x = -38 };
}
Run Code Online (Sandbox Code Playgroud)


Pyp*_*ros 27

匿名结构和数组是我最喜欢的.(参见http://www.run.montefiore.ulg.ac.be/~martin/resources/kung-f00.html)

setsockopt(yourSocket, SOL_SOCKET, SO_REUSEADDR, (int[]){1}, sizeof(int));
Run Code Online (Sandbox Code Playgroud)

要么

void myFunction(type* values) {
    while(*values) x=*values++;
}
myFunction((type[]){val1,val2,val3,val4,0});
Run Code Online (Sandbox Code Playgroud)

它甚至可以用来实现链表...

  • 此功能通常称为"复合文字".匿名(或未命名)结构指定没有成员名称的嵌套结构. (3认同)

Gia*_*sti 24

好吧......我认为C语言的一个优点是它的可移植性和标准性,所以每当我在我正在使用的实现中找到一些"隐藏的技巧"时,我尽量不使用它,因为我试图保持我的C代码尽可能标准和便携.

  • @Joe D如果它是一个跨平台项目,如Windows/OSX/Linux,可能有点,还有不同的拱门,如x86 vs x86_64等... (3认同)

小智 24

gcc有很多我喜欢的C语言扩展,可以在这里找到.我最喜欢的一些是功能属性.一个非常有用的例子是format属性.如果您定义一个采用printf格式字符串的自定义函数,则可以使用此方法.如果启用此函数属性,gcc将对您的参数进行检查,以确保您的格式字符串和参数匹配,并在适当时生成警告或错误.

int my_printf (void *my_object, const char *my_format, ...)
            __attribute__ ((format (printf, 2, 3)));
Run Code Online (Sandbox Code Playgroud)


kol*_*vra 24

我第一次见到时"震惊"我的(隐藏)功能是关于printf.此功能允许您使用变量格式化格式说明符本身.寻找代码,你会看到更好的:

#include <stdio.h>

int main() {
    int a = 3;
    float b = 6.412355;
    printf("%.*f\n",a,b);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

*角色达到了这个效果.


phi*_*ant 19

编译时断言,如此处所述.

//--- 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);
Run Code Online (Sandbox Code Playgroud)


kri*_*iss 16

常量字符串连接

我很惊讶没有在答案中看到它,因为我所知道的所有编译器都支持它,但许多程序员似乎忽略了它.有时它非常方便,而且不仅仅是在编写宏时.

我在当前代码中使用的用例:我#define PATH "/some/path/"在配置文件中有一个(实际上它是由makefile设置的).现在我想构建包含文件名的完整路径来打开资源.它只是去:

fd = open(PATH "/file", flags);
Run Code Online (Sandbox Code Playgroud)

而不是可怕的,但很常见的:

char buffer[256];
snprintf(buffer, 256, "%s/file", PATH);
fd = open(buffer, flags);
Run Code Online (Sandbox Code Playgroud)

请注意,常见的可怕解决方案是:

  • 三倍的时间
  • 更不容易阅读
  • 慢得多
  • 设置为任意缓冲区大小限制的强大功能(但你必须使用更长的代码来避免没有常量字符串连接).
  • 使用更多的堆栈空间


Mar*_*ker 15

好吧,我从来没有用它,而且我不确定我是否会向任何人推荐它,但我觉得这个问题在不提及Simon Tatham的常规技巧的情况下是不完整的.


unw*_*ind 12

结构分配很酷.许多人似乎并没有意识到结构也是值,并且可以被分配,没有必要使用memcpy(),当一个简单的赋值就可以了.

例如,考虑一些虚构的2D图形库,它可能会定义一个表示(整数)屏幕坐标的类型:

typedef struct {
   int x;
   int y;
} Point;
Run Code Online (Sandbox Code Playgroud)

现在,你做的事情可能看起来"错误",比如编写一个创建从函数参数初始化的点的函数,并返回它,如下所示:

Point point_new(int x, int y)
{
  Point p;
  p.x = x;
  p.y = y;
  return p;
}
Run Code Online (Sandbox Code Playgroud)

这是安全的,当然很长(因为)使用struct赋值通过值复制返回值:

Point origin;
origin = point_new(0, 0);
Run Code Online (Sandbox Code Playgroud)

通过这种方式,您可以编写非常干净且面向对象的代码,所有代码都采用简单的标准C.

  • 当然,以这种方式传递大型结构会产生性能影响; 它通常很有用(确实很多人都没有意识到你能做到的事情),但你需要考虑传递指针是否更好. (4认同)

Fer*_*cio 12

初始化数组或枚举时,可以在初始化列表中的最后一项之后放置逗号.例如:

int x[] = { 1, 2, 3, };

enum foo { bar, baz, boom, };
Run Code Online (Sandbox Code Playgroud)

这样做是为了让你自动生成代码,你不必担心删除最后一个逗号.


INS*_*INS 10

奇怪的矢量索引:

int v[100]; int index = 10; 
/* v[index] it's the same thing as index[v] */
Run Code Online (Sandbox Code Playgroud)

  • 请告诉我你实际上并没有使用这个"所有的时间",就像问题一样! (17认同)
  • 当你考虑v [index] ==*(v + index)和index [v] ==*(index + v)时不是很奇怪 (5认同)
  • 它甚至更好...... char c = 2 ["你好"]; (c =='l'之后). (4认同)

Kev*_* S. 9

C编译器实现了几个标准之一.但是,拥有标准并不意味着语言的所有方面都已定义. 例如,Duff的设备是最受欢迎的"隐藏"功能,它已经变得如此受欢迎,以至于现代编译器具有特殊用途的识别代码,以确保优化技术不会破坏这种常用模式的预期效果.

一般情况下,当您在编译器使用的任何C标准的剃刀边缘上运行时,不鼓励隐藏功能或语言技巧.许多这样的技巧从一个编译器到另一个编译器不起作用,并且这些类型的功能通常会从给定制造商的一个版本的编译器套件失败到另一个版本.

破坏C代码的各种技巧包括:

  1. 依赖于编译器如何在内存中布局结构.
  2. 关于整数/浮点数的字节顺序的假设.
  3. 功能ABI的假设.
  4. 关于堆栈帧增长方向的假设.
  5. 关于语句内执行顺序的假设.
  6. 关于函数参数中语句执行顺序的假设.
  7. 关于short,int,long,float和double类型的位大小或精度的假设.

当程序员对大多数C标准中指定为"编译器相关"行为的执行模型做出假设时,会出现其他问题和问题.

  • @Blaisorblade,更好的是,使用编译时断言以一种方式记录您的假设,这将使编译在违反它们的平台上失败. (2认同)

one*_*sse 9

使用sscanf时,您可以使用%n找出您应该继续阅读的位置:

sscanf ( string, "%d%n", &number, &length );
string += length;
Run Code Online (Sandbox Code Playgroud)

显然,你不能添加另一个答案,所以我会在这里添加第二个答案,你可以使用"&&"和"||" 作为条件:

#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 );
}
Run Code Online (Sandbox Code Playgroud)

此代码将输出:

Hi
ROFL


Dro*_*per 8

使用INT(3)在代码中设置断点是我一直以来的最爱

  • 我觉得它不便携.它可以在x86上运行,但其他平台呢? (3认同)
  • 这是一种很好的技术,它是特定于X86的(虽然在其他平台上可能有类似的技术).但是,这不是C的功能.它取决于非标准的C扩展或库调用. (2认同)

Sri*_*yer 8

我最喜欢的C"隐藏"功能是在printf中使用%n来写回堆栈.通常,printf会根据格式字符串从堆栈中弹出参数值,但%n可以将它们写回.

在这里查看3.4.2节.可能会导致很多令人讨厌的漏洞.


Ale*_*own 8

Gcc(c)有一些你可以启用的有趣功能,比如嵌套函数声明,以及?:b形式的?:运算符,如果a不是false,则返回a.


S.C*_*sen 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)
};
Run Code Online (Sandbox Code Playgroud)

  • +1整洁.我曾经使用过Microsoft的CompilerAssert宏,但你的也不错.(`#define CompilerAssert(exp)extern char _CompilerAssert [(exp)?1:-1]`) (2认同)

Pat*_*ter 8

我最近发现了0个位域.

struct {
  int    a:3;
  int    b:2;
  int     :0;
  int    c:4;
  int    d:3;
};
Run Code Online (Sandbox Code Playgroud)

这将给出一个布局

000aaabb 0ccccddd
Run Code Online (Sandbox Code Playgroud)

而不是没有:0;

0000aaab bccccddd
Run Code Online (Sandbox Code Playgroud)

0宽度字段表示应在下一个原子实体(char)上设置以下位域


Ben*_*bee 7

C99风格的可变参数宏,又名

#define ERR(name, fmt, ...)   fprintf(stderr, "ERROR " #name ": " fmt "\n", \
                                  __VAR_ARGS__)
Run Code Online (Sandbox Code Playgroud)

哪个会像

ERR(errCantOpen, "File %s cannot be opened", filename);
Run Code Online (Sandbox Code Playgroud)

在这里我还使用stringize运算符和字符串常量连接,其他功能我真的很喜欢.


DGe*_*try 6

在某些情况下,可变大小的自动变量也很有用.这些是在nC99中添加的,并且已经在gcc中支持了很长时间.

void foo(uint32_t extraPadding) {
    uint8_t commBuffer[sizeof(myProtocol_t) + extraPadding];
Run Code Online (Sandbox Code Playgroud)

最终在堆栈上有一个缓冲区,其中包含固定大小的协议头和可变大小数据的空间.您可以使用alloca()获得相同的效果,但此语法更紧凑.

在调用此例程之前,您必须确保extraPadding是一个合理的值,否则您最终会破坏堆栈.你必须在调用malloc或任何其他内存分配技术之前检查参数是否健全,所以这并不是很不寻常.


pax*_*blo 5

我喜欢你可以制作的可变大小的结构:

typedef struct {
    unsigned int size;
    char buffer[1];
} tSizedBuffer;

tSizedBuffer *buff = (tSizedBuffer*)(malloc(sizeof(tSizedBuffer) + 99));

// can now refer to buff->buffer[0..99].
Run Code Online (Sandbox Code Playgroud)

还有offsetof宏,现在是ANSI C,但是在我第一次看到它的时候是一段魔法.它基本上使用address-of运算符(&)将空指针重铸作为结构变量.


Joe*_*e D 5

GCC中的Lambda(例如匿名函数):

#define lambda(return_type, function_body) \
    ({ return_type fn function_body fn })
Run Code Online (Sandbox Code Playgroud)

这可以用作:

lambda (int, (int x, int y) { return x > y; })(1, 2)
Run Code Online (Sandbox Code Playgroud)

这扩展到:

({ int fn (int x, int y) { return x > y } fn; })(1, 2)
Run Code Online (Sandbox Code Playgroud)