什么是Cmacros有用?

我已经写了一些C ,我可以很好地阅读它,以便大致了解它在做什么,但是每次遇到一个macros,它都会把我完全抛到脑后。 我最终不得不记得这个macros是什么,并且在我读的时候把它代入我的脑海。 我遇到的那些直观易懂的东西,总是像小小的function,所以我一直在想,为什么它们不只是function。

我可以理解,需要为预处理器中的debugging或跨平台构build定义不同的构buildtypes,但是定义任意replace的能力似乎仅用于使已经很难理解的语言更难以理解。

为什么为C引入这样一个复杂的预处理器? 还有没有人有一个使用它的例子,这将使我明白为什么它似乎仍然被用于除了简单的目的以外#debug样式条件编译?

编辑:

读了一些答案,我仍然不明白。 最常见的答案是内联代码。 如果inline关键字没有这样做,那么它有一个很好的理由不这样做,或实施需要修复。 我不明白为什么需要一个完全不同的机制,这意味着“真正内联这个代码”(除了内联之前的代码)。 我也不理解被提到的“如果太愚蠢的function”的想法。 当然,任何接受input和产生输出的代码最好放在一个函数中。 我想我可能没有得到它,因为我不习惯写C的微观优化,但预处理器只是一个复杂的解决scheme,几个简单的问题。

我最终不得不记得这个macros是什么,并且在我读的时候把它replace成我的头。

这似乎反映了macros的命名不佳。 如果它是一个log_function_entry()macros,我会假设你不需要模拟预处理器。

我遇到的那些直观,容易理解的东西总是像小小的function,所以我总是想知道为什么他们不能正常工作。

通常他们应该是,除非他们需要操作通用参数。

 #define max(a,b) ((a)<(b)?(b):(a)) 

将使用<运营商的任何types的工作。

更多的只是函数,macros可以让你使用源文件中的符号来执行操作。 这意味着你可以创build一个新的variables名,或者引用macros所在的源文件和行号。

在C99中,macros也允许你调用诸如printf之类的可变参数

 #define log_message(guard,format,...) \ if (guard) printf("%s:%d: " format "\n", __FILE__, __LINE__,__VA_ARGS_); log_message( foo == 7, "x %d", x) 

其中格式像printf一样工作。 如果守卫为真,则输出该消息以及打印该消息的文件和行号。 如果这是一个函数调用,它不会知道你调用它的文件和行,并使用vaprintf将是一个更多的工作。

这个摘录几乎总结了我对这个问题的看法,比较了几种使用Cmacros的方法,以及如何在D实现它们。

从DigitalMars.com复制

C发明时,编译器技术是原始的。 在前端安装一个文本macros预处理器是添加许多强大function的简单方便的方法。 节目的规模越来越大,越来越复杂,说明这些特点带来了许多固有的问题。 D没有预处理器; 但是D提供了更可扩展的手段来解决相同的问题。

macros

预处理器macros为C增加了强大的function和灵活性。 但他们有一个缺点:

  • macros有没有范围的概念; 从定义的angular度来看,它们是有效的。 他们在.h文件,嵌套代码等等中划分了一行。当包含数以万计的macros定义行时,避免无意的macros扩展成为问题。
  • debugging器不知道macros。 试图用符号数据debugging一个程序只会被debugging器破坏,只知道macros扩展,而不是macros本身。
  • macros使源代码无法标记,因为之前的macros更改可以任意重做令牌。
  • macros的纯文本基础导致任意和不一致的用法,使得使用macros的代码容易出错。 (有些尝试解决这个问题是在C++模板中引入的。)
  • macros仍被用来弥补语言expression能力的不足,比如头文件的“包装”。

这里列举了macros的常用用法,以及D中相应的function:

  1. 定义文字常量:

    • C预处理器的方式

       #define VALUE 5 
    • D方式

       const int VALUE = 5; 
  2. 创build一个值或标志的列表:

    • C预处理器的方式

       int flags: #define FLAG_X 0x1 #define FLAG_Y 0x2 #define FLAG_Z 0x4 ... flags |= FLAG_X; 
    • D方式

       enum FLAGS { X = 0x1, Y = 0x2, Z = 0x4 }; FLAGS flags; ... flags |= FLAGS.X; 
  3. 设置函数调用约定:

    • C预处理器的方式

       #ifndef _CRTAPI1 #define _CRTAPI1 __cdecl #endif #ifndef _CRTAPI2 #define _CRTAPI2 __cdecl #endif int _CRTAPI2 func(); 
    • D方式

      调用约定可以用块指定,所以不需要为每个函数改变它:

       extern (Windows) { int onefunc(); int anotherfunc(); } 
  4. 简单的通用编程:

    • C预处理器的方式

      根据文本replaceselect使用哪个函数:

       #ifdef UNICODE int getValueW(wchar_t *p); #define getValue getValueW #else int getValueA(char *p); #define getValue getValueA #endif 
    • D方式

      D使符号的声明成为其他符号的别名:

       version (UNICODE) { int getValueW(wchar[] p); alias getValueW getValue; } else { int getValueA(char[] p); alias getValueA getValue; } 

