您最喜欢的C编程技巧是什么?[关闭]


134

例如,我最近在linux内核中遇到了这个问题:

/ *如果条件为真,则强制执行编译错误* /
#定义BUILD_BUG_ON(条件)((无效)sizeof(字符[1-2 * !!(条件)]))

因此,在您的代码中,如果您具有某种必须为8个字节大小的倍数的结构,可能由于某些硬件限制,您可以执行以下操作:

BUILD_BUG_ON((sizeof(struct mystruct)%8)!= 0);

除非struct mystruct的大小是8的倍数,否则它不会编译;如果它是8的倍数,则根本不会生成任何运行时代码。

我知道的另一个技巧是从“ Graphics Gems”一书中获得的,该书允许单个头文件在一个模块中声明和初始化变量,而在其他使用该模块的模块中,仅将其声明为externs。

#ifdef DEFINE_MYHEADER_GLOBALS
#define GLOBAL
#定义INIT(x,y)(x)=(y)
#其他
#define GLOBAL extern
#定义INIT(x,y)
#万一

GLOBAL int INIT(x,0);
GLOBAL int somefunc(int a,int b);

这样,定义x和somefunc的代码可以:

#define DEFINE_MYHEADER_GLOBALS
#include“ the_above_header_file.h”

而仅使用x和somefunc()的代码则执行以下操作:

#include“ the_above_header_file.h”

因此,您将获得一个标头文件,该标头文件声明了需要它们的全局实例和函数原型的实例,以及相应的extern声明。

那么,您最喜欢的C编程技巧是什么?


9
这似乎更像是C预处理技巧。
jmucchiello

关于BUILD_BUG_ON宏,使用#errorinside和#if怎么了?
里卡多

Answers:


80

C99使用匿名数组提供了一些非常酷的东西:

删除无意义的变量

{
    int yes=1;
    setsockopt(yourSocket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
}

变成

setsockopt(yourSocket, SOL_SOCKET, SO_REUSEADDR, (int[]){1}, sizeof(int));

传递可变数量的参数

void func(type* values) {
    while(*values) {
        x = *values++;
        /* do whatever with x */
    }
}

func((type[]){val1,val2,val3,val4,0});

静态链表

int main() {
    struct llist { int a; struct llist* next;};
    #define cons(x,y) (struct llist[]){{x,y}}
    struct llist *list=cons(1, cons(2, cons(3, cons(4, NULL))));
    struct llist *p = list;
    while(p != 0) {
        printf("%d\n", p->a);
        p = p->next;
    }
}

我敢肯定,我还没有想到其他任何很棒的技术。


2
我相信您的第一个示例也可以写成&(int){1},如果您想使它的意图更清晰一点。
莉莉巴拉德

67

在阅读Quake 2源代码时,我想到了以下内容:

double normals[][] = {
  #include "normals.txt"
};

(或多或少,我现在没有方便的代码来检查它)。

从那时起,对预处理器进行创造性使用的新世界在我眼前张开。我不再仅包含标题,而是不时地包含整个代码块(它大大提高了可重用性):-p

谢谢约翰·卡马克!xD


13
您不能在优化线程中说出carmack,而不必提及地震源中的快速逆平方根。en.wikipedia.org/wiki/Fast_inverse_square_root
pg1989 2011年

首先他从哪里获得0x5f3759df?
RSH1 2011年

2
@RoryHarvey:从查找时可以发现,似乎纯粹是经验性的。一些研究(我不记得我在哪里看到过)表明它接近最优,但不是完全最优。同样,对于64位,似乎发现了该值,而不是计算出来的值。
Matthieu M.

50

我喜欢= {0};用来初始化结构而无需调用memset。

struct something X = {0};

这会将结构(或数组)的所有成员初始化为零(但不会填充任何填充字节-如果您也需要将其填充为零,请使用memset)。

但是您应该知道,对于大型动态分配的结构,这存在一些问题


顺便说一下,全局变量不需要。
斯特拉格

5
静态变量不需要。全局变量可能为零,但这不是必需的。
杰米

4
有时const struct something zero_something = { 0 };,我可以将其扩展为:然后,struct something X = zero_something;我可以使用例程或在例程执行过程中途中重置变量,可以使用“ X = zero_something;”。唯一可能的反对意见是,它涉及从某处读取数据。如今,“ memset()”可能会更快-但我喜欢赋值的清晰性,也可以在初始值设定项中使用非零值(和memset(),然后对单个成员进行调整)可能比简单副本要慢)。
乔纳森·勒夫勒

