当前位置: 首页 > 知识库问答 >
问题:

理解C的预处理器在宏间接扩展自身时的行为

施彦
2023-03-14

当我在一个大型项目中工作时,我偶然发现了一个错误,在这个错误中,一个宏没有正确地扩展。结果输出是“EXPAND(0)”,但EXPAND定义为“#define EXPAND(X)X”,因此很明显输出应该是“0”。

“没问题”,我心想。“这可能是一些愚蠢的错误,毕竟这里有一些讨厌的宏,有很多地方会出错”。正如我所想的那样,我将行为不端的宏隔离到它们自己的项目中,大约200行,并开始在MWE上工作以确定问题所在。200行变成了150,又变成了100,然后是20,10……令我震惊的是,这是我最后的MWE:

#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
   
EXPAND(TEST PARENTHESIS()) // EXPAND(0)

4行。

雪上加霜的是,几乎对宏的任何修改都能使它们正常工作:

#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)

// Manually replaced PARENTHESIS()
EXPAND(TEST ()) // 0
#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)

// Manually replaced TEST()
EXPAND(EXPAND(0)) // 0
// Set EXPAND to 0 instead of X
#define EXPAND(X) 0
#define PARENTHESIS() ()
#define TEST() EXPAND(0)

EXPAND(TEST PARENTHESIS()) // 0

但最重要也是最奇怪的是,下面的代码以完全相同的方式失败:

#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
   
EXPAND(EXPAND(EXPAND(EXPAND(TEST PARENTHESIS())))) // EXPAND(0)

这意味着预处理器完全能够扩展expand,但是由于某种原因,它绝对拒绝在最后一步中再次扩展它。

现在,我将如何在我的实际程序中解决这个问题,这不是现在也不是现在。虽然解决方案是不错的(例如,将标记expand(TEST parenthese())扩展到0)的方法,但我最感兴趣的是:为什么?为什么C预处理器在第一种情况下得出结论“扩展(0)”是正确的扩展,而在其他情况下却不是?

虽然很容易找到C预处理器的功能(以及您可以使用它的一些魔法),但我还没有找到一个解释它是如何实现的,我想借此机会更好地理解预处理器是如何完成它的工作的,以及它在扩展宏时使用什么规则。

那么,鉴于此:预处理器决定将最终宏扩展为“expand(0)”而不是“0”背后的原因是什么?

共有2个答案

江文斌
2023-03-14

针对这种情况,宏替换有三个相关步骤:

  1. 对参数执行宏替换。
  2. 用宏的定义替换宏,参数用参数替换。
  3. 重新扫描结果以便进一步替换,同时取消替换的宏名称。

展开(测试括号()):

  • 步骤1,对展开的参数、测试括号()执行宏替换:
    • test后面没有括号,因此它不被解释为宏调用。
    • parenthese()是一个宏调用,因此执行三个步骤:参数为空,因此不对它们进行处理。则括号()将替换为()。然后重新扫描()并且找不到宏。
    • 步骤1已经完成,我们有expand(TEST())。(test()未重新扫描,因为它不是任何宏替换的结果。)
    • 步骤1,参数为空,因此不对其进行处理。
    • 步骤2,将测试()替换为展开(0)
    • 步骤3,重新扫描展开(0),但取消显示展开

    展开(TEST())中:

    • 步骤1,对expand的参数执行宏替换:
      • 第1步,test的参数为空,因此不进行处理。
      • 步骤2,将测试()替换为展开(0)
      • 步骤3,重新扫描此替换,并用0替换expand(0)

      问题中的其他例子也类似。归结起来就是:

      • 测试括号()中,测试后缺少括号,导致在处理封闭宏调用的参数时无法展开。
      • 展开括号时,括号会放在它后面,但这是在扫描测试之后,并且在处理参数期间不会重新扫描它。
      • 替换封闭宏后,test将被重新扫描并替换,但此时封闭宏的名称将被取消。

赵永新
2023-03-14

宏扩展是一个复杂的过程,只有通过理解发生的步骤才能真正理解。

