C的隐藏功能

时间:2020-03-06 14:42:37  来源:igfitidea点击:

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

解决方案

我一直以来最喜欢使用INT(3)在代码处设置断点

好吧...我认为C语言的强项之一是它的可移植性和标准性,因此,每当我在当前使用的实现中发现一些"隐藏技巧"时,我都尽量不要使用它,因为我会尽量保持C代码尽可能标准且可移植。

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

早期版本的gcc尝试在源代码中遇到" #pragma"时运行游戏。另请参阅此处。

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示例) 。

C编译器实现了几种标准之一。但是,拥有标准并不意味着已经定义了语言的所有方面。例如,达夫(Duff)的设备已成为最受欢迎的"隐藏"功能,以至于现代编译器都具有特殊目的的识别代码,以确保优化技术不会掩盖这种常用模式的预期效果。

通常,当我们在编译器使用的任何C标准的剃刀边缘上运行时,都不建议使用隐藏功能或者语言技巧。从一个编译器到另一个编译器,许多这样的技巧不起作用,并且从给定制造商的一个编译器套件版本到另一个版本,这些功能通常会失效。

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

  • 依靠编译器如何在内存中布置结构。
  • 整数/浮点数的字节序的假设。
  • 功能ABI的假设。
  • 关于堆叠框架增长方向的假设。
  • 关于语句内执行顺序的假设。
  • 关于函数参数中语句执行顺序的假设。
  • 假设short,int,long,float和double类型的位大小或者精度。

每当程序员对所有C标准中都指定为"依赖于编译器"的行为的执行模型进行假设时,就会出现其他问题。

奇怪的向量索引:

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

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

它甚至可以用来实例化链接列表...

函数指针。我们可以使用函数指针表来实现例如快速间接线程代码解释器(FORTH)或者字节码分派器,或者模拟类似于OO的虚拟方法。

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

C的一个缺点是带符号的算术溢出是未定义的行为(UB)。因此,每当看到x + y之类的表达式都为有符号整数时,它可能会溢出并导致UB。

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

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

谢谢大家对Aaron和leppie的评论。

多字符常量:

int x = 'ABCD';

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

编辑:这种技术是不可移植的,特别是如果我们将int序列化的话。
但是,创建自记录枚举可能非常有用。例如

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

如果我们正在查看原始内存转储并且需要确定枚举的值而不必查找它,则这将变得更加简单。

我曾经在一些代码中展示过它,并询问它做了什么:

hexDigit = "0123456789abcdef"[someNybble];

另一个喜欢的是:

unsigned char bar[100];
unsigned char *foo = bar;
unsigned char blah = 42[foo];

这并不是真正的隐藏功能,但是在我第一次看到这样的东西时,它对我来说就像伏都教一样:

void callback(const char *msg, void *data)
{
    // do something with msg, e.g.
    printf("%s\n", msg);

    return;
    data = NULL;
}

这种构造的原因是,如果使用-Wextra而不使用" data = NULL;"行进行编译,则gcc会发出有关未使用参数的警告。但是,由于这条无用的行,我们不会收到警告。

编辑:我知道还有其他(更好)的方法来防止这些警告。当我第一次看到这个时,对我来说看起来很奇怪。

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

我非常喜欢在C99中添加的指定初始值设定项(并在gcc中长期支持):

#define FOO 16
#define BAR 3

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

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

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

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

我们最终将在堆栈上保留一个缓冲区,以容纳固定大小的协议标头和可变大小的数据。使用alloca()可以获得相同的效果,但是此语法更紧凑。

在调用此例程之前,必须确保extraPadding是一个合理的值,否则最终会破坏堆栈。在调用malloc或者任何其他内存分配技术之前,我们必须精打细算检查参数,因此这并非罕见。

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

每个语句都会被求值,但是表达式的值将是最后一个语句的值。

通过使用异常类型转换进行类型转换。尽管不是隐藏功能,但它非常棘手。

例子:

如果我们需要了解编译器如何存储浮点数,请尝试以下操作:

uint32_t Int;
float flt = 10.5; // say

Int = *(uint32_t *)&flt;

printf ("Float 10.5 is stored internally as %8X\n", Int);

或者

float flt = 10.5; // say

printf ("Float 10.5 is stored internally as %8X\n", *(uint32_t *)&flt);

注意类型转换的巧妙用法。将变量(此处为&flt)的地址转换为所需类型(此处为(uint32_t ))并提取其内容(应用'')。

这同样适用于表达的另一面:

*(float *)&Int = flt;

这也可以使用union实现:

typedef union
{
  uint32_t Int;
  float    flt;

} FloatInt_type;

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

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

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

// can now refer to buff->buffer[0..99].

也是offsetof宏,它现在在ANSI C中,但是我第一次看到它时是一个巫术。基本上,它使用地址运算符(&)将重指针作为结构变量进行重铸。

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

在此处查看第3.4.2节。可以导致很多令人讨厌的漏洞。

