什么是C宏有用?

时间:2009-03-17 11:30:50

标签: c macros c-preprocessor language-design language-features

我已经写了一些 C ,我可以很好地阅读它,以便大致了解它在做什么,但每次遇到一个宏它都完全抛弃了我。我最终必须记住宏是什么,并在我阅读时将其替换为我的脑袋。我遇到的那些直观且易于理解的东西总是像迷你小功能一样,所以我总是想知道为什么它们不仅仅是功能。

我可以理解在预处理器中为调试或跨平台构建定义不同构建类型的必要性,但是定义任意替换的能力似乎只对使得已经很难理解的语言更难理解。

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

编辑:

阅读了许多答案后,我仍然没有得到它。最常见的答案是内联代码。如果内联关键字没有这样做,那么它有充分的理由不这样做,或者实现需要修复。我不明白为什么需要一个完全不同的机制,这意味着“真正内联这个代码”(除了形成内联之前编写的代码)。我也不明白提到“如果它太愚蠢而不能被赋予功能”的想法。当然,任何需要输入并产生输出的代码都最好放在一个函数中。我想我可能没有得到它,因为我不习惯编写 C 的微观优化,但预处理器只是对一些简单问题的复杂解决方案。

18 个答案:

答案 0 :(得分:53)

  

我最终必须记住宏是什么,并在我阅读时将其替换在脑海中。

这似乎反映了宏的命名。我假设你不必模拟预处理器,如果它是log_function_entry()宏。

  

我遇到的那些直观且易于理解的东西总是像迷你小功能一样,所以我总是想知道为什么它们不仅仅是功能。

通常它们应该是,除非它们需要对通用参数进行操作。

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

适用于<运算符的任何类型。

更多只是函数,宏允许您使用源文件中的符号执行操作。这意味着您可以创建新的变量名称,或引用宏所在的源文件和行号。

在C99中,宏还允许您调用variadic函数,例如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会更多一些工作。

答案 1 :(得分:17)

此摘录通过比较使用C宏的几种方式以及如何在D中实现它们,总结了我对此问题的看法。

copied from DigitalMars.com

  

发明C时,编译器   技术很原始。安装一个   文本宏预处理器到前面   结束是一种简单明了的方式   添加许多强大的功能。该   增加尺寸&amp;复杂性   节目已经说明了这些   功能有许多固有的   问题。 D没有   预处理;但是D提供了更多   可扩展意味着解决同样的问题   问题。

预处理器宏为C添加了强大的功能和灵活性。但他们有一个缺点:

  • 宏没有范围概念;从定义到结束,它们都是有效的。他们在.h文件,嵌套代码等中删除了一些文件。当#include成千上万行宏定义时,避免无意的宏扩展会成为问题。
  • 调试器不知道宏。尝试使用符号数据调试程序会被调试器破坏,只知道宏扩展,而不是宏本身。
  • 宏使得无法对源代码进行标记,因为早期的宏更改可以任意重做令牌。
  • 宏的纯文本基础导致任意和不一致的使用,使得使用宏的代码容易出错。 (C++中的模板引入了一些尝试解决此问题。)
  • 宏仍然用于弥补语言表达能力的不足,例如头文件周围的“包装”。

这是宏的常见用法的枚举,以及D中的相应特征:

  1. 定义文字常量:

    • C预处理器方式

      #define VALUE 5
      
    • D方式

      const int VALUE = 5;
      
  2. 创建值或标志列表:

    • 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预处理器方式

      根据文本替换选择要使用的功能:

      #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;
      }
      
  5. DigitalMars website上有更多示例。

答案 2 :(得分:15)

它们是一种基于C的编程语言(更简单的一种),所以它们对于在编译时进行元编程很有用...换句话说,你可以用更少的行和时间编写生成C代码的宏代码它将直接用C语言写出来。

它们对于编写“多态”或“过载”的“类似函数”表达式也非常有用;例如最大宏定义为:

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

对任何数字类型都很有用;在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;}
...

