C/C++中宏/Macro的深入讲解

前言

宏(Macro)本质上就是代码片段,通过别名来使用。在编译前的预处理中,宏会被替换为真实所指代的代码片段,即下图中 Preprocessor 处理的部分。

C/C++ 代码编译过程 - 图片来自 ntu.edu.sg

根据用法的不同,分两种,Object-like 和 Function-like。前者用于 Object 对象,后者用于函数方法。

C/C++ 代码编译过程中,可通过相应参数来获取到各编译步骤中的产出,比如想看被预处理编译之后的宏,使用 gcc 使加上 -E 参数。

$ gcc -E macro.c

宏的定义

通过 #define 指令定义一个宏。

#define NAME_OF_MACRO value

比如,以下代码定义了一个名为 BUFFER_SIZE 的宏,指代 1024 这个数字。

#define BUFFER_SIZE 1024

使用时,

foo = (char *) malloc (BUFFER_SIZE);

使用预处理器编译:

$ gcc -E test.c

编译结果:

foo = (char *) malloc (1024);

多行

宏的定义是跟随 #define 在一同一行内的,但可通过 反斜杠 \ 实现换行从而定义出多行的宏。


多行的宏经过编译后会还原到一行中。

test.c

#include <stdio.h>
#define GREETING_STR \
 "hello \
world"

int main() { printf(GREETING_STR); }

编译后:

int main() {
 printf("hello world");
 return 0;
}

宏展开时的顺序

宏的展开是在处理源码时按照其出现位置进行的,如果宏定义有嵌套关系,也是层层进行展开,比如:

#include <stdio.h>

#define GREETING_NAME "wayou"
#define GREETING "hello," GREETING_NAME

int main() {
 printf(GREETING);
 return 0;
}

首先遇到 GREETING,将其展开成 GREETING_NAME "wayou",然后发现另一个宏 GREETING_NAME,将其展开最后得到 "hello," "wayou"。所以编译后的代码为:

int main() {
 printf("hello," "wayou");
 return 0;
}

其展开的顺序并不是宏定义时的顺序,为了验证,可将上面示例代码中两个宏的定义调换一下,得到:

-#define GREETING_NAME "wayou"
#define GREETING "hello," GREETING_NAME
+#define GREETING_NAME "wayou"

再次编译查看产出,会发现没有区别,也不会报 GREETING 中所依赖的 GREETING_NAME 找不到的错。其实 #define 只是告诉编译器定义了这么个宏,而具体的求值,则是使用宏的地方才开始的。

像下面这样,当宏存在覆盖时,会以新的为准,其结果为 37。

#define BUFSIZE 1020
#define TABLESIZE BUFSIZE
#undef BUFSIZE
#define BUFSIZE 37

Object-like 宏

Object-like 类型的宏看起来就像普通的数据对象,故名。多用于数字常量的情形下。且宏名一般使用全大写形式方便识别。像上面示例中,都是 Object-like 的。

Function-like 宏

也可定义出使用时像是方法调用一样的宏,这便是 Function-like 类型的宏。

#define lang_init() c_init()
lang_init()

// 编译后
c_init()

函数类型的宏只在以方法调用形式使用时才会被展开,即名称后加括号,否则会被忽略。当宏名和函数名重名时,这一策略就会显得有用了,比如:

extern void foo(void);
#define foo() /* optimized inline version */
…
 foo();
 funcptr = foo;

这里 foo() 的调用会来自宏里面定义的那个函数,而 funcptr 会正确地指向函数地址,如果后者也被宏展开,则成了 funptr=foo() 显然就不对了。

函数类型的宏在定义时需注意,宏名与后面括号不能有空格,否则就是普通的 Object-like 类型对象。

#define lang_init ()  c_init()
lang_init()

// 编译后:
() c_init()()

宏的参数

函数类型的宏,可以像正常函数一样指定入参,入参需为逗号分隔合法的 C 字面量。

#define min(X, Y) ((X) < (Y) ? (X) : (Y))
 x = min(a, b);     → x = ((a) < (b) ? (a) : (b));
 y = min(1, 2);     → y = ((1) < (2) ? (1) : (2));
 z = min(a + 28, *p);  → z = ((a + 28) < (*p) ? (a + 28) : (*p));

入参中的括号

入参中只需要括号对称,但不要求方括号或花括号成对出现,所以下面的代码:

macro (array[x = y, x + 1])

其入参实际为 array[x = y 和 x + 1]。

入参的展开

入参本质上也是宏,对象类型的宏,在函数宏展示时,这些参数也被展示到了函数宏的函数体里。

 min (min (a, b), c)

首先被展开成:

min (((a) < (b) ? (a) : (b)), (c))

然后进一步展开成(此处换行为方便阅读,实际编译后没有):

((((a) < (b) ? (a) : (b))) < (c)
 ? (((a) < (b) ? (a) : (b)))
 : (c))

参数的缺省