DigitalMars网站上有更多的例子。

请记住,macros(和预处理器)来自C最早的日子。它们曾经是内联“函数”的唯一方式(因为当然,内联是最近的关键字),它们仍然是只能强制内联的方法。

此外,macros是唯一的方法,你可以做这样的技巧,例如在编译时插入文件和行到string常量。

现在,macros已经成为唯一可行的方法,很多都是通过更新的机制来处理的。 但他们不时还是有其位置的。

它们是C语言的一种编程语言(比较简单),所以它们在编译的时候用于元编程是非常有用的。换句话说,你可以编写生成C代码所用的代码less,时间短的macros代码直接写在C中

它们对于编写“多态”或“超载”的“函数式”expression式也非常有用; 例如定义为的最大macros:

 #define max(a,b) ((a)>(b)?(a):(b)) 

对于任何数字types都是有用的; 在C中你不能写:

 int max(int a, int b) {return a>b?a:b;} float max(float a, float b) {return a>b?a:b;} double max(double a, double b) {return a>b?a:b;} ... 

即使你想要,因为你不能重载函数。

更不用说条件编译和文件包括(也是macros语言的一部分)…

macros允许有人在编译期间修改程序行为。 考虑一下:

  • C常量允许在开发时修复程序行为
  • Cvariables允许在执行时修改程序行为
  • Cmacros允许在编译时修改程序行为

在编译时意味着未使用的代码甚至不会进入二进制文件,并且构build过程可以修改这些值,只要它与macros预处理器集成即可。 例如:make ARCH = arm(假设转发macros定义为cc -DARCH = arm)

简单的例子:(从glibc limits.h中定义long的最大值)

 #if __WORDSIZE == 64 #define LONG_MAX 9223372036854775807L #else #define LONG_MAX 2147483647L #endif 