gcc有许多我喜欢的C语言扩展,可以在这里找到。我最喜欢的一些是函数属性。一个非常有用的示例是format属性。如果我们定义一个采用printf格式字符串的自定义函数,则可以使用此方法。如果启用此功能属性,则gcc会检查参数,以确保格式字符串和参数匹配,并会酌情生成警告或者错误。

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

注册变量

我曾经用register关键字声明一些变量来帮助加快速度。这将提示C编译器将CPU寄存器用作本地存储。由于现代的C编译器会自动执行此操作,因此很有可能不再需要此操作。

将结构初始化为零

struct mystruct a = {0};

这将所有结构元素归零。

结构分配很酷。很多人似乎都没有意识到结构也是值,并且可以在周围进行赋值,而简单的赋值就可以避免使用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编写非常干净且面向对象的代码。

好吧,我从未使用过它,并且不确定是否会推荐给任何人,但是我觉得如果不提西蒙·塔瑟姆(Simon Tatham)的日常套路,这个问题是不完整的。

编译时断言,如此处已讨论。

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

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

#define ERR(name, fmt, ...)   fprintf(stderr, "ERROR " #name ": " fmt "\n", \
                                  __VAR_ARGS__)

就像

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

在这里,我还使用了stringize运算符和字符串常量连接,这是我非常喜欢的其他功能。

摘抄:

In this page, you will find a list of
  interesting C programming
  questions/puzzles, These programs
  listed are the ones which I have
  received as e-mail forwards from my
  friends, a few I read in some books, a
  few from the internet, and a few from
  my coding experiences in C.

http://www.gowrikumar.com/c/index.html

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

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

enum foo { bar, baz, boom, };

这样做是为了使我们在自动生成代码时不必担心消除最后一个逗号。

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

假设我们有一个成员类型相同的结构:

struct Point {
    float x;
    float y;
    float z;
};

我们可以将其实例转换为浮点指针并使用数组索引:

Point a;
int sum = 0, i = 0;
for( ; i < 3; i++)
    sum += ((float*)a)[i];

很基础,但是在编写简洁代码时很有用。

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

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

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