即使你想要,因为你不能超载功能。

更不用说条件编译和文件包含(也是宏语言的一部分)......

答案 3 :(得分:12)

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

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

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

简单示例: (来自glibc limits.h,定义long的最大值)

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

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

(POSIX版本请求)

#define _POSIX_C_SOURCE 200809L

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

(硬编码路径)

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

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

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

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

可能会声明大部分文本(否则不会被简化)(始终在编译时)。使用函数或常量(在编译时)不可能这样做。

为了避免使事情变得非常复杂并避免建议编码风格不佳,我不会举一个在不同的,不兼容的操作系统中编译的代码示例。使用您的交叉构建系统,但应该清楚的是,预处理器在没有构建系统帮助的情况下允许这样做,而不会因为缺少接口而破坏编译。

最后,考虑条件编译在嵌入式系统中的重要性,其中处理器速度和内存有限,系统非常异构。

现在,如果你问,是否有可能用适当的定义替换所有宏常量定义和函数调用?答案是肯定的,但它不会简单地使编译期间改变程序行为的需要消失。仍然需要预处理器。

答案 4 :(得分:11)

请记住,宏(和预处理器)来自C的早期。它们曾经是内联'函数'的唯一方法(因为,当然,内联是一个非常新近的关键字),它们仍然是强行内联的唯一方法。

此外,宏是你在编译时将文件和行插入字符串常量这样的技巧的唯一方法。

现在,宏以前的许多事情都是通过新机制更好地处理的唯一方法。但他们不时仍然有自己的位置。

答案 5 :(得分:8)

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

预处理器宏实际上提供了在编译时执行的Turing-complete语言。 C ++方面的一个令人印象深刻(有点可怕)的例子是:Boost Preprocessor库使用C99 / C++98预处理器来构建(相对)安全的编程结构,然后扩展到你输入的任何底层声明和代码,无论是C还是C ++。

在实践中,当你没有在更安全的语言中使用高级结构的自由度时,我建议将预处理器编程作为最后的手段。但有时候,如果你的背靠墙并且黄鼠狼正在关闭......你知道你能做什么是很好的。

答案 6 :(得分:7)

来自Computer Stupidities

  

我在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&lt;&lt; 1)
   #define BIT_2(1 <&lt; 2)
   #define BIT_3(1 <&lt; 3)
   #define BIT_4(1 <&lt; 4)
   ...
   #define BIT_28(1&lt;&lt; 28)
   #define BIT_29(1&lt;&lt;&lt; 29)
   #define BIT_30(1 <&lt;&lt; 30)    #define BIT_31(1&lt;&lt; 31)

     

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

     

#define BIT(x)(1 <&lt;(x))

     

无论如何,我想知道编写原始代码的人是使用计算器还是只是在纸上计算出来。

这只是Macros的一种可能用途。

答案 7 :(得分:5)

我会补充说已经说过了什么。

因为宏可以处理文本替换,所以它们允许你做一些使用函数无法做到的非常有用的事情。

这里有一些宏可以真正有用的案例:

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

这是一个非常受欢迎且经常使用的宏。例如,当您需要遍历数组时,这非常方便。

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

如果另一个程序员在变换中向a添加了五个元素,那么这并不重要。 for - 循环将始终遍历所有元素。

C库的比较内存和字符串的功能非常难以使用。

你写道:

char *str = "Hello, world!";

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

char *str = "Hello, world!";

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

检查str是否指向"Hello, world"。我个人认为这两种解决方案看起来都很丑陋而且令人困惑(特别是!strcmp(...))。

以下是一些人(包括我)在需要使用strcmp / memcmp来比较字符串或内存时使用的两个整洁的宏:

/* 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!")) {
    /* ... */
}

这意味着更清楚了!

这些是宏用于事物功能无法完成的情况。宏不应该用于替换功能,但它们有其他好的用途。

答案 8 :(得分:5)

宏实际发光的一个例子就是用它们进行代码生成。

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