在编译时validation(使用#define __WORDSIZE),如果我们正在编译32或64位。 使用multilib工具链时,使用参数-m32和-m64可能会自动更改位的大小。

(POSIX版本请求)

 #define _POSIX_C_SOURCE 200809L 

编译期间请求POSIX 2008支持。 标准库可能支持许多(不兼容)的标准,但是有了这个定义,它将提供正确的函数原型(例如:getline(),no gets()等)。 如果库不支持这个标准,它可能会在编译时产生一个#error,而不是在执行过程中崩溃。

(硬编码path)

 #ifndef LIBRARY_PATH #define LIBRARY_PATH "/usr/lib" #endif 

在编译期间定义一个硬编码目录。 例如,可以使用-DLIBRARY_PATH = / home / user / lib进行更改。 如果这是一个const char *,那么在编译期间如何configuration它?

(pthread.h,编译时复杂的定义)

 # define PTHREAD_MUTEX_INITIALIZER \ { { 0, 0, 0, 0, 0, 0, { 0, 0 } } } 

可能会声明大量的文本,否则不会被简化(总是在编译时)。 用函数或常量(编译时)不可能做到这一点。

为了避免真正复杂的事情,并避免提示糟糕的编码风格,我不会给出一个代码编译在不同的,不兼容的操作系统的例子。 使用你的交叉构build系统,但是应该清楚的是,预处理器允许没有编译系统的帮助,不用因为没有接口而破坏编译。

最后,考虑一下embedded式系统中条件编译的重要性,这些embedded式系统的处理器速度和内存是有限的,系统是非常不同的。

现在,如果你问,是否有可能用适当的定义来replace所有的macros常量定义和函数调用? 答案是肯定的,但是在编译过程中不会简单地改变程序行为。 预处理器仍然是必需的。

除了内联效率和条件编译之外,macros可以用来提高低级C代码的抽象级别。 C并没有真正让你摆脱内存和资源pipe理的细节以及数据的精确布局,并且支持非常有限的信息隐藏forms和其他大型系统pipe理机制。 使用macros,您不再局限于仅使用C语言中的基本构造:您可以定义自己的数据结构和编码构造(包括类和模板!),同时还可以名义上编写C!

预处理macros实际上提供了一个在编译时执行的图灵完成语言。 在C ++方面,令人印象深刻的(也是稍微可怕的)例子之一就是: Boost预处理器库使用C99 / C ++ 98预处理器来构build(相对)安全的编程结构,然后扩展到任何基础声明和代码你input的是C还是C ++。

在实践中,我build议关于预处理器编程作为最后的手段,当你没有在更安全的语言中使用高级构造的纬度。 但有时候,如果你的背部靠在墙上,黄鼠狼就会关上,你知道怎么办?!

从计算机愚蠢 :

我已经在很多UNIX的免费游戏程序中看到过这段代码:

/ *
*位值。
* /
#define BIT_0 1
#define BIT_1 2
#define BIT_2 4
#define BIT_3 8
#define BIT_4 16
#define BIT_5 32
#define BIT_6 64
#define BIT_7 128
#define BIT_8 256
#define BIT_9 512
#define BIT_10 1024
#define BIT_11 2048
#define BIT_12 4096
#define BIT_13 8192
#define BIT_14 16384
#define BIT_15 32768
#define BIT_16 65536
#define BIT_17 131072
#define BIT_18 262144
#define BIT_19 524288
#define BIT_20 1048576
#define BIT_21 2097152
#define BIT_22 4194304
#define BIT_23 8388608
#define BIT_24 16777216
#define BIT_25 33554432
#define BIT_26 67108864
#define BIT_27 134217728
#define BIT_28 268435456
#define BIT_29 536870912
#define BIT_30 1073741824
#define BIT_31 2147483648

实现这一点的更简单的方法是:

#define BIT_0 0x00000001
#define BIT_1 0x00000002
#define BIT_2 0x00000004
#define BIT_3 0x00000008
#define BIT_4 0x00000010

#define BIT_28 0x10000000
#define BIT_29 0x20000000
#define BIT_30 0x40000000
#define BIT_31 0x80000000

更简单的方法仍然是让编译器进行计算:

#define BIT_0(1)
#define BIT_1(1 << 1)
#define BIT_2(1 << 2)
#define BIT_3(1 << 3)
#define BIT_4(1 << 4)

#define BIT_28(1 << 28)
#define BIT_29(1 << 29)
#define BIT_30(1 << 30)
#define BIT_31(1 << 31)

但是为什么要定义32个常量呢? C语言也有参数化的macros。 你真正需要的是:

#define BIT(x)(1 <<(x))

无论如何,我不知道是谁写的原始代码使用计算器或只是在纸上计算出来的。

这只是macros的一个可能的用途。

macros的真正亮点之一就是在用代码生成代码的时候。

我曾经在一个旧的C ++系统上工作,他使用插件系统以自己的方式将parameter passing给插件(使用自定义的类地图结构)。 一些简单的macros被用来处理这个怪癖,并允许我们使用真正的C ++类和function在插件中的正常参数没有太多的问题。 所有的胶水代码都是由macros生成的。

考虑到你的问题的意见,你可能不会完全理解的是,调用一个函数可能需要相当的开销。 参数和键寄存器可能必须在进入的过程中被复制到堆栈,并且堆栈在退出的过程中解开。 旧英特尔芯片尤其如此。 macros让程序员保持一个函数的抽象(几乎),但避免了函数调用的代价高昂的开销。 inline关键字是build议性的,但编译器可能并不总是正确的。 “C”的荣耀和危险在于,你通常可以根据自己的意愿弯曲编译器。

在你的面包和黄油里,日常的应用程序编程这种微型优化(避免函数调用)通常会更糟糕,但是如果你正在编写一个由操作系统内核调用的时间关键函数,那么它可以造成巨大的差异。

我会加上已经说过的话。

因为macros在文本replace上工作,所以它们允许你做非常有用的事情,这是不可能使用函数的。

这里有一些macros可以非常有用的例子:

 /* Get the number of elements in array 'A'. */ #define ARRAY_LENGTH(A) (sizeof(A) / sizeof(A[0])) 

这是一个非常stream行和经常使用的macros。 例如,当你需要迭代一个数组时,这非常方便。

 int main(void) { int a[] = {1, 2, 3, 4, 5}; int i; for (i = 0; i < ARRAY_LENGTH(a); ++i) { printf("a[%d] = %d\n", i, a[i]); } return 0; } 

在这里,如果另外一个程序员在这个删除中增加了五个元素,那么这并不重要。 for -loop将始终遍历所有元素。

C库的函数比较内存和string是相当丑陋的使用。

你写:

 char *str = "Hello, world!"; if (strcmp(str, "Hello, world!") == 0) { /* ... */ } 

要么

 char *str = "Hello, world!"; if (!strcmp(str, "Hello, world!")) { /* ... */ } 

检查是否指向"Hello, world" 。 我个人认为,这两个解决scheme看起来相当丑陋和混乱(特别是!strcmp(...) )。

这里有两个整齐的macros,有些人(包括我)在使用strcmp / memcmp来比较string或内存时使用这些macros:

 /* Compare strings */ #define STRCMP(A, o, B) (strcmp((A), (B)) o 0) /* Compare memory */ #define MEMCMP(A, o, B) (memcmp((A), (B)) o 0) 

现在你可以像这样写代码了:

 char *str = "Hello, world!"; if (STRCMP(str, ==, "Hello, world!")) { /* ... */ } 

这里的意图更清晰!

这些都是macros被用于function无法完成的事情。 macros不应该用来代替函数,但是它们还有其他用途。

这对于内联代码和避免函数调用开销是很好的。 如果您想稍后更改行为而无需编辑大量地点,也可以使用它。 这对复杂的事情没有用处,但对于想要内联的简单代码行来说,这并不坏。

与常规函数不同,您可以在macros中执行控制stream(if,while,for …)。 这是一个例子:

 #include <stdio.h> #define Loop(i,x) for(i=0; i<x; i++) int main(int argc, char *argv[]) { int i; int x = 5; Loop(i, x) { printf("%d", i); // Output: 01234 } return 0; } 

其中一个显而易见的原因是,通过使用macros,代码将在编译时被扩展,并且您将获得一个伪函数调用,而不会导致调用开销。

否则,你也可以使用它作为符号常量,这样你就不必在几个地方编辑相同的值来改变一个小的东西。

通过利用C预处理器的文本操作,可以构造多态数据结构的C等价物。 使用这种技术,我们可以构build一个可用于任何C程序的原始数据结构的可靠工具箱,因为它们利用了C语法,而不是任何特定实现的细节。

有关如何使用macros来pipe理数据结构的详细说明,请参阅此处 – http://multi-core-dump.blogspot.com/2010/11/interesting-use-of-c-macros-polymorphic.html

macros可以让你摆脱复制粘贴的碎片,这是你无法用任何其他方法消除的。

例如(真正的代码,VS 2010编译器的语法):

 for each (auto entry in entries) { sciter::value item; item.set_item("DisplayName", entry.DisplayName); item.set_item("IsFolder", entry.IsFolder); item.set_item("IconPath", entry.IconPath); item.set_item("FilePath", entry.FilePath); item.set_item("LocalName", entry.LocalName); items.append(item); } 

这是您将相同名称的字段值传递给脚本引擎的地方。 这是复制粘贴? 是。 DisplayName用作脚本的string和编译器的字段名称。 那不好吗? 是。 如果你重构你的代码并将LocalName重命名为RelativeFolderName (就像我做的那样),忘记对string做同样的事情(就像我做的那样),脚本将以你不期望的方式工作(事实上,在我的例子中,取决于你是否忘记在单独的脚本文件中重命名该字段,但如果该脚本用于序列化,这将是一个100%的错误)。

如果你使用这个macros,这个bug将没有空间:

 for each (auto entry in entries) { #define STR_VALUE(arg) #arg #define SET_ITEM(field) item.set_item(STR_VALUE(field), entry.field) sciter::value item; SET_ITEM(DisplayName); SET_ITEM(IsFolder); SET_ITEM(IconPath); SET_ITEM(FilePath); SET_ITEM(LocalName); #undef SET_ITEM #undef STR_VALUE items.append(item); } 

不幸的是,这为其他types的错误打开了一扇大门。 你可以写一个错误的macros,并永远不会看到一个被破坏的代码,因为编译器不显示所有预处理后它看起来如何。 别人可以使用相同的名称(这就是为什么我用#undef尽快“释放”macros)。 所以,明智地使用它。 如果你看到另一种摆脱复制粘贴的代码(比如函数)的方式,就用这种方法。 如果您发现使用macros删除复制粘贴的代码是不值得的,请保留复制粘贴的代码。

macros…当你的&#(* $&编译器拒绝内联某些东西。

这应该是一个激励海报,不是吗?

严肃地说,谷歌预处理器滥用 (您可能会看到类似的问题,作为#1的结果)。 如果我正在写一个超越assert()function的macros,我通常会试着看看我的编译器是否真的内联一个类似的函数。

其他人会反对使用#if进行条件编译..他们宁愿你:

 if (RUNNING_ON_VALGRIND) 

而不是

 #if RUNNING_ON_VALGRIND 

..为了debugging的目的,因为你可以在debugging器中看到if()而不是#if。 然后我们深入#ifdef和#if。

如果它在10行以下的代码,尝试内联。 如果不能内联,请尝试优化它。 如果它太愚蠢是一个function,那就制作一个macros。

虽然我不是一个macros的狂热粉丝,并且不再倾向于写出更多的C,但基于我目前的任务,类似这样的(显然可能有一些副作用)是方便的:

 #define MIN(X, Y) ((X) < (Y) ? (X) : (Y)) 

现在我已经好几年没有写过这样的东西了,但是像这样的“function”都是我职业生涯早期所维护的代码。 我想扩展可能被认为是方便的。