>

  • 当识别具有参数的宏(宏名称标记后跟(标记)时,扫描并拆分以下标记,直到匹配的)(在,标记上)。在此过程中不会发生宏扩展(因此s和)必须直接存在于输入流中,而不能存在于其他宏中)。

    每个宏参数,如果其名称出现在宏正文中,前面没有###或后面没有##或后面没有##,则“预存储”宏,以便宏展开--在替换到宏正文之前,所有参数中的宏都将被递归展开。

    生成的宏参数令牌流被替换到宏的主体中。###参数中涉及的参数将基于步骤1中的原始解析器标记进行修改(字符串化或粘贴)和替换(对于这些参数,不执行步骤2)。

    再次扫描生成的宏主体令牌流,以查找要展开的宏,但忽略当前正在展开的宏。此时,输入中的其他令牌(在步骤1中扫描和解析的内容之后)可以作为任何被识别的宏的一部分包括在内。

    重要的是,有两个不同的递归展开(上面的步骤2和步骤4),只有步骤4中的一个忽略相同宏的递归宏展开。步骤2中的递归展开不会忽略当前宏,因此可以递归展开它。

    对于上面的例子,让我们看看会发生什么。对于输入

    EXPAND(TEST PARENTHESIS())
    
    • 步骤1查看宏expand并在参数列表测试括号()中扫描x
    • 步骤2不将测试识别为宏(没有以下(),但识别括号:
      • 步骤1(嵌套)获取参数的空令牌序列
      • 步骤2扫描空序列ad不执行任何操作
      • 步骤3将()插入到宏主体中,结果正好是:()
      • 步骤4扫描()中的宏,但未找到任何宏
      • 步骤1获取参数的空序列
      • 步骤2不执行任何操作
      • 步骤3代入给出展开(0)
      • 的正文
      • 步骤4递归扩展了该功能,取消test。此时,expandtest都被抑制(由于处于步骤4扩展中),因此不会发生任何事情

      您的另一个示例expand(TEST())不同

      • 步骤1expand被识别为宏,test()被解析为参数x
      • 步骤2,递归解析此流。请注意,因为这是步骤2,所以expand不会被取消
        • 步骤1测试被识别为具有空序列参数的宏
        • 步骤2--无(空令牌序列中无宏)
        • 步骤3,替换为bosy给出展开(0)
        • 步骤4,抑制test并递归展开结果
          • 第1步,expand被识别为宏(请记住,此时只有test被步骤4递归抑制--expand在步骤2递归中,因此不被抑制),其参数为0
          • 步骤2,扫描0,但不会发生任何事情
          • 步骤3,代入正文给出0
          • 步骤4,再次扫描0以查找宏(但再次未发生任何情况)

          所以这里的最终结果是0

  •  类似资料:
    • 我正在尝试实现一个正确的枚举到字符串宏,它将自动实现枚举值和代码中的名称之间的关联。 例如,我想定义一个名为“test”的新宏,如下所示: 这样,通过调用,我可以访问字符串。目前,这个的当前实现如下所示: 虽然当我这样做时,处理的枚举没有得到正确的名称分隔: 因为我希望使用而不是。 有没有办法用逗号分隔来展开名称? 干杯!

    • 本文向大家介绍深入理解C预处理器,包括了深入理解C预处理器的使用技巧和注意事项,需要的朋友参考一下 C 预处理器不是编译器的组成部分,是编译过程中一个单独的步骤。C预处理器只是一个文本替换工具,它会指示编译器在实际编译之前完成所需的预处理。 所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。 下表包含所有重要的预处理器指令: 指令 描述 #

    • 预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。 所有的预处理器指令都是以井号(#)开头,只有空格字符可以出现在预处理指令之前。预处理指令不是 C++ 语句,所以它们不会以分号(;)结尾。 我们已经看到,之前所有的实例中都有 #include 指令。这个宏用于把头文件包含到源文件中。 C++ 还支持很多预处理指令,比如 #include、#define、#if、#else、#line

    • #include <stdio.h> #define NAME "Joe" int main() { printf ("Hello %s\n", NAME); return 0; } 技巧 使用gcc -g编译生成的程序,是不包含预处理器宏信息的: (gdb) p NAME No symbol "NAME" in current context. 如果想在gdb中查看宏信息,可以使

    • 本文向大家介绍C#中的预处理器指令详解,包括了C#中的预处理器指令详解的使用技巧和注意事项,需要的朋友参考一下 目录 1. #define 和 #undef 2. #if、#elif、#else 和#endif 3. #warning 和 #error 4. #region 和#endregion 5. #line 6. #pragma   C#中有许多名为“预处理器指令”的命令。这些命令从来不会

    • 主要内容:1. 预处理器示例,2. 预定义的宏,3. 预处理器运算符,4. 参数化宏Objective-C预处理器不是编译器的一部分,而是编译过程中的一个单独步骤。 简单来说,Objective-C预处理器只是一个文本替换工具,它指示编译器在实际编译之前进行必要的预处理。 我们将Objective-C预处理器称为OCPP。 所有预处理器命令都以井号()开头。它必须是第一个字符(前面不能有空格),并且为了便于阅读,预处理器指令应该从第一列开始。 以下部分列出了所有重要的预处理程序指