45

如果我们在谈论c技巧,我最喜欢的必须是Duff的用于循环展开的设备!我只是在等待正确的机会,让我真正地在愤怒中使用它...


4
我曾经使用它来产生可衡量的性能提升,但是如今,它在很多硬件上都没有用。一律剖析!
丹·奥尔森,2009年

6
是的,那种不了解上下文的人是由Duff的设备创建的:如果代码不够快,无法正常工作,那么“代码可读性”就没用了。投票否决您的人大概都不需要为硬实时编写代码。
罗布K 2009年

1
+1,我实际上需要使用Duff的设备几次。第一次是一个循环,该循环基本上只是复制内容并在此过程中进行了一些小的转换。它比该体系结构中的简单memcpy()快得多。
Makis

3
愤怒来自于您的同事和继任者,他们必须在您之后维护您的代码。
乔纳森·勒夫勒

1
就像我说的那样,我仍在等待合适的机会-但没有人使我感到烦恼。我从事C语言开发已有25年了,我认为我是90年代初第一次接触Duff的设备,而我还没有使用它。正如其他人所评论的那样,由于编译器在这种优化上变得更好,因此这种技巧现在越来越少了。
杰克逊

42

使用 __FILE____LINE__进行调试

#define WHERE fprintf(stderr,"[LOG]%s:%d\n",__FILE__,__LINE__);

6
在某些编译器上,您也会获得FUNCTION
JBRWilkinson

11
__FUNCTION__只是的别名__func____func__在c99中。非常方便。__PRETTY_FUNCTION__在C(GCC)中,它只是的另一个别名__func__,但在C ++中,它将为您提供完整的功能签名。
sklnd 2011年

FILE 显示文件名的完整路径,所以我使用basename(FILE
Jeegar Patel 2012年


28

有一次,我和我重新定义了伙伴关系,找到了一个棘手的堆栈损坏错误。

就像是:

#define return DoSomeStackCheckStuff, return

4
希望在函数体中定义#define,在末尾#undefined!
斯特拉格2009年

我不太喜欢这一点-我想到的第一件事是DoSomeStackCheckStuff由于某些错误而占用了内存,并且谁在阅读代码的人都没有意识到return的重新定义,并且想知道/ hell /是怎么回事。
gilligan 2011年

8
@strager但这会使它基本上无用。重点是在每个函数调用中添加一些跟踪。否则,您只需添加对DoSomeStackCheckStuff要跟踪的函数的调用即可。
笨拙的

1
@gilligan我不认为这是您一直都启用的功能;一键调试工作似乎非常方便。
sunetos 2011年

这真的有效吗?:)我本来会写的#define return if((DoSomeStackCheckStuff) && 0) ; else return……我想我还是疯了!
Paolo Bonzini 2011年

22

我喜欢具有动态大小的对象的“结构黑客”。该站点也对此进行了很好的解释(尽管它们引用的是C99版本,您可以在其中编写“ str []”作为结构的最后一个成员)。您可以这样创建一个字符串“对象”:

struct X {
    int len;
    char str[1];
};

int n = strlen("hello world");
struct X *string = malloc(sizeof(struct X) + n);
strcpy(string->str, "hello world");
string->len = n;

