在 C/C++ 中,宏定义是一种用于在编译时替换文本的机制。在某些情况下,你可能会想要动态地改变宏的值,而这可以通过使用宏参数来实现。当然,这些只是宏定义的基本操作。下面我们来看看关于宏的进阶操作。

一、字符串化操作(#)

在 C/C++ 中,字符串化操作(#)可以将一个宏参数转换为字符串字面量(并非简单的添加双引号,详情可见下面的例子)。

字符串化示例
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;

#define TO_STRING(VAL) #VAL

int main() {
string sea_epoch = TO_STRING("SeaEpoch"www.seayj.cn'');

cout << sea_epoch << endl;

return 0;
}

程序输出结果为:

1
"SeaEpoch"www.seayj.cn''

二、拼接操作(##)

在 C/C++ 中,拼接操作(##)可以将两个宏参数连接在一起。因为宏替换是在编译阶段运行的,所以利用该操作可以实现很多“奇怪”的代码🤓。详情请参考下面的程序示例,因为比较简单就不做详细解释了。

拼接示例
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;

#define CONCATENATE_VARIABLE_NAMES(prefix, suffix) prefix##suffix

int main() {
string presuf = "SeaEpoch";

cout << CONCATENATE_VARIABLE_NAMES(pre, suf) << endl;

return 0;
}

程序输出结果为:

1
SeaEpoch

三、预定义宏/预处理器宏

在 C/C++ 中,预定义宏是一种特殊类型的宏,它们在编译时已经定义,并且无法被用户定义。它们是编译器内置的,用于提供有关编译器的信息。

3.1 标准预定义标识符

根据 ISO C99 和 ISO C++11 规范,C/C++ 语言中预定义了以下标识符:

  • __func__:封闭函数(用作 char 的函数局部 static const 数组)的未限定、未修饰名称。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>
    using namespace std;

    static void SeaEpoch(const string& url) {
    cout << "当前执行函数:" << __func__ << endl;
    }

    int main() {
    SeaEpoch("www.seayj.cn");

    return 0;
    }
    程序输出结果为:
    1
    当前执行函数:SeaEpoch

3.2 标准预定义宏

根据 ISO C99、C11、C17 和 ISO C++17 标准,编译器提供了以下预定义宏:

  • __cplusplus:当翻译单元编译为 C++ 时,定义为整数文本值,表示 C++ 标准的版本号。其他情况下则不定义。
  • __DATE__:当前源文件的编译日期。日期是 Mmm dd yyyy 格式的恒定长度字符串文本。月份名 Mmm 与 C 运行时库 (CRT) asctime 函数生成的缩写月份名相同。如果值小于 10,则日期 dd 的第一个字符为空格。任何情况下都会定义此宏。
  • __FILE__:当前源文件的名称。__FILE__ 展开为字符型字符串文本。要确保显示文件的完整路径,请使用(诊断中源代码文件的完整路径)/FC。任何情况下都会定义此宏。
  • __LINE__:定义为当前源文件中的整数行号。可使用 #line 指令来更改此宏的值(例如 #line 1 可将当前行号设置为 1,后续行号根据该行行号进行累加)。__LINE__ 值的整型类型因上下文而异。任何情况下都会定义此宏。
  • __STDC__:仅在编译为 C,并且指定了 /Za 编译器选项时,定义为 1。从 Visual Studio 2022 17.2 版本开始,当编译为 C 并指定 /std:c11/std:c17 编译器选项时,它定义为 1。其他情况下则不定义。
  • __STDC_HOSTED__:如果实现是托管实现并且支持整个必需的标准库,则定义为 1。其他情况下则定义为 0。
  • __STDC_NO_ATOMICS__ 如果实现不支持可选的标准原子,则定义为 1。当编译为 C 且指定 /std C11 或 C17 选项之一时,MSVC 实现会将其定义为 1
  • __STDC_NO_COMPLEX__ 如果实现不支持可选的标准复数,则定义为 1。当编译为 C 且指定 /std C11 或 C17 选项之一时,MSVC 实现会将其定义为 1
  • __STDC_NO_THREADS__ 如果实现不支持可选的标准线程,则定义为 1。当编译为 C 且指定 /std C11 或 C17 选项之一时,MSVC 实现会将其定义为 1
  • __STDC_NO_VLA__ 如果实现不支持可选的可变长度数组,则定义为 1。当编译为 C 且指定 /std C11 或 C17 选项之一时,MSVC 实现会将其定义为 1
  • __STDC_VERSION__ 当编译为 C 且指定 /std C11 或 C17 选项之一时定义。对于 /std:c11,它扩展到 201112L;对于 /std:c17,则扩展到 201710L
  • __STDCPP_DEFAULT_NEW_ALIGNMENT__ 当指定 /std:c17 或更高版本时,此宏会扩展为 size_t 字面量,该字面量的对齐值由对非对齐感知的 operator new 的调用所保证。较大的对齐值传递到对齐感知重载,例如 operator new(std::size_t, std::align_val_t)。有关详细信息,请参阅 /Zc:alignedNew(C++17 过度对齐的分配)
  • __STDCPP_THREADS__当且仅当程序可以有多个执行线程并编译为 C++ 时,定义为 1。其他情况下则不定义。
  • __TIME__:预处理翻译单元的翻译时间,时间是 hh:mm:ss 格式的字符型字符串文本,与 (CRT) asctime 函数返回的时间相同,任何情况下都会定义此宏。

下面可以看一下这个示例,可能是标准预定义宏的一种使用场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;

#define DEBUG_MSG(msg) \
cout \
<< "[" \
<< __FILE__ \
<< ":" \
<< __LINE__ \
<< "] " \
<< __func__ \
<< ":" \
<< #msg \
<< endl;

int main() {
DEBUG_MSG(这是一段测试文字,测试一下 DEBUG 的信息输出格式~);

return 0;
}

程序输出结果为:

1
[C:\Users\SeaYJ\Documents\Visual Studio 2022\Repositories\CppTest\main.cpp:17] main:这是一段测试文字,测试一下 DEBUG 的 信息输出格式~

3.3 MSVC 特有的预定义宏

对于 MSVC 编译器,微软还设置了很多其他有用的预定义宏。这里不做更多详细的介绍了,详情可见 Microsoft 专用预定义宏

参考资料

🔖 预定义宏
🔖 一脸懵逼!原来 C 语言宏定义还能这么玩?
🔖 C语言中如何使用宏连接多个字符串(#和##的用法)