一文搞定“宏”

一、宏和宏替换

宏(Macro)在编程中是一种特殊的代码片段。它在编译时被替换为指定的代码,这个替换过程就叫宏替换(macro substitution)。

宏的本义是“大”,英文macro源于希腊语“makros”,意为“大的,长的”。

宏只是一个字,macro只是一个词,但它却可以包罗万象。有一句话特地从远方赶来应景:一沙一世界,一叶一菩提。

宏就是那粒沙,那片叶。它的使命是:将一系列复杂代码块打包封装成一个简单命令。

在C语言中,用#define预处理器指令创建宏(macros)。

二、宏的分类及用法

宏分为两类:一类是不带参数的简单文本替换,叫类对象宏;一类是带有参数的宏,叫类函数宏。

1.类对象宏(Object-like macros

类对象宏是我们平时最常用的宏,这些宏代表值,并且不接受任何参数。

原型:

#define identifier replacement-list ( replacement-list 可选)

比如:

#define PI 3.14159265358979323846

注:应尽量使用const定义常量,因为宏没有类型检查。

2.类函数宏(Function-like macros

类函数宏也称带参数的宏,编译时会将替换列表(replacement-list)中所有出现的相应参数进行替换。对类函数宏的调用形式上与函数一样,所以叫类函数宏。

类函数宏才真正体现出宏的精髓:简化。它能通过一条简单的指令来完成一系列复杂的操作。同时它也具有函数一样的复用性,能够减少重复的代码。

它有3种形式:

#define identifier ( parameters ) replacement-list //指定所有参数
#define identifier ( parameters, ... ) replacement-list //部分指定参数
#define identifier ( ... ) replacement-list //不指定参数

(1)指定所有参数的宏替换

比如下面的for语句宏替换:

#define _for(i, start, end) for (int i = start; i < end; i++)

经过宏替换可以使代码变得更简洁,这样如果代码中有多个for循环,这种简洁的写法就能减少代码量。

(2)部分指定参数的宏替换

这时候需要用到一个占位符__VA_ARGS__,当调用宏时,会用宏参数...对应的实际参数把__VA_ARGS__替换掉。

示例代码:

#include<stdio.h>
#define PRINTF(format, ...) printf(format,__VA_ARGS__)
int main() {
    PRINTF("Hello, world!\n"); //此行代码会报错
    PRINTF("Number: %d\n", 42);
    PRINTF("Strings: %s and %s\n", "Hello", "world");
    return 0;
}

上代码中的第一行PRINTF("Hello, world!\n");是不能正常要印的。因为它只有一个参数,因此format会被替换为"Hello, world!\n",_VA_ARGS__会被替换为空,但二者之间还有个逗号被保留下来,因此替换后的代码为:

printf("Hello, world!\n",); //多了个逗号

要解决这个问题,就要用到__VA_ARGS__的好搭档:

__VA_OPT__ ( content )

它的作用是:如果__VA_ARGS__不为空,则将__VA_OPT__替换为括号内的content,否则将扩展为无。

只要将上面的宏替换改为如下代码即可:

#define PRINTF(format, ...) printf(format __VA_OPT__(,) __VA_ARGS__)

以下是一些帮助理解的示例代码:

#define F(...) f(0 __VA_OPT__(,) __VA_ARGS__)
F(a, b, c)   // 替换为 f(0, a, b, c)
F()        // 替换为 f(0)
 
#define G(X, ...) f(0, X __VA_OPT__(,) __VA_ARGS__)
G(a, b, c)   // 替换为 f(0, a, b, c)
G(a, )      // 替换为 f(0, a)
G(a)       //替换为 f(0, a)
 
#define SDEF(sname, ...) S sname __VA_OPT__(= { __VA_ARGS__ })
SDEF(foo);       // replaced by S foo;
SDEF(bar, 1, 2);   // replaced by S bar = { 1, 2 };

(3)不指定参数的宏替换

与部分指定参数的宏替换用法相似,只是完全不用指定参数。

#include <stdio.h>
#define PRINTF(...) printf(__VA_ARGS__)
int main() {
    PRINTF("Hello, world!\n");
    PRINTF("Number: %d\n", 42);
    PRINTF("Strings: %s and %s\n", "Hello", "world");
    return 0;
}

需要注意的是,如果类函数宏的参数含有逗号,逗号只会被识别为参数的分隔符(separator),而不会被识别为逗号操作符。比如下面的宏:

macro(array[x = y, x + 1])
atomic_store (p, (struct S){ a, b });

它们会由于参数数量不匹配导致编译失败。

但是在字符串中的逗号会被视为普通的逗号,不会被识别为参数分隔符。

三、宏的命名约定

因为函数和宏的调用形式一样,无法区分。因此一个好的命名习惯是:把宏名全部大写,函数名不要全部大写。

四、宏替换的高级应用

1.操作符#:将参数转换为字符串。

操作符#被称为“字符串化(stringification)”操作符。它用于宏定义中的替换列表中,会把其后的宏参数转换为一个用双引号(quotes)括起来的字符串。什么是字符串,就是诸如“你大爷还是你大爷”这样的一串字符,也就是一个实打实的字符串,英文为string literal,很多资料上把这个叫“字符串字面量”,这个翻译真是屌爆了!佛法指数直达五颗星,把每个初次相见的有缘人都度去了度娘。

比如下面的宏:

#define PRINT(n) printf("the value of "#n" is %d",n)

当执行下面的代码:

int a=10;
PRINT(a);

#a就会把a替换为字符串“a”时一个字符串代码就会预处理为:

printf("the value of 'a'is %d", a);

运行代码就能在屏幕上打印:

the value of a is 10

当#后接__VA_ARGS__时,展开的整个__VA_ARGS__都将被括在引号中:

#define showlist(...) puts(#__VA_ARGS__)
showlist();            // 展开为puts("")
//参数中如果有",会被展开为\"
showlist(1, "x", int); // 展开为puts("1, \"x\", int")

2.操作符##:将参数转换为字符串。

操作符#被称为“连接(concatenation)”操作符。它用于宏定义中的替换列表中,像加号一样将前后两个标识符连接起来,形成一个新的标识符。这在需要根据参数动态生成不同名称时非常有用。

比如,写一个函数求2个数的最大值的时候,不同的数据类型就得写不同的函数

比如这样两个函数:

int int_max(int x, int y)
{
	return x > y ? x : y;
}
float float_max(float x, float y)
{
	return x > y ? x : y;
}

但是这样写起来太繁琐了,可以用带有##的宏动态生成函数名:

//宏定义
#define GENERIC_MAX(type) \
type type##_max(type x,type y)\
{\
return(x > y ? x : y);\
}

这样就可以实现用一个宏定义两个不同的函数,减少的代码。完整代码如下:

#include<stdio.h>
//宏定义
#define GENERIC_MAX(type) \
type type##_max(type x,type y)\
{\
return(x > y ? x : y);\
}
int main() {
    //函数的定义
    GENERIC_MAX(int);
    GENERIC_MAX(float);
    //函数的调用
    printf("%d\n", int_max(3, 9));
    printf("%f\n", float_max(3.14, 5.52));
    return 0;
}

五、宏与函数的对比

1.宏的优势

(1)高性能。宏替换就是简单的文本替换,它是在编译阶段完成的(相当于Word里的查找替换),因此它在执行时不需要函数调用的开销:传递参数和返回值等,所以宏比函数在程序的内存开销和速度方面更胜一筹。

(2)不限类型。函数的参数必须为某一具体的类型,宏不用规定类型。

(3)灵活的参数。宏的参数可以是类型名,函数不可以。例如:

#include<stdio.h>
#define VAR(type, var, num) \
type var = num;
int main()
{
    VAR(int, a, 5);
    printf("%d\n", a);
    VAR(double, b, 3.14);
    printf("%f\n", b);
	return 0;
}

(4)切换代码。宏可根据编译条件切换程序中的代码块,函数无法实现。比如assert宏,在调试模式时替换为一种代码,在发布模式替换为另一种代码。assert的用法参见“捕捉错误的assert

2.宏的劣势

(1)不支持类型检查。有些事从一个角度看是优势,从另一个角度看就是劣势。比如人们说骑自行车环保又健康,但从另一个角度讲,你会被并驾齐驱的汽车甩开一个时代,也可能被迎面而来的它按到地上摩擦到另一个空间。灵活同时意味着不够严谨。宏替换不限制类型,也无法检查类型,因此在使用时要特别注意类型一致性,否则可能会导致编译错误或运行时错误。

(2)没法调试。由于宏替换是在编译阶段执行的,所以无法在运行时调试。这增加了调试的难度,尤其是当宏替换涉及复杂的逻辑时。

(3)可读性和可维护性偏低。过度使用宏替换可能会导致代码的可读性和可维护性降低。因为宏替换会隐藏一些复杂的逻辑和计算过程,使得代码变得难以理解和修改。这一点可以从那些库函数的头文件中获得良好的体验。

(4)宏可能会产生一些意想不到的问题,见后文陈述。

六、宏替换的问题

需要注意的是,由于宏只是简单的文本替换,因此在使用时需要特别小心以避免一些常见的陷阱,如运算符优先级问题、多次求值等。

1.运算符优先级问题

当宏参数是复杂的表达式时,就可能出现运算符优先级问题。例如:

#include <stdio.h>
#define X2(x) (printf("%d\n", x * 2))
int main() {
    int a = 2;
    //将展开为 (a + 1 * 2),而不是 (a + 1) * 2
    //输出4 而不是 6
    X2(a + 1);
    return 0;
}

这样的问题可以通过将宏参数包在括号中解决,只要将上面的宏替换改为:

#define X2(x) (printf("%d\n", (x) * 2))

2.多次修改变量值问题

因为自增运算符(++)、自减运算符(—)具有不通过赋值就能改变变量值的神奇效果,如果宏替换中的参数包含这两个运算符,就可能产生多次修改变量值的问题。

比如输出两个数中的最大值的宏MAX:

#include<stdio.h>
#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
	int x = 3, y = 7;
	int z = MAX(x++, y++);
	printf("%d %d %d\n", x, y, z);
	return 0;
}

MAX(x++, y++)展开为:((x++)>( y++)?( x++):( y++)),它显然存在重复修改变量的值的问题。

计算过程是这样的

①条件判断(x++)>( y++),结果:x++表达式的值为3,y++表达式的值为7,x=4,y=8。

②因为3>7不成立,返回y++的值。结果:y++的值为8,y=9,z=8。

所以程序输出:4 9 8。

七、#undef:移除一个宏定义

使用时直接在#undef加上宏名即楞。

#undef NAME

如果一个宏需要被重新定义,那么需要先用#undef将其移除。

  • 42
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

金创想

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值