在这里,我们已经分配的X型的结构上是一个int(对于LEN)的大小,再加上“世界你好”的长度,加上1堆(因为STR 1被列入的sizeof(X)。

当您想在同一块中的某些可变长度数据之前紧挨着一个“标题”时,它通常很有用。


我个人发现自己自己更容易malloc()和realloc(),并在需要查找长度时使用strlen()更容易,但是如果您需要一个永远不知道字符串长度并且可能需要查找很多字符串的程序,时代,这可能是更好的路。
克里斯·卢兹

4
“ ...可以在其中编写” str []“的C99版本。相当频繁。我认为是C99。我知道较早的编译器会抱怨零大小的数组。
smcameron

3
我也很喜欢这个,但是,您应该使用malloc(offsetof(X,str)+ numbytes)之类的东西,否则由于填充和对齐问题会出错。例如的sizeof(结构X)可以是8而不是5
Fozi

3
@Fozi:我实际上认为这不是问题。由于此版本具有str[1](没有str[]),因此str中包含1个字节的str sizeof(struct X)。这包括len和之间的任何填充str
埃文·特兰

2
@Rusky:那会对什么产生负面影响?假设后面有“填充” str。好的,当我分配时sizeof(struct X) + 10,这str实际上使10 - sizeof(int)(或更多,因为我们说有填充)变大了。此覆盖 str和之后的任何填充。这将有唯一的办法任何区别,就是是否有后成员str打破了整个事情反正,灵活的成员必须是最后一次。最后的任何填充只会导致分配过多。请提供一个具体示例,说明它实际上可能如何出错。
埃文·特兰

17

通过模拟类,使用C进行面向对象的代码。

只需创建一个结构和一组函数,并将指向该结构的指针作为第一个参数。


2
是否还有某种东西可以像以前的cfront一样将C ++转换为C?
MarkJ 2009年

11
这几乎不是面向对象的。对于具有继承的OO,您需要在对象结构中添加某种虚拟函数表,而“子类”可能会重载该表。为此,有很多半熟的“带有类的C”风格的框架,但是我建议不要使用。
exDM69 2011年

应该说。+1。
阿米特S

3
@ exDM69,面向对象既是一种思考问题的方式,又是一种编码范例;您无需继承即可成功完成此操作。在深入研究C ++之前,我在几个项目上做了这个。
Mark Ransom

16

代替

printf("counter=%d\n",counter);

#define print_dec(var)  printf("%s=%d\n",#var,var);
print_dec(counter);

14

使用愚蠢的宏技巧使记录定义更易于维护。

#define COLUMNS(S,E) [(E) - (S) + 1]

typedef struct
{
    char studentNumber COLUMNS( 1,  9);
    char firstName     COLUMNS(10, 30);
    char lastName      COLUMNS(31, 51);

} StudentRecord;

11

为了创建一个变量,该变量在除声明的变量之外的所有模块中都是只读的:

// Header1.h:

#ifndef SOURCE1_C
   extern const int MyVar;
#endif

// Source1.c:

#define SOURCE1_C
#include Header1.h // MyVar isn't seen in the header

int MyVar; // Declared in this file, and is writeable

// Source2.c

#include Header1.h // MyVar is seen as a constant, declared elsewhere

感觉很危险。这些是不匹配的声明和定义。进行编译时Source2.c,编译器可能会假设MyVar即使更改了对的函数调用,它也不会更改Source1.c。(请注意,作为一个实际的const变量,它与指向const的指针不同。在后一种情况下,指向的对象仍可以通过其他指针进行修改。)
jilles 2011年

1
这不会产生仅在某些编译单元中为只读的变量。这会产生不确定的行为(请参见ISO 9899的第6.2.7.2页和第6.7.3.5页)。
Ales Hakl,2011年

8

移位最多只能定义为31(32位整数)上的移位量。

如果要计算移位也需要使用更高的移位值,该怎么办?这是Theora视频编码解码器的工作方式:

unsigned int shiftmystuff (unsigned int a, unsigned int v)
{
  return (a>>(v>>1))>>((v+1)>>1);
}

或者更具可读性:

unsigned int shiftmystuff (unsigned int a, unsigned int v)
{
  unsigned int halfshift = v>>1;
  unsigned int otherhalf = (v+1)>>1;

  return (a >> halfshift) >> otherhalf; 
}

以上述方式执行任务比使用这样的分支要快得多:

unsigned int shiftmystuff (unsigned int a, unsigned int v)
{
  if (v<=31)
    return a>>v;
  else
    return 0;
}

... and gcc实际上内联它:) +1
Tim Post

2
在我的计算机上,gcc-4.3.2通过使用cmov指令(有条件移动)摆脱了第二个分支中的分支
Adam Rosenfield

3
“比使用分支要快得多”:区别在于分支对于的所有值都是正确的v,而halfshift技巧只将允许范围翻了一番,在32位体系结构上为63,在64位体系结构上为127。
Pascal Cuoq

