C语言篇:宏

宏是最常见的预处理指令,它的作用是对源文件做文本替换。

普通宏

宏定义的基本语法是:

c 复制代码
#define 标识符 替换列表

宏的作用域从定义位置开始,直到文件结束或被#undef指令结束。每当下文出现这个标识符时,都会被替换为替换列表中的文本。

比如:

c 复制代码
ONE;
#define ONE 1
ONE;

预处理之后会变成:

c 复制代码
ONE;

1;

在翻译阶段中,处理注释和空白字符在预处理之前,而且在之后的编译阶段所有空白字符都被忽略,所以宏定义中的空白字符只要能分隔元素就行,多余的空白字符没有意义。

比如:

c 复制代码
#define NUM     1+   2    
(NUM);

gcc预处理之后为:

c 复制代码
(1+ 2);

宏替换可以递归进行,从而实现高级抽象。

比如:

c 复制代码
#define THREE 3
#define NINE THREE * THREE
NINE;

预处理之后为:

c 复制代码
3 * 3;

递归替换过程中,宏的定义顺序是无所谓的,但是每个宏只能被替换一次。


比如:

c 复制代码
#define NINE THREE * THREE
#define THREE 3
NINE;

在定义宏NINE时,THREE还没有定义,但在对NINE做替换时,两个宏都已经被定义了,所以预处理的结果和上文相同。


比如:

c 复制代码
#define FIVE FIVE + 0
FIVE;

预处理的结果为:

c 复制代码
FIVE + 0;

因为FIVE首先被替换为FIVE + 0,但这个宏已经被替换过一次,所以不能再替换了。这样可以避免无限递归。


比如:

c 复制代码
#define FOUR FIVE - 1
#define FIVE FOUR + 1
FOUR;
FIVE;

预处理的结果为:

c 复制代码
FOUR + 1 - 1;
FIVE - 1 + 1;

对于FOUR,它首先被替换为FIVE - 1,再把其中的FIVE替换为FOUR + 1,变成了FOUR + 1 - 1;,这时FOUR已经被替换过,所以递归终止。

FIVE同理。


由于宏只是简单的文本替换,不会进行C语言的语法检查,所以可以实现一些怪东西。

比如:

c 复制代码
#define INC + 1
int a = 0;
int b = a INC;

预处理的结果为:

c 复制代码
int a = 0;
int b = a + 1;

这种东西看看就行了,实用价值基本没有,还会大大降低代码的可读性和可维护性。原来的代码乍一看都不像是C语言。

在C语言中,宏经常被用来定义常量。因为上述原因,当替换列表是一个表达式时,为了避免运算优先级或结合性出现意外,会给每个宏以及整体添加括号。同时宏名习惯上使用全大写,提醒程序员注意。


虽然宏可以在一定程度上改变语法,但也是有限制的。预处理在语法分析之前,但在词法分析之后,此时每个运算符、标识符、字面量、关键字、空白字符都已经被处理为记号,无法分割或重组。

比如:

c 复制代码
#define PREFIX p
int PREFIX_a;

PREFIX_a并不会变成p_a,因为PREFIX_a是一个独立标识符,不会进行部分宏替换。


比如:

c 复制代码
#define LESS <
1 LESS= 2;

预处理的结果为:

c 复制代码
1 < = 2;

这是一个非法表达式,因为两个运算符缺少操作数。即便把LESS替换为<,且LESS=之间没有空格,也无法组合出新的运算符。


宏不能重复定义,除非两次定义完全相同。

如果要改变宏的定义,可以先用#undef预处理指令取消宏定义。语法为:

c 复制代码
#undef 标识符

如果标识符不是宏,则这条指令无效,且不会报错。

宏的替换列表可以为空,此时替换相当于删除。

带参数的宏

宏可以带参数,它的定义和使用看起来就像函数。

基本语法是:

c 复制代码
// 定义
#define 标识符(参数列表) 替换列表

// 使用
标识符(参数列表)

定义中的参数列表是由逗号分隔的标识符,宏替换时,替换列表中每个在参数列表中出现的标识符都会被替换为使用时的实际参数。

使用中的参数列表是由逗号分隔的记号序列,预处理时仅检查参数数量是否匹配,不会检查内容是否符合C语言语法。

如前所述,除了起分隔作用的空白字符,多余空白字符都没有意义。

