78

我已经写了一点 C 语言,我可以很好地阅读它以大致了解它在做什么,但是每次我遇到一个宏时,它都让我彻底崩溃。我最终不得不记住宏是什么,并在阅读时将其替换在脑海中。我遇到的那些直观易懂的总是像小函数一样,所以我一直想知道为什么它们不只是函数。

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

为什么要为 C 引入如此复杂的预处理器?有没有人有一个使用它的例子,这会让我明白为什么它似乎仍然用于#debug 样式条件编译以外的其他目的?

编辑:

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

4

18 回答 18

56

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

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

我遇到的那些直观易懂的总是像小函数一样,所以我一直想知道为什么它们不只是函数。

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

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

将适用于任何类型的<操作员。

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

在 C99 中,宏还允许您调用可变参数函数,例如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. 如果守卫为真,它会输出消息以及打印消息的文件和行号。如果它是一个函数调用,它不会知道你从中调用它的文件和行,并且使用 avaprintf会多一些工作。

于 2009-03-17T11:46:50.130 回答
19

通过比较C宏的几种使用方式以及如何在D.

从 DigitalMars.com 复制

早在C发明之时,编译器技术还很原始。在前端安装文本宏预处理器是添加许多强大功能的简单直接的方法。程序的规模和复杂性不断增加,这表明这些特性伴随着许多固有的问题。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;
      }
      

DigitalMars 网站上有更多示例。

于 2009-03-18T00:26:56.130 回答
16

它们是 C 之上的一种编程语言(一种更简单的语言),因此它们对于在编译时进行元编程很有用......换句话说,您可以编写宏代码,以更少的行数和时间生成 C 代码直接用C写。

它们对于编写“多态”或“重载”的“函数式”表达式也非常有用;例如,一个 max 宏定义为:

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

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

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

于 2009-05-14T11:03:56.480 回答
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()、无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 } } }

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

为了避免真正使事情复杂化并避免暗示糟糕的编码风格,我不会给出在不同的、不兼容的操作系统中编译的代码示例。为此使用您的交叉构建系统,但应该清楚的是,预处理器允许在没有构建系统帮助的情况下实现这一点,也不会因为缺少接口而中断编译。

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

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

于 2011-11-09T07:00:33.467 回答
11

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

此外,宏是您可以在编译时执行诸如将文件和行插入字符串常量之类的技巧的唯一方法。

这些天来,宏曾经是唯一方法的许多事情通过更新的机制得到了更好的处理。但他们仍然有自己的位置,有时。

于 2009-03-17T11:39:07.637 回答
8

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

预处理器宏实际上提供了在编译时执行的图灵完备语言。一个令人印象深刻(也有点吓人)的例子在 C++ 端结束了:Boost 预处理器库使用C99 / C++98预处理器来构建(相对)安全的编程结构,然后将其扩展到任何底层声明和代码您输入,无论是 C 还是 C++。

在实践中,我建议将预处理器编程作为最后的手段,当您没有自由在更安全的语言中使用高级构造时。但有时,如果你的背靠在墙上,而黄鼠狼正在逼近……,知道你能做什么是件好事!

于 2009-03-17T12:22:43.243 回答
7

来自计算机愚蠢

我在许多适用于 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_19752BIT_10425
#define
bit_24 8388608
#define bit_24 16777216
#define bit_26
67108864
#define
bit_28 268435456
#define bit_29 536870912
#define bit_301073741824
#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 0x4000000000
#define bit_31 0x4000000000

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

#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 语言也有参数化的宏。您真正需要的是:

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

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

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

于 2011-11-09T07:04:55.090 回答
6

我将补充已经说过的内容。

因为宏适用于文本替换,它们允许您做非常有用的事情,而这些事情是使用函数无法做到的。

以下是宏真正有用的几种情况:

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

在这里,如果另一个程序员在声明中再添加五个元素并不重要afor-loop 将始终遍历所有元素。

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

这里的意图更清楚了!

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

于 2014-10-13T18:58:49.890 回答
5

宏真正发挥作用的一种情况是使用它们进行代码生成。

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

于 2009-03-17T11:46:50.693 回答
4

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

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

于 2009-03-18T01:07:23.087 回答
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;
} 
于 2013-06-20T10:23:20.277 回答
3

它有利于内联代码并避免函数调用开销。如果您想稍后更改行为而不编辑很多地方,也可以使用它。它对复杂的事情没有用,但对于你想要内联的简单代码行,它还不错。

于 2009-03-17T11:35:27.897 回答
2

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

这里给出了如何使用宏来管理数据结构的详细解释 - http://multi-core-dump.blogspot.com/2010/11/interesting-use-of-c-macros-polymorphic.html

于 2011-11-05T18:57:28.697 回答
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用作脚本的字符串和编译器的字段名称。那不好吗?是的。如果您重构您的代码并重命名LocalNameRelativeFolderName(就像我所做的那样)并且忘记对字符串执行相同的操作(就像我所做的那样),那么脚本将以您不期望的方式工作(实际上,在我的示例中它取决于你是不是忘记在单独的脚本文件中重命名字段,但是如果脚本用于序列化,那将是一个 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)。所以,明智地使用它。如果您看到另一种摆脱复制粘贴代码(例如函数)的方法,请使用这种方法。如果您发现使用宏删除复制粘贴的代码不值得,请保留复制粘贴的代码。

于 2014-11-20T16:35:22.823 回答
1

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

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

于 2009-03-17T11:36:41.810 回答
0

宏 .. 当您的 &#(*$& 编译器拒绝内联某些内容时。

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

严肃地说,谷歌预处理器滥用(你可能会看到一个与#1 结果类似的 SO 问题)。如果我正在编写一个超出 assert() 功能的宏,我通常会尝试查看我的编译器是否真的会内联类似的函数。

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

if (RUNNING_ON_VALGRIND)

而不是

#if RUNNING_ON_VALGRIND

.. 出于调试目的,因为您可以在调试器中看到 if() 但看不到 #if。然后我们深入研究#ifdef 与#if。

如果它的代码少于 10 行,请尝试内联它。如果不能内联,试着优化一下。如果它太傻而不能成为一个函数,那就做一个宏。

于 2009-03-17T11:33:12.177 回答
0

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

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

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

于 2009-03-17T11:42:05.977 回答
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 编译器)。

现在,细微的差别是宏由预处理器处理,而内联由编译器处理,但现在没有实际区别。

什么时候适合使用这些?

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

于 2017-12-22T16:54:42.660 回答