函数宏在使用时其入参可缺省,但不能全部缺省,至少提供一个入参。

min(, b)    → ((  ) < (b) ? (  ) : (b))
min(a, )    → ((a ) < ( ) ? (a ) : ( ))
min(,)     → ((  ) < ( ) ? (  ) : ( ))
min((,),)    → (((,)) < ( ) ? ((,)) : ( ))

min()   error→ macro "min" requires 2 arguments, but only 1 given
min(,,)  error→ macro "min" passed 3 arguments, but takes just 2

字符化/Stringizing

如果函数宏中入参在字符串中,是不会被展开的,它就是普通的字符串字面量,这样的结果是符合预期的。

#define foo(x) x, "x"
foo(bar)    → bar, "x"

但如果确实想将入参展开成字符串,可在使用入参时,加上 # 前缀。

#define WARN_IF(EXP) \
do { if (EXP) \
    fprintf (stderr, "Warning: " #EXP "\n"); } \
while (0)
WARN_IF (x == 0);
   → do { if (x == 0)
      fprintf (stderr, "Warning: " "x == 0" "\n"); } while (0);

此处 #EXP 在字符串中会被正确展开。What's more, 如果这里的 x 也是宏,那只会在 if 语句中进行展开。

拼接

通过 ## 可将两个宏展开成一个,即将两者进行了拼接,这种操作叫 "token pasting",或 "token concatenation",就是拼接嘛。

宏拼接一般用在需要拼接的宏是来自宏参数的情况,其他情况,大可直接将两个宏写在一起即可,用不着 ## 指令。

考察下面这个场景,其中命令名重复出现:

struct command
{
 char *name;
 void (*function) (void);
};

struct command commands[] =
{
 { "quit", quit_command },
 { "help", help_command },
 …
};

通过定义宏配合拼接,可达到精简代码的目的:

#define COMMAND(NAME) { #NAME, NAME ## _command }

struct command commands[] =
{
 COMMAND (quit),
 COMMAND (help),
 …
};

不定参数

像普通函数一样,函数类型的宏也可定义接收不定参数。

#define eprintf(…) fprintf (stderr, __VA_ARGS__)

调用时,命名参数后面,包括逗号都会进入到 __VA_ARGS__ 关键字当中。但 C++ 中还支持对这些参数命名从而不用 __VA_ARGS__。

eprintf ("%s:%d: ", input_file, lineno)

// 编译后:
fprintf (stderr, "%s:%d: ", input_file, lineno)

C++ 中可这么写:

#define eprintf(args…) fprintf (stderr, args)

不定参数与命名参数混合的情况

不定参数为命名参数后面省略的部分。

#define eprintf(format, …) fprintf (stderr, format, __VA_ARGS__)

预设的宏

标准库及编译器中预设了一些有用的宏,可以在这里 查阅。

取消和重置宏

当某个宏不再使用时,可通过 #undef 将取注销掉。#undef 后紧跟宏名,后面不要跟其他东西,即使是函数类型的宏。

#define FOO 4
x = FOO;    → x = 4;
#undef FOO
x = FOO;    → x = FOO;

两个宏相似的定义

满足以下条件时,我们认为两者是相似的:

  • 类型相同,比如同为对象类型,或函数类型的宏
  • 展开后各位置的符号(token)相同
  • 如果是函数宏,入参相同
  • 空白的不限但出现的位置相同

比如,下面这些是相似的:

#define FOUR (2 + 2)
#define FOUR     (2  +  2)
#define FOUR (2 /* two */ + 2)

而下面这些则不然:

#define FOUR (2 + 2)
#define FOUR ( 2+2 ) // 空白位置不一样
#define FOUR (2 * 2) // 宏的内容不一样
#define FOUR(score,and,seven,years,ago) (2 + 2) // 入参不一样

宏重复定义时的表现

对于使用了 #undef 注销过的宏,再次定义同名的宏时,要求新定义的宏不与老的相似。

而如果说一个已经存在的宏,并没有注销,重复定义时,如果相似,则新的定义会忽略,如果不相似,编译器会报警告同时使用新定义的宏。这允许在多个文件中定义同一个宏。

相关资源

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • C++十六进制宏的用法详解

    流行的用法:用二进制的每一位代表一种状态. 001,010,100这样就表示三种状态. 通过或|运算就可以组合各种状态. 001|010=011 001|010|100=111 通过与&运算可以去除某种状态. 111&001=110 可以定义这样的宏组合成函数的参数 #defineP10x001L//001 #defineP20x002L//010 #defineP30x004L//100 voidFunc(long){} Func(P1|P2); 可以这样判断某位是否是1 由于001与x

  • 简单讲解C++的内部和外部函数以及宏的定义

    C++内部函数和外部函数 函数本质上是全局的,因为一个函数要被另外的函数调用,但是,也可以指定函数只能被本文件调用,而不能被其他文件调用.根据函数能否被其他源文件调用,将函数区分为内部函数和外部函数. 内部函数 如果一个函数只能被本文件中其他函数所调用,它称为内部函数.在定义内部函数时,在函数名和函数类型的前面加static.函数首部的一般格式为: static 类型标识符 函数名(形参表); 如 static int fun(int a, int b); 内部函数又称静态(static)函数.

  • 浅析c++ 宏 #val 在unicode下的使用

    #define CHECK(condition) cout<<check failed:<<#condition<<endl; 上面这句宏,当你 CHECK(myfunc()); 时,假设myfunc返回false,会输出:check failed:myfunc() 在宏中,#condition 是把参数转换为字符串,这在打印log时,可以很方便的打印出函数名称等等 这个大家可能都知道了,太小儿科了,但是,当你在unicode下用的时候,很可能会出现乱码 解决的办法是

  • c++ 编程 几个有用的宏详解

    1. 打印错误信息 如果程序的执行必须要求某个宏被定义,在检查到宏没有被定义是可以使用#error,#warning打印错误(警告)信息,如: #ifndef __unix__ #error "This section will only work on UNIX systems" #endif 只有__unix__宏被定义,程序才能被正常编译. 2. 方便调试 __FILE, __LINE, __FUNCTION是由编译器预定义的宏,其分别代表当前代码所在的文件名,行号,以及函数名.

  • C++基础入门教程(二):数据、变量、宏等

    哎,木了个头的,这书太详细了~看得累人 前面部分太过基础了,很多语法方法的东西我也不重复记录了~ 今天才看看C++涉及到数据的一些基础点吧,我把书中稍微比较有营养的部分抽离出来 1.初始化 一般情况下,我们声明和初始化变量是这样的:int iNum = 10; 其实这是延续了C语言的语法,C++还有一种语法,是这样的:int iNum(10); 其实大部分人还是习惯用第一种吧(我也是),但如果哪天我们在别人的代码里看到第二种形式,不要被吓蒙了~ 2.宏定义 宏,用最粗俗,不,是最通俗的说法来解释

  • 在C++中自定义宏的简单方法

    可以使用宏定义没有返回值的"函数".例如: 复制代码 代码如下: #define PrintMax(a, b) \   do \   { \     int x = a, y = b; \     printf("Max: %d\n", x > y ? x : y);\   } while (0) // ... PrintMax(3, 4); 这样的"函数"与真正意义上的函数有本质的区别,因为宏是一个编译前行为,仅仅是编译前对文本进行替换.

  • C/C++中宏定义(#define)

    #define是C语言中提供的宏定义命令,其主要目的是为程序员在编程时提供一定的方便,并能在一定程度上提高程序的运行效率,但学生在学习时往往不能 理解该命令的本质,总是在此处产生一些困惑,在编程时误用该命令,使得程序的运行与预期的目的不一致,或者在读别人写的程序时,把运行结果理解错误,这对 C语言的学习很不利. 宏的定义在程序中是非常有用的,但是使用不当,就会给自身造成很大的困扰.通常这种困扰为:宏使用在计算方面. 本例子主要是在宏的计算方面,很多时候,大家都知道定义一个计算的宏,对于编译和编程

  • C/C++语言宏定义使用实例详解

     C/C++语言宏定义使用实例详解 1. #ifndef 防止头文件重定义 在一个大的软件工程里面,可能会有多个文件同时包含一个头文件,当这些文件编译链接成 一个可执行文件时,就会出现大量"重定义"的错误.在头文件中实用#ifndef #define #endif能避免头文件的重定义. 方法:例如要编写头文件test.h 在头文件开头写上两行: #ifndef TEST_H #define TEST_H //一般是文件名的大写 头文件结尾写上一行: #endif 这样一个工程文件里同时

  • C/C++宏定义的可变参数详细解析

    编写代码的过程中,经常会输出一些调试信息到屏幕上,一般会调用printf这类的函数.但是当调试解决之后,我们需要手工将这些地方删除或者注释掉.最近在看<Linux C编程一站式学习>这本书,就想到一个方法: 复制代码 代码如下: void myprintf(char* fmt, ...){}#ifdef DEBUG#define printf(fmt, args...) myprintf(fmt, ##args)#endif 调试阶段带着DEBUG调试,正式上线就可以把printf变成一个空函

  • C/C++ 宏详细解析

    众多C++书籍都忠告我们C语言宏是万恶之首,但事情总不如我们想象的那么坏,就如同goto一样.宏有一个很大的作用,就是自动为我们产生代码.如果说模板可以为我们产生各种型别的代码(型别替换),那么宏其实可以为我们在符号上产生新的代码(即符号替换.增加). 关于宏的一些语法问题,可以在google上找到.相信我,你对于宏的了解绝对没你想象的那么多.如果你还不知道#和##,也不知道prescan,那么你肯定对宏的了解不够. 我稍微讲解下宏的一些语法问题(说语法问题似乎不妥,macro只与preproc

随机推荐