对于 C 语言的宏定义,相信绝大多数人都用过,对于其基本定义,相信大家也都了解,无非就是一种预处理指令,用于在编译之前将代码中的标识符替换为指定文本。
比如下面这行代码:
#define test "This is a test string"
经过这个宏定义后,下面所有 "test" 出现的地方就都被替换成了 "This is a test string"。很简单,所以显然这种宏定义并不是今天要讲的。
我们先稍微提高一点难度,看一下如下代码:
#include <stdio.h>
int main(void) {
printf("%s\n", __FILE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
printf("%d\n", __LINE__);
return 0;
}
乍一看好像没什么问题,但是如果我告诉你,这就是一个完整的代码,并且可以编译运行,可能有一部分小伙伴就有疑问了。
仔细看代码,可以发现这里面存在四个参数:__FILE__、__DATE__、__TIME__、__LINE__ 。这些参数在开头并没有被定义,理论上不是应该编译报错提示变量未定义吗?为什么能编译成功,并且运行,实际运行输出的又是什么呢?
既然有疑问,我们就来实践运行一下:
jay@jaylinuxlenovo:~/test$ ./test
test.c
Nov 23 2024
11:40:13
7
通过输出结果以及变量名,相信大家已经有一点概念了。实际上,这些是编译器内置的宏定义,由于编译器内部已经定义好,因此我们可以直接使用,其含义如下:
-
__FILE__: 当前源文件(编译器执行编译时的路径)的文件名字符串。
-
__DATE__: 当前源文件编译的日期字符串
-
__TIME__: 当前源文件编译的时间字符串。
-
__LINE__: 当前宏所在的行号。
这些内置的宏定义可以帮助我们在代码中获取一些与编译环境相关的信息,特别是在配合输出调试信息的时候往往起着至关重要的作用。
示例代码:
#include <stdio.h>
#define DEBUG_LOG(message) printf("[%s:%d] %s: %s\n", __FILE__, __LINE__, __FUNCTION__, message)
void foo() {
DEBUG_LOG("Inside foo()");
}
int main() {
DEBUG_LOG("Inside main()");
foo();
return 0;
}
运行结果:
jay@jaylinuxlenovo:~/test$ ./test
[test.c:8] main: Inside main()
[test.c:4] foo: Inside foo()
可以看到,调试信息的开头输出了其所在的文件名和代码行号,并且还给出了当前调试信息所属函数的函数名,这一点大大提高了我们在编码过程中定位问题的效率。
由此可见,编译器内置的宏定义非常有用。无论是编码还是调试,都能带给我们效率的提升。当然,内置的宏定义也不仅仅上述这些,不过其他的使用频次并没有那么高,而不同的编译器也会有不同的内置宏,因此如果要详细了解你的项目具备哪些内置宏,可以针对于你的编译器查询一下,说不定有很多你不知道但对你的项目很有帮助的宏哦。
接下来你可以休息一会,喝口水,消化一下上文的知识点。因为下面要进入我们的重点,同时其理解难度也会上升,请做好准备。
首先我们看一段代码:

??? 让我们聚焦于一个点:
![]()
?????这是个什么玩意儿???
别急,以上就是我们今天的重点:字符串化操作符和连接操作符。实际上这两者都不是很难,只要理解其含义,再看上面这一行代码就很简单了。
# 字符串化操作符
所谓字符串化操作符,实际上就是一种将宏参数转换为字符串常量的特殊运算符。
下面我们来看一个示例:
#include <stdio.h>
#define LOG_MESSAGE(msg) printf("Message: %s\n", #msg)
int main() {
LOG_MESSAGE(HelloWorld);
return 0;
}
运行结果:
jay@jaylinuxlenovo:~/test$ ./test
Message: HelloWorld
上述代码中,我们定义了一个 LOG_MESSAGE 的宏,它接受一个参数 msg。同时我们在宏定义中使用 # 运算符将参数 msg 转换为了字符串并交给 printf 打印。因此,LOG_MESSAGE(HelloWorld); 这条语句实际上就相当于 printf("Message: %s\n", "Hello, World!");
自然运行完后你会在命令行看到 Message: HelloWorld 的输出信息。
## 连接操作符
连接操作符 ## 用于将两个标识符连接成一个单独的标识符。这种操作可以在宏展开时动态生成标识符,给编程提供很大的灵活性。
下面提供一个示例:
#include <stdio.h>
#define CONCAT(a, b) a ## b
int main() {
int x12 = 100;
printf("%d\n", CONCAT(x, 12)); // 输出:100
return 0;
}
运行结果:
jay@jaylinuxlenovo:~/test$ ./test
100
可以看到,上述代码定义了一个宏:CONCAT,它接受两个参数 a 和 b。在宏定义中,我们使用了 ## 操作符将 a 和 b 连接在了一起,形成了一个新的标识符,所以在我们调用了 CONCAT(x,12) 时,宏展开后的代码就相当于是 x12,因此打印结果就是 100 。
让我们再来看几个例子加深一下印象:
1. 定义一系列相似的变量或函数
#define DEFINE_STRUCT(name, type) struct name ## _struct { type value; };
DEFINE_STRUCT(Point, int) // 定义 struct Point_struct { int value; };
DEFINE_STRUCT(Vector, double) // 定义 struct Vector_struct { double value; };
2. 生成枚举常量
#define ENUM_CONSTANTS(name, start, end) \
enum name { \
name ## _ ## start, \
name ## _ ## end \
};
ENUM_CONSTANTS(Weekday, Monday, Friday) // 定义 enum Weekday { Weekday_Monday, Weekday_Friday };
ENUM_CONSTANTS(Month, January, December) // 定义 enum Month { Month_January, Month_December };
通过使用连接操作符 ##,我们可以在宏定义中动态生成标识符,从而实现更灵活和通用的宏定义。这在定义一系列相似的变量或函数时非常有用,并且可以减少冗余代码。但是需要注意的是,连接符 ## 只能用于预处理阶段,不能在运行时使用。
现在我们再来看一开始给出的这个例子:
![]()
对于其含义你是否就一目了然了呢?
实际上这句话就是动态定义了一个字符数组,如果我们的 fn 为 test,那么这句话实际上就变成了:
const char __rti_test_name[] = "test";
是不是很简单!
到这里,你对 C 语言宏定义的理解是不是又加深了不少?想一下你的日常编码过程中是否能够用到上述这些小技巧,如果可以的话就赶紧用起来,使你的代码格调更高,效率更高!


658

被折叠的 条评论
为什么被折叠?



