文章目录
- 一、宏定义
- 1.1、宏概念
- 1.2、宏作用阶段
- 1.3、宏函数
- 1.4、宏函数与普通函数的区别
- 二、宏里面特殊字符的作用
- 2.1、#(stringizing)字符串化操作符
- 2.2、@#(charizing)字符化操作符
- 2.3、##(token-pasting)符号链接操作符
- 2.4、/行继续操作符
- 三、可变参数宏与__VA_ARGS__ 宏
- 3.1、__VA_ARGS__ 宏
- 3.2、可变参数名称
- 3.3、无参传入情况
- 四、条件宏
前言:
在C和C++编程语言中,宏是一种强大的预处理工具,它允许程序员在编译之前对源代码进行文本替换或条件编译。宏通过预处理器指令定义,并在预处理阶段被展开到源代码中。
一、宏定义
1.1、宏概念
宏定义简单点说就是查找替换(文本替换),C中的宏分为两类,对象宏和函数宏。宏定义的一般形式为:
#define 宏名 字符串
注意:
- 宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换
- 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束
1.2、宏作用阶段
编译一个C语言程序的第一步骤就是预处理阶段,这一阶段就是宏发挥作用的阶段。C预处理器在源代码编译之前对其进行一些文本性质的操作,主要任务包括删除注释、插入被#include进来的文件内容、定义和替换由#define 定义的符号以及确定代码部分内容是否根据条件编译(#if )来进行编译。”文本性质”的操作,就是指只是简单粗暴的进行文本替换,而不考虑其中任何的语义内容。
1.3、宏函数
配合#、##一起使用函数宏可以节省大量的工作量,例如:获取环境变量的一系列操作,可以通过宏函数生成实际的函数定义,如下:
#define ENV_MAX_LEN (256)
#define DEFINE_FUNC_ENV(ENV, default) \
const char *CFG_Get##ENV() \
{ \
do { \
static char ENV##Var[ENV_MAX_LEN] = {'\0'}; \
char *value = getenv(#ENV); \
if (IS_NULL(value)) { \
ASSERTEX_NO_RET(IS_NOT_NULL(value), LOG_LEVEL_WARN, "env (%s) not set, use value(%s)", \
#ENV, default); \
return default; \
} \
errno_t retCode = strncpy_s(ENV##Var, ENV_MAX_LEN, value, strlen(value)); \
ASSERTEX((retCode == EOK), "", LOG_LEVEL_ERR, "strncpy_s(%s) failed(%d).", value, retCode); \
return ENV##Var; \
} while (0); \
}
// 定义函数,从环境变量中获取服务基本配置
DEFINE_FUNC_ENV(APPID, NULL)
DEFINE_FUNC_ENV(APPNAME, "")
// 定义函数,从环境变量中获取服务的位置信息
DEFINE_FUNC_ENV(PODNAME, "defaultPod")
DEFINE_FUNC_ENV(NODE_ID, "defaultNode")
DEFINE_FUNC_ENV(NodeID, "defaultNode")
DEFINE_FUNC_ENV(ContainerID, "defaultContainerID")
DEFINE_FUNC_ENV(HOSTING_SERVER_IP, "127.0.0.1")
1.4、宏函数与普通函数的区别
宏函数与普通函数主要区别如下:
- 宏函数的本质是进行文本替换,并没有实际函数调用(压栈、退栈)带来的开销
- 宏函数不会检查参数的类型,普通的函数调用会检查参数类型
二、宏里面特殊字符的作用
2.1、#(stringizing)字符串化操作符
将宏定义中的传入的参数名转换成用一对双引号括起来的参数名字符串,例如:
#define ASSERTEX_NO_RET_EQ(a, b) \
ASSERTEX_NO_RET((a == b), LOG_LEVEL_ERR, "expect " #a "(%d) equals " #b "(%d) failed", a, b)
注意:
- 其只能用于有传入参数的宏定义中,且在宏定义体中使用#时,#必须位于宏参数前面
- 忽略传入参数名前后的空格
#define toStr(str) #str
char *str = toStr( 123 ); // 扩展成 char *str = "123"
2.2、@#(charizing)字符化操作符
将传的单字符参数名转换成字符,以一对单引用括起来,如下:
#define makechar(x) #@x
char ch = makechar(b); // 扩展成 char ch = 'b'
2.3、##(token-pasting)符号链接操作符
将多个宏参数名拼接成一个参数名,是一种分割链接方式,它的作用是先分隔,然后进行强制连接,如下:
#define exampleNum(n) num##n
int num9 = 9;
int num = exampleNum(9); // 扩展成 int num = num9;
#define TYPE(type, name) type name##_##type##_type
TYPE(int, a); // 扩展成 int a_int_type;
注意:当用##连接形参时,##前后的空格可有可无。如:#define exampleNum(n) num ## n 相当于 #define exampleNum(n) num##n
2.4、/行继续操作符
当定义的宏不能用一行表达完整时,可以用"/"表示下一行继续此宏的定义
// 判断返回值,并打印信息和返回错误码
#define ASSERTEX(expr, ret, level, fmt, va...) \
do { \
if (!(expr)) { \
STD_LOG(level, fmt, ##va); \
return ret; \
} \
} while (0)
三、可变参数宏与__VA_ARGS__ 宏
3.1、VA_ARGS 宏
在GNU C中,从C99开始,宏可以接受可变数目的参数,就象可变参数函数一样。和函数一样,宏也用三个点…来表示可变参数。VA_ARGS 宏用来表示可变参数的内容,简单来说就是将左边宏中 … 的内容原样抄写在右边__VA_ARGS__ 所在的位置。如下:
#include <stdio.h>
#define debug(...) printf(__VA_ARGS__)
int main(void)
{
int year = 2018;
debug("this year is %d\n", year); //效果同printf("this year is %d\n", year);
}
3.2、可变参数名称
通过一些语法,你可以给可变参数起一个名字,而不是使用__VA_ARGS__ ,如下例中的args:
#include <stdio.h>
#define debug(format, args...) printf(format, args)
int main(void)
{
int year = 2018;
debug("this year is %d\n", year); //效果同printf("this year is %d\n", year);
}
3.3、无参传入情况
可变参数宏中的可变参数必须至少有一个参数传入,不然会报错,为了解决这个问题,需要一个特殊的“##”操作,如果可变参数被忽略或为空,“##”操作将使预处理器(preprocessor)去除掉它前面的那个逗号。如下例所示:
#include <stdio.h>
#define debug(format, args...) printf(format, ##args)
int main(void)
{
int year = 2018;
debug("hello, world"); //只有format参数,没有args可变参数
}
四、条件宏
下面是常用的条件宏,如下:
宏名 | 功能 |
---|---|
#ifdef | 如果宏已经定义,则编译下面代码 |
#ifndef | 如果宏没有定义,则编译下面代码 |
#define | 定义宏 |
#endif | 结束一个#if……#else条件编译块 |
- 使用条件宏避免头文件多次包含
在C++编程中,头文件重复包含是一个常见的问题,可能会导致重复定义错误、编译时间增加等问题。为了防止头文件被重复包含,通常使用预处理宏(也称为条件宏)来实现。例如:
// MyHeader.h
#ifndef MYHEADER_H
#define MYHEADER_H
// 头文件的内容从这里开始
class MyClass {
public:
void doSomething();
};
// 头文件的内容在这里结束
#endif // MYHEADER_H
- 使用
#pragma once
预处理指令
在C++编程中,
#pragma once
是一个非标准的但广泛支持的预处理指令,用于防止头文件被多次包含(即“重复包含”)到同一个源文件或翻译单元中。当编译器遇到#pragma once
时,它会确保当前头文件在当前的编译过程中只被包含一次,无论它在源文件中被#include
了多少次。例如:
// MyHeader.h
#pragma once
class MyClass {
public:
void doSomething();
};