`

可变大小的结构,在其他地方的常见解析器库中都可以看到。

struct foo
{
  int a;
  int b;
  char b[1]; // using [0] is no longer correct
             // must come at end
};

char *str = "abcdef";
int len = strlen(str);
struct foo *bar = malloc(sizeof(foo) + len);

strcpy(bar.b, str); // try and stop me!

这是gcc中的三个好方法:

__FILE__ 
__FUNCTION__
__LINE__

包装malloc并重新分配,如下所示:

#ifdef _DEBUG
#define mmalloc(bytes)                  malloc(bytes);printf("malloc: %d\t<%s@%d>\n", bytes, __FILE__, __LINE__);
#define mrealloc(pointer, bytes)        realloc(pointer, bytes);printf("realloc: %d\t<%s@%d>\n", bytes, __FILE__, __LINE__);
#else //_DEBUG
#define mmalloc(bytes)                  malloc(bytes)
#define mrealloc(pointer, bytes)        realloc(pointer, bytes)

实际上,这是我的全部砷(BailIfNot用于OO c):

#ifdef _DEBUG
#define mmalloc(bytes)                  malloc(bytes);printf("malloc: %d\t<%s@%d>\n", bytes, __FILE__, __LINE__);
#define mrealloc(pointer, bytes)        realloc(pointer, bytes);printf("realloc: %d\t<%s@%d>\n", bytes, __FILE__, __LINE__);
#define BAILIFNOT(Node, Check)  if(Node->type != Check) return 0;
#define NULLCHECK(var)          if(var == NULL) setError(__FILE__, __LINE__, "Null exception", " var ", FATAL);
#define ASSERT(n)               if( ! ( n ) ) { printf("<ASSERT FAILURE@%s:%d>", __FILE__, __LINE__); fflush(0); __asm("int 
malloc: 12      <hash.c@298>
trace: nodeCreate <hash.c@302>
malloc: 5       <hash.c@308>
malloc: 16      <hash.c@316>
malloc: 256     <hash.c@320>
trace: dataLoadHead <hash.c@441>
malloc: 270     <hash.c@463>
malloc: 262144  <hash.c@467>
trace: dataLoadRecursive <hash.c@404>
x3"); } #define TRACE(n) printf("trace: %s <%s@%d>\n", n, __FILE__, __LINE__);fflush(0); #else //_DEBUG #define mmalloc(bytes) malloc(bytes) #define mrealloc(pointer, bytes) realloc(pointer, bytes) #define BAILIFNOT(Node, Check) {} #define NULLCHECK(var) {} #define ASSERT(n) {} #define TRACE(n) {} #endif //_DEBUG

这是一些示例输出:

#define C_copy(to, from) to->copy(to, from)

#define true 1
#define false 0
#define C_OO_PROTOTYPE(type)\
void type##_init (struct type##_struct *my);\
void type##_dispose (struct type##_struct *my);\
char type##_equal (struct type##_struct *my, struct type##_struct *yours); \
struct type##_struct * type##_copy (struct type##_struct *my, struct type##_struct *from); \
const type type##__prototype = {type##_init, type##_dispose, type##_equal, type##_copy

#define C_OO_OVERHEAD(type)\
        void (*init) (struct type##_struct *my);\
        void (*dispose) (struct type##_struct *my);\
        char (*equal) (struct type##_struct *my, struct type##_struct *yours); \
        struct type##_struct *(*copy) (struct type##_struct *my, struct type##_struct *from); 

#define C_OO_IN(ret, type, function, ...)       ret (* function ) (struct type##_struct *my, __VA_ARGS__);
#define C_OO_OUT(ret, type, function, ...)      ret type##_##function (struct type##_struct *my, __VA_ARGS__);

#define C_OO_PNEW(type, instance)\
        instance = ( type *) malloc(sizeof( type ));\
        memcpy(instance, & type##__prototype, sizeof( type ));

#define C_OO_NEW(type, instance)\
        type instance;\
        memcpy(&instance, & type ## __prototype, sizeof(type));

#define C_OO_DELETE(instance)\
        instance->dispose(instance);\
        free(instance);

#define C_OO_INIT(type)         void type##_init (struct type##_struct *my){return;}
#define C_OO_DISPOSE(type)      void type##_dispose (struct type##_struct *my){return;}
#define C_OO_EQUAL(type)        char type##_equal (struct type##_struct *my, struct type##_struct *yours){return 0;}
#define C_OO_COPY(type)         struct type##_struct * type##_copy (struct type##_struct *my, struct type##_struct *from){return 0;}

我刚刚读了这篇文章。它具有一些C语言和其他几种语言的"隐藏功能"。

面向对象的C宏:
我们需要一个构造函数(init),一个析构函数(dispose),一个等号(equal),一个复印机(copy)和一个用于实例化的原型(原型)。

使用该声明,我们需要声明一个常量原型以进行复制和派生。然后,我们可以执行" C_OO_NEW"。如果需要,我可以发布更多示例。 LibPurple是带有回调系统的大型面向对象的C代码库(如果我们想看到一个正在使用的系统)

typeof(foo) copy_of_foo; //declare bar to be a variable of the same type as foo
copy_of_foo = foo; //now copy_of_foo has a backup of foo, for any type

我喜欢typeof()运算符。它像sizeof()一样工作,因为它是在编译时解析的。它不返回字节数,而是返回类型。当我们需要声明一个变量与其他变量的类型相同时,这很有用。

#include <stdio.h>

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

我不确定这可能只是gcc扩展。

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

struct SomeStruct
{
   unsigned a : 5;
   unsigned b : 1;
   unsigned c : 7;
};

*字符可达到此效果。

为了清除输入缓冲区,我们不能使用fflush(stdin)。正确的方法如下:scanf("%* [^ \ n]%* c")
这将丢弃输入缓冲区中的所有内容。

使用NaN进行链式计算/错误返回:

//#include <stdint.h>
静态uint64_t iNaN = 0xFFF8000000000000;
const double NaN = *(double *)&iNaN; //安静的NaN

内部函数可以将NaN作为错误标志返回:它可以安全地用于任何计算中,并且结果始终为NaN。

注意:测试NaN是棘手的,因为NaN!= NaN ...使用isnan(x)或者自己滚动。
如果x为NaN,x!= x在数学上是正确的,但某些编译器倾向于对其进行优化

我经过15年以上的C编程才发现了这一点:

xxxc cccc ccba aaaa

位域!冒号后的数字是成员所需的位数,成员打包成指定的类型,因此如果unsigned为16位,则上面的内容类似于以下内容:

#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 lambda(return_type, function_body) \
    ({ return_type fn function_body fn })

intptr_t用于声明指针类型的变量。特定于C99并在stdint.h中声明

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

lambda (int, (int x, int y) { return x > y; })(1, 2)

可以用作:

({ int fn (int x, int y) { return x > y } fn; })(1, 2)

扩展为:

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

常量字符串串联

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

我目前的程式码中有用例:
我在配置文件中有一个#define PATH" / some / path /"`(实际上是由makefile设置的)。现在,我想构建完整的路径,包括打开资源的文件名。它只是去:

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

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

sscanf ( string, "%d%n", &number, &length );
string += length;

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

  • 长三倍
  • 不太容易阅读
  • 慢得多
  • 设置为任意缓冲区大小限制时,它的功能不那么强大(但是我们将不得不使用更长的代码来避免这种情况,而不会受到恒定的字符串污染)。
  • 使用更多的堆栈空间

我喜欢__LINE____FILE__。参见此处:http://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html

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

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

显然,我们无法添加其他答案,因此我将在此处添加第二个答案,我们可以使用" &&"和" ||"作为条件:

Hi
ROFL

此代码将输出:

struct {
  int    a:3;
  int    b:2;
  int     :0;
  int    c:4;
  int    d:3;
};

我最近发现了0个位域。

000aaabb 0ccccddd

这将给出一个布局

0000aaab bccccddd

而不是没有:0;

##代码##

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