一文搞定“宏”

一、宏和宏替换

宏(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
    评论
【优质项目推荐】 1、项目代码均经过严格本地测试,运行OK,确保功能稳后才上传平台。可放心下载并立即投入使用,若遇到任何使用问题,随时欢迎私信反馈与沟通,博主会第一时间回复。 2、项目适用于计算机相关专业(如计科、信息安全、数据科学、人工智能、通信、物联网、自动化、电子信息等)的在校学生、专业教师,或企业员工,小白入门等都适用。 3、该项目不仅具有很高的学习借鉴价值,对于初学者来说,也是入门进阶的绝佳选择;当然也可以直接用于 毕设、课设、期末大作业或项目初期立项演示等。 3、开放创新:如果您有一基础,且热爱探索钻研,可以在此代码基础上二次开发,进行修改、扩展,创造出属于自己的独特应用。 欢迎下载使用优质资源!欢迎借鉴使用,并欢迎学习交流,共同探索编程的无穷魅力! 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和档都非常完善,方便用户快速上手和理解代码;最后,我会期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

金创想

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

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

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

打赏作者

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

抵扣说明:

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

余额充值