答案 9 :(得分:4)

与常规函数不同,您可以在宏中控制流(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;
} 

答案 10 :(得分:4)

鉴于您的问题中的评论,您可能不完全了解调用函数可能会产生相当大的开销。参数和键寄存器可能必须在进入的过程中复制到堆栈中,并且堆栈在出路时展开。旧的英特尔芯片尤其如此。宏让程序员保持函数的抽象(几乎),但避免了函数调用的昂贵开销。 inline关键字是建议性的,但编译器可能并不总是正确。 'C'的荣耀和危险在于你通常可以根据自己的意愿弯曲编译器。

在你的面包和黄油中,日常应用程序编程这种微优化(避免函数调用)通常更糟,然后无用,但如果你正在编写一个由操作内核调用的时间关键函数系统,它可以产生巨大的差异。

答案 11 :(得分:3)

内联代码并避免函数调用开销很有用。如果您想稍后更改行为而不编辑大量地点,也可以使用它。它对复杂的东西没有用,但对于你想要内联的简单代码行,它并不坏。

答案 12 :(得分:2)

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

此处给出了有关如何使用宏来管理数据结构的详细说明 - http://multi-core-dump.blogspot.com/2010/11/interesting-use-of-c-macros-polymorphic.html

答案 13 :(得分:2)

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

例如(真实代码,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用作脚本的字符串和编译器的字段名称。那不好吗?是。如果您重构代码并将LocalName重命名为RelativeFolderName(就像我一样)而忘记对字符串执行相同操作(就像我一样),脚本将以您不期望的方式工作(事实上​​,在我的示例中,它取决于您是否忘记在单独的脚本文件中重命名该字段,但如果该脚本用于序列化,那将是100%的错误。)

如果您使用宏,则不会有错误的空间:

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

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

答案 14 :(得分:1)

一个显而易见的原因是,通过使用宏,代码将在编译时扩展,并且您将获得没有调用开销的伪函数调用。

否则,您也可以将它用于符号常量,这样您就不必在几个地方编辑相同的值来改变一件小事。

答案 15 :(得分:0)

虽然我不是宏的忠实粉丝,并且根据我目前的任务,不再倾向于写很多C,这样的事情(显然会有一些副作用)很方便:

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

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

答案 16 :(得分:0)

宏...适用于你的&amp;#(* $&amp;编译器只是拒绝内联。

那应该是一张励志海报,不是吗?

严肃地说,谷歌preprocessor abuse(您可能会看到与#1结果类似的问题)。如果我正在编写一个超出assert()功能的宏,我通常会尝试查看我的编译器是否实际内联了类似的函数。

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

if (RUNNING_ON_VALGRIND)

而不是

#if RUNNING_ON_VALGRIND

..出于调试目的,因为你可以在调试器中看到if()而不是#if。然后我们深入#ifdef vs #if。

如果它的代码不足10行,请尝试内联它。如果无法内联,请尝试优化它。如果它太愚蠢而不是一个函数,那就制作一个宏。

答案 17 :(得分:0)

我没有看到有人提到这一点,关于像宏这样的功能,例如:

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

一般情况下,建议在不需要时避免使用宏,原因很多,可读性是主要问题。所以:

  

什么时候应该在功能上使用这些?

几乎从来没有,因为有一个更易读的替代方案是inline,请参阅https://www.greenend.org.uk/rjk/tech/inline.htmlhttp://www.cplusplus.com/articles/2LywvCM9/(第二个链接是一个C ++页面,但就我所知,这一点适用于c编译器。)

现在,略有不同的是,宏由预处理器处理,内联由编译器处理,但现在没有实际区别。

  

何时适合使用这些?

适用于小功能(最多两个或三个衬垫)。目标是在程序运行期间获得一些优势,因为像宏(和内联函数)这样的函数是在预处理期间完成的代码替换(或者在内联的情况下编译)并且不是存储在内存中的真实函数,所以没有函数调用开销(链接页面中的更多细节)。