8

声明用于实现有限状态机的函数的指针数组。

int (* fsm[])(void) = { ... }

最令人高兴的优点是,强制每个刺激/状态检查所有代码路径很简单。

在嵌入式系统中,我经常会映射一个ISR以指向这样的表,并根据需要对其进行重新引导(在ISR之外)。


我喜欢的一种技术是,如果您有一个需要初始化的函数,则可以通过调用初始化例程来初始化指针。运行该命令时,最后要做的是将指针替换为指向实际函数的指针,然后调用该函数。这样,在第一次调用该函数时会自动调用初始化程序,然后在以后每次调用实函数。
TMN

7

另一个不错的预处理器“技巧”是使用“#”字符来打印调试表达式。例如:

#define MY_ASSERT(cond) \
  do { \
    if( !(cond) ) { \
      printf("MY_ASSERT(%s) failed\n", #cond); \
      exit(-1); \
    } \
  } while( 0 )

编辑:以下代码仅适用于C ++。感谢smcameron和Evan Teran。

是的,编译时间断言总是很棒的。它也可以写成:

#define COMPILE_ASSERT(cond)\
     typedef char __compile_time_assert[ (cond) ? 0 : -1]

但是COMPILE_ASSERT宏不能使用两次,因为它会用typedef污染名称空间,第二种用法是:错误:typedef'__compile_time_assert'的重新定义
smcameron

您真的尝试过吗?您可以“ typedef foo;” 尽可能多的次数。这就是您进行预声明的方式。我已经在gcc,VC和嵌入式环境的多个编译器上使用了2.5年,并且从未遇到过任何困难。
吉拉德·纳尔

我讨厌C预处理程序... :(
Hasen

1
是的,我尝试过。我剪切并粘贴了来自gcc编译器的错误消息。
smcameron

1
@Gilad:在c ++中具有冗余的typedef是合法的,但在c中则不合法。
埃文·特兰

6

因为我从未使用过它,所以我不会真的把它称为最喜欢的把戏,但是提到Duff的设备使我想起了这篇文章,用C语言实现协程。有用一些时间。


实际上,我实际上已经使用了该技术,以使驱动一系列相关的异步I / O的代码几乎可以被人类阅读。主要区别在于,我不将协程状态存储在static变量中,而是动态分配结构,并将指向该结构的指针传递到协程函数中。一堆宏使它更加美味。它不是很好,但是比跳转到各处的异步/回调版本更好。swapcontext()如果可以的话,我会使用绿色线程(通过* nixes)。
2011年

6
#if TESTMODE == 1    
    debug=1;
    while(0);     // Get attention
#endif

while(0); 对程序没有影响,但是编译器将发出有关“此操作无效”的警告,这足以使我了解有问题的行,然后查看想要引起注意的真正原因。


9
您不能使用#warning吗?
Stefano Borini

显然,我可以。它不是完全标准,但是可以在我使用的编译器中使用。有趣的是,嵌入式编译器翻译了#define,而gcc却没有。
gbarry

6

我是xor hack的粉丝:

交换2个没有第三个临时指针的指针:

int * a;
int * b;
a ^= b;
b ^= a;
a ^= b;

或者我真的很喜欢只有一个指针的异或链表。(http://zh.wikipedia.org/wiki/XOR_linked_list)

链表中的每个节点都是上一个节点和下一个节点的Xor。为了遍历,可以通过以下方式找到节点的地址:

LLNode * first = head;
LLNode * second = first.linked_nodes;
LLNode * third = second.linked_nodes ^ first;
LLNode * fourth = third.linked_nodes ^ second;

等等

或向后移动:

LLNode * last = tail;
LLNode * second_to_last = last.linked_nodes;
LLNode * third_to_last = second_to_last.linked_nodes ^ last;
LLNode * fourth_to_last = third_to_last.linked_nodes ^ second_to_last;

等等

虽然不是很有用(您不能从任意节点开始遍历),但我发现它非常酷。


5

这本书来自“足够用绳子击打自己的脚”一书:

在标题中声明

#ifndef RELEASE
#  define D(x) do { x; } while (0)
#else
#  define D(x)
#endif

在您的代码中放置测试语句,例如:

D(printf("Test statement\n"));

当宏的内容扩展为多个语句时,do / while会有所帮助。

仅当不使用编译器的“ -D RELEASE”标志时,才会打印该语句。

然后,您可以例如。将标志传递到您的makefile等

不确定这在Windows中如何工作,但在* nix中是否能正常工作


定义RELEASE时,您可能想将D(x)扩展为{},以便它与if语句配合使用。否则为“如果(a)D(x);” 当您定义了RELEASE时,它将扩展为“ if(a)”。这将使你在发布一些不错的错误verison
MarkJ

3
@MarkJ:不。它的方式是“ if(a)D(x);” 扩展为“ if(a);” 很好。如果您将D(x)扩展为{},则为“ if(a)if(b)D(x); else foo();” 会不正确地扩展为“ if(a)if(b){}; else foo();”,从而导致“ else foo()”与第二个if而不是第一个if匹配。
亚当·罗森菲尔德2009年

老实说,我主要使用此宏来测试打印语句,或者如果我有条件语句,则将其全部括起来。D(if(a)foo(););
西蒙·沃克,2009年

1
@AdamRosenfield:使用#define D(x) do { } while(0)代替来处理这种情况(并且也可以应用于插入的分支x以保持一致性)
rpetrich 2011年

3

Rusty实际上在ccan中生成了一整套构建条件,请查看build assert模块:

#include <stddef.h>
#include <ccan/build_assert/build_assert.h>

struct foo {
        char string[5];
        int x;
};

char *foo_string(struct foo *foo)
{
        // This trick requires that the string be first in the structure
        BUILD_ASSERT(offsetof(struct foo, string) == 0);
        return (char *)foo;
}

实际的标头中还有许多其他有用的宏,这些宏很容易就位。

我尽一切努力通过主要坚持使用内联函数来抵制黑暗面(和预处理程序滥用),但是我确实喜欢像您所描述的那样聪明,有用的宏。


是的,我最近遇到了ccan,并正在考虑提供一些代码,但还没有完全按照“ ccan方式”来做。不过,感谢您提供的链接,希望能有更多的动机来研究ccan,我真的希望能引起大家的注意。
smcameron

好吧,我不会太在意“ can方式”,直到它更成熟为止。。。现在ccan-lint被提议作为GSOC项目。它是一个很小而相当友好的团体..也是丢弃摘录的好地方:)
Tim Post

顺便说一句,我注意到Rusty的BuILD_ASSERT就像Linux内核中的宏(毫不奇怪),但是缺少“ nots”(或“刘海”或“!”)之一,并且注意到,我认为我发布的宏的示例用法是不正确。应该是:“ BUILD_BUG_ON(((sizeof(struct mystruct)%8))”
smcameron's Marcam

3

关于这类东西的两本很好的原始书是“编程编写固态代码的实践。其中一个(我不记得是哪个)说:最好在可能的地方使用#define枚举,因为枚举由编译器检查。


1
AFAIK,在C89 / 90中,没有枚举的类型检查。枚举只是某种程度上更方便#defines。
cschol

第二版ED K&R,第39页底部。至少有机会进行检查。
乔纳森·沃特莫夫

3

不特定于C,但我一直很喜欢XOR运算符。它可以做的一件很酷的事情是“不使用临时值交换”:

int a = 1;
int b = 2;

printf("a = %d, b = %d\n", a, b);

a ^= b;
b ^= a;
a ^= b;

printf("a = %d, b = %d\n", a, b);

结果:

a = 1,b = 2

a = 2,b = 1


a = 1;b = 2;a = a + b; b = ab;a = ab; 也会给出相同的结果
Grambot 2011年

这也将交换a和b:a ^ = b ^ = a ^ = b;
vikhyat,2011年

@TheCapn:但是附加可能溢出。
Michael Foukarakis 2011年


2

我喜欢container_of清单中使用的概念。基本上,您不需要指定nextlast列表中的每个结构字段。而是将列表结构标头附加到实际的链接项。

看看include/linux/list.h现实生活中的例子。


1

我认为userdata指针的使用非常简洁。如今,时尚正在逐渐失势。它不是C功能,而是在C中非常容易使用。


1
我希望我能理解您的意思。你能解释更多吗?什么是userdata指针?
Zan Lynx'3


它主要用于回调。您希望在每次触发回调时将这些数据还给您。对于将C指针传递给回调函数特别有用,因此您可以将对象绑定到事件。
Evan Teran

是的。谢谢。我经常使用它,但我从未这样称呼过。
Zan Lynx'3

1

我使用X-Macros来让预编译器生成代码。它们对于在一处定义错误值和相关的错误字符串特别有用,但是它们可以远远超出此范围。


1

我们的代码库有一个类似于

#ifdef DEBUG

#define my_malloc(amt) my_malloc_debug(amt, __FILE__, __LINE__)
void * my_malloc_debug(int amt, char* file, int line)
#else
void * my_malloc(int amt)
#endif
{
    //remember file and line no. for this malloc in debug mode
}

它允许在调试模式下跟踪内存泄漏。我一直认为这很酷。


1

宏的乐趣:

#define SOME_ENUMS(F) \
    F(ZERO, zero) \
    F(ONE, one) \
    F(TWO, two)

/* Now define the constant values.  See how succinct this is. */

enum Constants {
#define DEFINE_ENUM(A, B) A,
    SOME_ENUMS(DEFINE_ENUMS)
#undef DEFINE_ENUM
};

/* Now a function to return the name of an enum: */

const char *ToString(int c) {
    switch (c) {
    default: return NULL; /* Or whatever. */
#define CASE_MACRO(A, B) case A: return #b;
     SOME_ENUMS(CASE_MACRO)
#undef CASE_MACRO
     }
}

0

这是一个示例,该示例如何使C代码完全不了解硬件在运行该应用程序时实际使用了什么。main.c进行设置,然后可以在任何编译器/架构上实现自由层。我认为稍微抽象一下C代码是很整洁的事情,因此不必过于专心。

在此处添加完整的可编译示例。

/* free.h */
#ifndef _FREE_H_
#define _FREE_H_
#include <stdio.h>
#include <string.h>
typedef unsigned char ubyte;

typedef void (*F_ParameterlessFunction)() ;
typedef void (*F_CommandFunction)(ubyte byte) ;

void F_SetupLowerLayer (
F_ParameterlessFunction initRequest,
F_CommandFunction sending_command,
F_CommandFunction *receiving_command);
#endif

/* free.c */
static F_ParameterlessFunction Init_Lower_Layer = NULL;
static F_CommandFunction Send_Command = NULL;
static ubyte init = 0;
void recieve_value(ubyte my_input)
{
    if(init == 0)
    {
        Init_Lower_Layer();
        init = 1;
    }
    printf("Receiving 0x%02x\n",my_input);
    Send_Command(++my_input);
}

void F_SetupLowerLayer (
    F_ParameterlessFunction initRequest,
    F_CommandFunction sending_command,
    F_CommandFunction *receiving_command)
{
    Init_Lower_Layer = initRequest;
    Send_Command = sending_command;
    *receiving_command = &recieve_value;
}

/* main.c */
int my_hw_do_init()
{
    printf("Doing HW init\n");
    return 0;
}
int my_hw_do_sending(ubyte send_this)
{
    printf("doing HW sending 0x%02x\n",send_this);
    return 0;
}
F_CommandFunction my_hw_send_to_read = NULL;

int main (void)
{
    ubyte rx = 0x40;
    F_SetupLowerLayer(my_hw_do_init,my_hw_do_sending,&my_hw_send_to_read);

    my_hw_send_to_read(rx);
    getchar();
    return 0;
}

4
细心阐述,也许解释实际用途?
莱昂纳多·埃雷拉,

作为一个例子,如果我必须使用som硬件接口编写测试程序,该程序最终会产生中断。然后,可以将该模块设置为执行正常范围之外的功能,作为信号/中断处理程序。
eaanon01

0
if(---------)  
printf("hello");  
else   
printf("hi");

填写空格,使hello和hi都不会出现在输出中。
回答:fclose(stdout)


您可以使用{}工具栏按钮设置代码格式(我已经为您完成了)。“报价”按钮不会保留空格或不应用语法突出显示。
阿尔瓦罗·冈萨雷斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.