有关宏的内容

最近看到一些跟宏相关的代码,发现这玩意儿真是个神奇的东西...😂

嗯,本文的目的不是要介绍一些宏的神奇用法(这些日后再说),只是要简单记录一下跟宏相关的一些指令和操作。因为突然发现曾经学的语言书籍里,对宏的介绍都很少,就介绍个条件编译,几页纸就没了,结果一到正儿八经的工程项目中,宏的骚操作到处都有...

OK,闲话少说吧。

条件指令

首先基本的内容与书上的内容一致,也是条件编译指令。
本来想弄个表格一次性说明,结果发现如果想要解释的清楚一点,必须得写一点示例代码,那就一个一个来吧。

#if

#if用来检查跟在其后的常量表达式是否为真,常见用法比如注释测试代码:

1
2
3
4
5
6
7
8
9
10
#include <iostream>

int main() {

#if 0
std::cout << "test code" << std::endl;
#endif

return 0;
}

#ifdef

#ifdef用来检查宏是否已定义,比如我们可以用它来判断系统类型:

1
2
3
4
5
6
7
8
9
10
#include <iostream>

int main() {

#ifdef _WIN32
std::cout << "windows os" << std::endl;
#endif

return 0;
}

#ifndef

#ifndef用来检查宏是否未定义,这与#ifdef刚好相反,也可以观察到#ifndef中间多了一个n,应该就是单词not的缩写吧。这个指令的用法就比较常见了,通常为了防止头文件重复包含,都会加上:

1
2
3
4
#ifndef __HEADER_H__
#define __HEADER_H__

#endif

#elif/#else

#elif#else两个指令可以与#if#ifdef配套使用,有点类似普通的if语句。比如:

1
2
3
4
5
6
7
#ifdef __linux__
std::cout << "linux os" << std::endl;
#elif _WIN32
std::cout << "windows os" << std::endl;
#else
std::cout << "other os";
#endif

#endif

#endif就没啥说的了,条件编译指令的结束指令。

操作指令

第二块要介绍的是操作指令。

#define

#define用来定义宏,比如:

1
2
#define PI 3.141592654
#define Add_INT(a, b) ((a) + (b))

#undef

#undef用来取消宏定义,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

int main() {

#undef _WIN32
#ifdef _WIN32
std::cout << "windows os" << std::endl;
#else
std::cout << "other os";
#endif

return 0;
}
/*
output:
other os
*/

在上述代码中,我们取消了_WIN32这个宏定义,结果程序最后输出了other os

#defined()

#defined()用于在条件编译指令中检查宏,比如我们想检查特定的宏是否被定义:

1
2
3
4
5
6
7
8
9
10
#include <iostream>

int main() {

#if defined(MY_MACRO)
std::cout << "MY_MACRO" << std::endl;
#endif

return 0;
}

注意这个宏无法单独使用,只能与条件编译指令一起使用。有了它之后,可以把条件编译写的更强大,也更...复杂...

#

#可以把传入的宏参数,转化为字符串字面量,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

#define TOSTRING(x) #x

int main() {

std::cout << TOSTRING(123) << std::endl;
std::cout << TOSTRING(abc) << std::endl;
std::cout << TOSTRING(1a2b3c) << std::endl;

return 0;
}
/*
123
abc
1a2b3c
*/

我们也很容易发现,这个宏操作的原理其实就是,在预处理阶段,进行文本替换的同时给宏参数加上了一堆双引号,也即:

1
2
3
TOSTRING(123) -> "123"
TOSTRING(abc) -> "abc"
TOSTRING(1a2b3c) -> "1a2b3c"

同时,#指令后面可以不跟任何内容,只作为占位符使用。

##

##用来连接宏参数,比如:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#define CONCAT(a,b) a##b

int main() {

// CONCAT(1, 2) -> 12
int a = CONCAT(1, 2);

std::cout << a << std::endl;

return 0;
}

其实就是删除了宏参数之间的逗号。

特殊指令

没啥好说的,这部分就是一些特殊的条件指令。

#include

最先出场的就是#include,这个没啥好说的了,大家写的第一行代码。

#error/#warning

#error用来在预处理阶段强制触发编译错误并输出自定义错误信息,比如:

1
2
3
4
5
6
7
8
9
10
#include <iostream>

#ifndef _WIN32
#error "this code need windows os"
#endif

int main() {

return 0;
}

#warning用法与#error类似,但是触发的是警告,需要注意的是#warning不属于 C/C++ 标准,但是 GCC、MSVC 和 clang 都支持。

#line

#line用于修改编译器内部记录的行号和文件名(主要用于诊断定位),比如:

1
2
#line 2 // 下一行从 2 开始计数
#line 40 "test2.c" // 下一行从 40 开始计数,若出错,则报错文件名为 test2.c

#pragma

#pragma用来向编译器传递非标准指令,比如:

1
2
#pragma once // 确保头文件只包含一次
#pragma pack(1) // 设置结构体对齐为 1 字节

另外,关于这个宏还需提一下,在 C99 和 C++11 中引入了_Pragma操作符来完成这部分功能。对应的用法:

1
2
_Pragma("once")
_Pragma("pack(1)")

也就是说,#pragma_Pragma用法是相同的,但是_Pragma有个很显然的优点就是,它可以与其他宏指令一起使用,但#pragma只能单独成行(有点莫名的孤独感出现了~🤣),比如:

1
2
3
4
5
6
// 使用 #pragma(无法在宏中直接使用)
#pragma GCC diagnostic ignored "-Wunused-variable"

// 使用 _Pragma(可在其他宏中使用)
#define SUPPRESS_WARNING(w) _Pragma(#w)
SUPPRESS_WARNING(GCC diagnostic ignored "-Wunused-variable")

标准预定义宏

最后,再整理一下常用的预定义宏,留作日常使用。
PS:严重怀疑这玩意儿最初一定是编译器开发人员为了给自己省事开的后门😂。

标准预定义宏

宏名称 功能描述 示例值
__FILE__ 当前源文件的完整路径(字符串) "src/main.cpp"
__LINE__ 当前代码行的行号(整数) 42
__DATE__ 编译日期(格式 “Mmm dd yyyy”) "Dec 14 2023"
__TIME__ 编译时间(格式 “hh:mm:ss”) "15:30:45"
__func__ (C99/C++11) 当前函数名(字符串) "main"
__cplusplus C++ 语言版本标识(仅在 C++ 中定义) 199711L (C++98)
201703L (C++17)
__STDC__ 标准 C 编译器(值为 1) 1
__STDC_VERSION__ C 标准版本(C 编译器) 199901L (C99)

Buy me a coffee ? :)
0%