C语言的隐藏特性

141

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


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

28

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

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

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


27

匿名结构体和数组是我最喜欢的之一。(参见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});

它甚至可以用于实例化链表...


3
这个特性通常称为“复合字面量”。匿名(或未命名)的结构体指的是没有成员名称的嵌套结构。 - calandoa
根据我的GCC,“ISO C90禁止复合字面量”。 - jmtd
"ISO C99支持复合字面量。" "作为扩展,GCC在C89模式和C++中支持复合字面量"(据gcc信息)。另外,"作为GNU扩展,GCC允许使用复合字面量对具有静态存储期的对象进行初始化(这在ISO C99中是不可能的,因为初始化程序不是常量)"。 - PypeBros

24

嗯……我认为C语言的一个强项是其可移植性和标准化,所以每当我在当前使用的实现中发现一些“隐藏技巧”时,我都尽量不使用它,因为我试图让我的C代码尽可能符合标准且具有可移植性。


但实际上,你有多少次需要使用另一个编译器编译你的代码呢? - Joe D
3
如果这是一个跨平台的项目,例如Windows/OSX/Linux,可能会有些不同,还有不同的架构,如x86 vs x86_64等... - Pharaun
@JoeD 除非你在一个非常狭隘的项目中,愿意嫁给一个编译器供应商,否则很重要。你可能想避免实际上必须切换编译器,但你确实想保持这个选项开放。然而,在嵌入式系统中,你并不总是有选择的余地。AHS,ASS。 - XTL

24

当我第一次看到之后,这个(隐藏的)特性让我"震惊"的是printf。这个特性允许您在格式化格式说明符本身时使用变量。查看代码,您会更清楚:

#include <stdio.h>

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

*字符可以达到这种效果。


24

gcc有许多C语言扩展,我很喜欢,可以在这里找到。其中我的一些最爱是函数属性。其中一个极其有用的例子是格式属性。如果您定义了一个自定义函数,该函数采用printf格式字符串,则可以使用此属性。如果启用此函数属性,gcc将对您的参数进行检查,以确保您的格式字符串和参数匹配,并根据需要生成警告或错误。

int my_printf (void *my_object, const char *my_format, ...)
            __attribute__ ((format (printf, 2, 3)));

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

16

字符串的常量拼接

我很惊讶地发现没有人提到这一点,因为我知道的所有编译器都支持它,但许多程序员似乎忽略了它。有时候它非常方便,不仅用于编写宏。

在我的当前代码中使用案例: 我在配置文件中有一个#define PATH "/some/path/"(实际上是由makefile设置的)。现在我想要构建完整的路径,包括要打开资源的文件名。只需这样做:

fd = open(PATH "/file", flags);

不要用可怕但非常常见的方法:

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

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

  • 长度是原来的三倍
  • 不易于阅读
  • 速度慢得多
  • 在设置任意缓冲区大小限制时不如另一种避免字符串常量拼接的更长代码强大(但您需要使用更长的代码才能避免这种情况)。
  • 使用更多的堆栈空间

1
分割字符串常量并且不使用脏的 \\ 在多个源代码行上也是有用的。 - dolmen

15

嗯,我从未使用过它,也不确定是否会向任何人推荐它,但是我觉得如果没有提到Simon Tatham的协同程序技巧,这个问题就会不完整。


12

初始化数组或枚举时,可以在初始化列表中的最后一项后面加上逗号。例如:

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

enum foo { bar, baz, boom, };

这样做是为了让你在自动生成代码时不需要担心去除最后一个逗号。


在多人开发环境中,这也是很重要的,比如Eric添加了“baz”,然后George添加了“boom”。如果Eric决定在下一个项目构建中撤回他的代码,它仍然可以与George的更改编译。这对于多分支源代码控制和重叠开发时间表非常重要。 - Harold Bamford
枚举可以是C99。数组初始化器和尾随逗号是K&R。 - Ferruccio
据我所知,普通枚举在C89中就已经存在了。至少它们已经存在了很长时间。 - XTL

12

结构体赋值非常方便。很多人似乎没有意识到结构体也是一种值,可以进行赋值操作,不必使用memcpy()函数,简单的赋值语句就能完成任务。

例如,考虑一个想象中的二维图形库,它可能定义了一个类型来表示(整数)屏幕坐标:

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 编写相当干净和面向对象的代码。


4
当然,这种方式传递大型结构体会有性能方面的影响;虽然这种方法经常很有用(而且确实是很多人没有意识到可以这么做的),但你需要考虑是否使用指针传递更好。 - Mark Baker
1
当然,可能会有这种情况。编译器也很可能检测到使用并进行优化。 - unwind
值得注意的是,在 C++ 中,标准特别允许优化掉复制操作(标准必须允许编译器实现它,因为这意味着可能不会调用具有副作用的复制构造函数),而且由于大多数 C++ 编译器也是 C 编译器,您的编译器很可能会进行此优化。 - Joseph Garvin
我修复了最后的“构造函数”调用,它调用了Point(0, 0),这当然是错误的,point_new()才是正确的方法。哎呀。 - unwind
C99结构初始化的设计是为了优化,而此代码则依赖于编译器可能会内联函数和可能会删除副本这一事实。 - dolmen
显示剩余2条评论

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