比如:

c 复制代码
#define ADD(X, Y) X + Y
ADD(0 + 1, 1 * 2);

预处理的结果为:

c 复制代码
0 + 1 + 1 * 2;

在使用带参数的宏时,可以使用圆括号()避免参数被分隔。其他括号(比如{}[])无效。

比如:

c 复制代码
#define ADD(X, Y) X + Y
ADD((1, 2), 3);
ADD({1, 2});

预处理的结果为:

c 复制代码
(1, 2) + 3;
{1 + 2};

第一个ADD中,由于存在(),里面的逗号不作为参数的分隔符,而是作为参数的一部分。

第二个ADD没有(),所以{1作为第一个参数,2}作为第二个参数。


参数列表可以为空,此时跟不带参数的宏作用相同,但使用时的括号不能省略。

c 复制代码
#define NUM() 1
NUM();

预处理的结果为:

c 复制代码
1;

使用时每个实际参数都可以为空,但逗号不能省略。

c 复制代码
#define ADD(X, Y) X + Y
ADD(1,);
ADD(,);

预处理的结果为:

c 复制代码
1 + ;
 + ;

带参数的宏经常被当作内联函数来使用,但两者还是有很大区别的。函数调用会先对实际参数求值,然后赋值给形式参数;而宏只是简单的文本替换,这意味着参数可能会被多次求值。

对于求值缓慢的参数,多次求值会损害程序性能,所以带参数的宏不一定比传统的函数调用快。

比如:

c 复制代码
#define CUBE(X) X * X * X
int factorial(int x);
CUBE(factorial(10));

预处理结果是:

c 复制代码
int factorial(int x);
factorial(10) * factorial(10) * factorial(10);

阶乘是一个比较耗时的过程,而CUBE宏需要调用这个函数3次,显然效率低于函数实现。


多次求值也意味着会执行多次副作用,这往往会引入BUG。

比如:

c 复制代码
#define CUBE(X) X * X * X
int a = 5;
CUBE(a++);

预处理结果是:

c 复制代码
int a = 5;
a++ * a++ * a++;

CUBE(a++)的值是不确定的,这取决于副作用的执行时机。而且a最后的值是8,从原代码看比较反直觉。

可变参数的宏

可变参数宏是C99引入的功能。

当宏定义中最后一个参数是...时,这就是一个可变参数宏。当使用可变参数宏时,超出命名参数数量的所有实际参数都被合并为一个参数,在替换列表中用__VA_ARGS__表示。

比如:

c 复制代码
#define VAM(X, ...) X(__VA_ARGS__)
VAM(f, 1, 2, 3);

预处理的结果为:

c 复制代码
f(1, 2, 3);

当然参数列表可以只有...


因为__VA_ARGS__是一个整体,而且里面的参数都以逗号分隔,所以可变参数宏经常和可变参数函数配合使用。

比如:

c 复制代码
#define SUM_PLUS_5(N, ...) sum(N + 1, 5, __VA_ARGS__)
int sum(int n, ...);
SUM_PLUS_5(3, 1, 2, 3);

其中sum是一个可变参数函数,计算多个未命名参数的和,参数n表示一共有多少个未命名参数。SUM_PLUS_5多传一个5进行计算。

预处理的结果是:

c 复制代码
int sum(int n, ...);
sum(3 + 1, 5, 1, 2, 3);

上面的代码其实存在一个缺陷,当__VA_ARGS__为空时,替换列表中__VA_ARGS__前的逗号还在,而C语言的函数不允许参数列表有后缀逗号。

比如:

c 复制代码
#define SUM_PLUS_5(N, ...) sum(N + 1, 5, __VA_ARGS__)
int sum(int n, ...);
SUM_PLUS_5(0);

预处理的结果是:

c 复制代码
int sum(int n, ...);
sum(0 + 1, 5, );

因为参数列表存在后缀逗号,sum(0 + 1, 5, )根本不是一个合法的函数调用,编译时会报错。


为了解决这个问题,C23 引入了__VA_OPT__,它后面跟一个括号(),只有当__VA_ARGS__不为空时,括号内的内容才会被扩展,否则它自身连同括号内的内容都被忽略。

比如:

c 复制代码
#define SUM_PLUS_5(N, ...) sum(N + 1, 5 __VA_OPT__(,) __VA_ARGS__)
int sum(int n, ...);
SUM_PLUS_5(3, 1, 2, 3);
SUM_PLUS_5(0);

预处理的结果为:

c 复制代码
int sum(int n, ...);
sum(3 + 1, 5 , 1, 2, 3);
sum(0 + 1, 5 );

这次把逗号,放在了__VA_OPT__的括号内。当__VA_ARGS__不为空时,预处理结果和之前相同;当__VA_ARGS__为空时,逗号被省略,从而得到了一个合法的函数调用。

#操作符

操作符#只能用于带参数的宏。它出现在替换列表中,后跟一个参数标识符,作用是把这个参数转换为字符串字面量。

比如:

c 复制代码
#define STR1(X) #X
#define STR2(...) #__VA_ARGS__
#define STR3(X, Y) #X, #Y
STR1(1 + 2);
STR2(1, 2, 3);
STR3(a b, c d);

预处理的结果为:

c 复制代码
"1 + 2";
"1, 2, 3";
"a b", "c d";

C语言标准并没有说明普通宏和带参数宏的处理顺序。

比如:

c 复制代码
#define NUM 1
#define STR(X) #X
STR(NUM);

预处理结果可能是"1";,也可能是"NUM";

##操作符

上文说过宏不会修改词法记号,但有一个例外,操作符##专门用于连接两个词法记号,从而形成新的词法记号。

比如:

c 复制代码
#define NUM 1 ## 2
#define ID a ## b ## c
NUM;
ID;

预处理的结果为:

c 复制代码
12;
abc;

对于带参数的宏,会先进行参数替换,再进行连接。

比如:

c 复制代码
#define JOIN(X, Y) X ## Y
JOIN(1, 2);
JOIN(a, b);
JOIN(<, =);

预处理的结果是:

c 复制代码
12;
ab;
<=;

程序员必须自己保证连接后是一个合法的词法记号,否则该行为是未定义的。

比如对于上面的宏,JOIN(<, a)就是未定义的,因为<a不是一个合法的词法记号,编译器一般会报错。

不过因为宏的参数是记号序列,只需要保证与##相邻的记号可以连接就行,而不是整个参数。

比如:

c 复制代码
#define JOIN(X, Y) X ## Y
JOIN(1 + a, b);
JOIN(a <, = b);
JOIN(a == 1, 1);

预处理的结果是:

c 复制代码
1 + ab;
a <= b;
a == 11;

##连接后的标识符也可以进行递归宏替换。

比如:

c 复制代码
#define AB 123
#define JOINED_AB A ## B
#define JOIN(X, Y) X ## Y
JOINED_AB;
JOIN(A, B);

预处理的结果是:

c 复制代码
123;
123;

预定义宏

C语言标准提供了一些预定义宏帮助我们调试,这些宏都是由编译器确定其内容并进行替换。

  • __FILE__:文件名
  • __LINE__:行号
  • __DATE__:日期
  • __TIME__:时间

此外还有一些宏,用于表示编译器是否支持某个语言功能。

比如__STDC_NO_VLA__,如果编译器不支持VLA,则必须定义这个宏。当我们检测到这个宏已经被定义时,就应该避免使用VLA。

相关推荐
Lonble7 小时前
C语言篇:翻译阶段
c语言·c
用户6120414922131 天前
C语言做的文本词频数量统计功能
c语言·后端·敏捷开发
小莞尔4 天前
【51单片机】【protues仿真】基于51单片机的篮球计时计分器系统
c语言·stm32·单片机·嵌入式硬件·51单片机
小莞尔5 天前
【51单片机】【protues仿真】 基于51单片机八路抢答器系统
c语言·开发语言·单片机·嵌入式硬件·51单片机
liujing102329295 天前
Day03_刷题niuke20250915
c语言
第七序章5 天前
【C++STL】list的详细用法和底层实现
c语言·c++·自然语言处理·list
l1t5 天前
利用DeepSeek实现服务器客户端模式的DuckDB原型
服务器·c语言·数据库·人工智能·postgresql·协议·duckdb
l1t5 天前
利用美团龙猫用libxml2编写XML转CSV文件C程序
xml·c语言·libxml2·解析器
Gu_shiwww5 天前
数据结构8——双向链表
c语言·数据结构·python·链表·小白初步