C语言的隐藏特性

141

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


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

10

奇怪的向量索引:

int v[100]; int index = 10; 
/* v[index] it's the same thing as index[v] */

4
这句话的意思是 "这样会更好...... char c = 2["Hello"];",执行后变量 c 的值为 'l'。 - yrp
5
考虑到 v[index] 等同于 *(v + index),以及 index[v] 等同于 *(index + v),所以这并不奇怪。 - Ferruccio
17
请告诉我你并不实际“一直”使用这个,就像问题所问的那样! - Tryke

9
C编译器实现了多个标准之一。然而,拥有标准并不意味着语言的所有方面都已定义。例如Duff's device是一个受欢迎的“隐藏”功能,它变得如此流行,以至于现代编译器具有特殊目的的识别代码,以确保优化技术不会破坏这种经常使用的模式的期望效果。
一般来说,不鼓励使用隐藏功能或语言技巧,因为您正在运行编译器使用的任何C标准的边缘。许多这样的技巧在一个编译器到另一个编译器上不起作用,并且这些类型的功能经常会从给定制造商的编译器套件的一个版本失败到另一个版本。
破坏C代码的各种技巧包括:
  1. 取决于编译器在内存中放置结构体的方式。
  2. 整数/浮点数字节序的假设。
  3. 函数ABIs的假设。
  4. 堆栈帧增长方向的假设。
  5. 语句执行顺序的假设。
  6. 函数参数中语句执行顺序的假设。
  7. 短整型、整型、长整型、浮点型和双精度型的位大小或精度的假设。

其他问题和问题会在程序员对执行模型做出假设时出现,这些行为在大多数C标准中都被指定为“依赖于编译器”的行为。


要解决这些问题中的大多数,使这些假设依赖于您平台的特性,并在各自的标题中描述每个平台。订单执行是一个例外-永远不要依赖它;另一方面,每个平台都需要有可靠的决策。 - Blaisorblade
2
@Blaisorblade,更好的方法是使用编译时断言以记录您的假设,这样在违反它们的平台上编译将失败。 - RBerteig
我认为应该将两者结合起来,这样你的代码就可以在多个平台上运行(这是最初的意图),如果功能宏设置错误,编译时断言会捕获它。我不确定例如函数ABIs的假设是否可以作为编译时断言进行检查,但对于大多数其他(有效)断言来说应该是可能的(除了执行顺序;-))。 - Blaisorblade
函数ABI检查应该由测试套件处理。 - dolmen

9
当使用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 );
}

这段代码将输出:
您好
ROFL

8

Gcc(C语言编译器)有一些有趣的功能可以启用,例如嵌套函数声明和 ?: 运算符的 a?:b 形式,如果 a 不为 false,则返回 a。


8

使用INT(3)在代码中设置断点是我一直以来最喜欢的方法。


3
我认为它不太具有可移植性。虽然在x86平台上可以运行,但其他平台呢? - Cristian Ciupitu
1
我不知道 - 你应该发布一个相关问题。 - Dror Helper
2
这是一种不错的技术,它是X86特定的(虽然其他平台可能有类似的技术)。然而,这不是C语言的一个特性。它依赖于非标准的C扩展或库调用。 - Ferruccio
1
在GCC中有__builtin_trap,在MSVC中有__debugbreak,它们可以在任何支持的架构上工作。 - Axel Gneiting

8

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

请查看第 3.4.2 节这里。可能会导致许多严重的漏洞。


链接已经失效,事实上,该网站似乎不再可用。您能否提供另一个链接? - thequark
@thequark:任何关于“格式字符串漏洞”的文章都会涉及一些信息...(例如http://crypto.stanford.edu/cs155/papers/formatstring-1.2.pdf)...但由于该领域的本质,安全网站本身有点不稳定,真正的学术文章很难获得(带有实现)。 - Sridhar Iyer

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

2
+1 很好。我以前使用过微软的 CompilerAssert 宏,但你的也不错。(#define CompilerAssert(exp) extern char _CompilerAssert[(exp)?1:-1] - Patrick Schlüter
1
我喜欢枚举方法。我之前使用的方法利用了死代码消除:“if (something_bad) {void BLORG_IS_WOOZLED(void); BLORG_IS_WOOZLED();}”,这种方法直到链接时才会出错,但它的优点是可以通过错误消息让程序员知道blorg已经被woozeled。 - supercat

8

我最近发现了0位字段。


这句话是关于IT技术的内容。
struct {
  int    a:3;
  int    b:2;
  int     :0;
  int    c:4;
  int    d:3;
};

这将提供一个布局

000aaabb 0ccccddd

将“:0;”替换为“without”。

0000aaab bccccddd

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

7
C99风格的可变参数宏,又称
#define ERR(name, fmt, ...)   fprintf(stderr, "ERROR " #name ": " fmt "\n", \
                                  __VAR_ARGS__)

这将会像这样使用

ERR(errCantOpen, "File %s cannot be opened", filename);

在这里,我还使用了字符串化操作符和字符串常量连接,其他功能我也非常喜欢。


你在 VA_ARGS 中多了一个 'R'。 - Blaisorblade

6

变量大小的自动变量在某些情况下也很有用。这些特性被添加到C99中,并且在gcc中已经得到了长期支持。

void foo(uint32_t extraPadding) {
    uint8_t commBuffer[sizeof(myProtocol_t) + extraPadding];

你最终会在堆栈上得到一个缓冲区,其中包含可变大小数据的固定大小协议头。使用alloca()可以达到相同的效果,但这种语法更加紧凑。
在调用此例程之前,您必须确保extraPadding是合理的值,否则您将爆掉堆栈。在调用malloc或任何其他内存分配技术之前,您必须对参数进行检查,因此这并不是非常不寻常的事情。

如果目标平台上的字节/字符宽度不完全是8位,这个代码还能正常工作吗?我知道这种情况很少见,但还是要考虑一下... :) - Stephan202

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