C++的 Build 可分为4个步骤:预处理、编译、汇编、链接。
Preprocessor(预处理)包含 4 个阶段:
预处理之后的效果是:条件编译的测试不通过部分被删去、宏被替换、头文件被插入等。
预处理是以 translation unit 为单位进行的:一个 translation unit 就是一个源文件连同由#include包含(或间接包含)的所有文本文件的全体。一般编译器对一个 translation unit 生成一个二进制文件(VS是.obj,GCC是.o)。
Preprocessor指令一般格式如下:
# preprocessing_instruction [arguments] newline
指令如下:(除了以上所列的Preprocessor指令外,其他指令是不被C++标准支持的,尽管有些编译器实现了自己的预处理指令。很据“可移植性比效率更重要”的原则,应该尽量仅适用C++标准的Preprocessor)
条件编译由 #if, #ifdef, #ifndef 开始,后跟 0-n 个 #elif ,后跟 0-1 个 #else ,后跟 #endif 。
#include
#define ABCD 2
int main() {
#ifdef ABCD
std::cout << "1: yes\n";
#else
std::cout<<"2:no\n";
#endif
#ifndef ABCD
std::cout << "2: no1\n";
#elif ABCD == 2
std::cout << "2: yes\n";
#else
std::cout << "2: no2\n";
#endif
#if !defined(DCBA) && (ABCD < 2 * 4 - 3) // todo: 不知道为什么ABCD < 2 * 4 - 3成立
std::cout << "3: yes\n";
# endif
std::cin.get();
return 0;
}
// code result:
1: yes
2: yes
3: yes
条件编译被大量用于依赖于系统又需要跨平台的代码,这些代码一般会通过检测某些宏定义来识别操作系统、处理器架构、编译器,进而条件编译不同代码,以和系统兼容。
PS:但话又说回来,C++标准的最大价值就是让所有版本的C++实现都一致,所以除非调用系统功能,否则不应该对系统做出任何假设。
源文件包含指示将某个文件的内容插入到该#include处,这里“某个文件”将被递归预处理(1-4步,见第1节)。文件包含的3种格式为:
#include
,在标准包含目录查找filename(一般C++标准库头文件在此)#include"filename"
,先查找被处理源文件所在目录,如果没找到再找标准包含目录#include pp-tokens
其,其中pp-tokens须是定义为或"filename"的宏,否则结果未知。注意filename可以是任何文本文件,而不必是.h、.hpp等后缀文件,例如可以是.c或.cpp文本文件(所以标题是“源文件包含”而非“头文件包含”)。#ifndef B_CPP
#define B_CPP
int b = 999;
#endif // B_CPP
// file: a.cpp
#include // 在标准库目录找
#include"b.cpp"// 先在源文件所在目录找, 再再标准库找
#define CMATH <cmath> // 如下两行效果即为 #include, 这是一个标准库
#include CMATH // 同上
int main() {
std::cout << b << '\n'; // 是在b.cpp定义的
std::cout << std::log10(10.0) << '\n';
std::cin.get();
return 0;
}
// code result: 注意将a.cpp和b.cpp放在同一文件夹,只编译a.cpp(命令为g++ a.cpp && ./a.out)
999
1
#define 定义宏替换,#define 之后的宏都将被替换为宏的定义,直到用 #undef 解除该宏的定义。
宏定义分为不带参数的常量宏(Object-like macros)和带参数的函数宏(Function-like macros)。其格式如下:
#define identifier replacement-list
#define identifier( parameters ) replacement-list
#define identifier( parameters, ... ) replacement-list
#define identifier( ... ) replacement-list
#undef identifier
对于有参数的函数宏,在replacement-list中,“#”置于identifier面前表示将identifier变成字符串字面值,“##”连接,下面的例子来自cppreference.com:
#include
// make function factory
#define FUNCTION(name, a) int fun_##name() {return a;} // “#”置于identifier面前表示将identifier变成字符串字面值,“##”连接
FUNCTION(abcd, 12); // 定义func_abc()函数其无参数, 返回 12
FUNCTION(fff, 2);// 定义func_fff()函数其无参数, 返回 2
FUNCTION(kkk, 23);// 定义func_kkk()函数其无参数, 返回 23
#undef FUNCTION
#define FUNCTION 34 // 之前已定义过的 fun_abcd()、fun_fff()、fun_kkk() 已定义好, 现在可以重新宏定义了
#define OUTPUT(a) std::cout << #a << '\n'
int main() {
std::cout << "abcd: " << fun_abcd() << std::endl; // use function factory
std::cout << "fff: " << fun_fff() << std::endl;
std::cout << "kkk: " << fun_kkk() << std::endl;
std::cout << FUNCTION << std::endl; // 新的宏定义是34
OUTPUT(million);
std::cin.get();
return 0;
}
// code result:
abcd: 12
fff: 2
kkk: 23
34
million
可变参数宏是C++11新增部分(来自C99),使用时用__VA_ARGS__指代参数“…”,一个摘自C++标准2011的例子如下(标准举的例子就是不一样啊):
#include
#define debug(...) fprintf(stderr, __VA_ARGS__) // __VA_ARGS__指代参数“...”
#define showlist(...) puts(#__VA_ARGS__)
#define report(test, ...) ((test) ? puts(#test) : printf(__VA_ARGS__))
int main() {
int x = 1;
int y = 2;
debug("Flag");
debug("X = %d\n", x);
showlist(The first, second, and third items.);
report(x>y, "x is %d but y is %d", x, y);
}
// 这段代码在预处理后产生如下代码:
fprintf(stderr, "Flag");
fprintf(stderr, "X = %d\n", x);
puts("The first, second, and third items.");
((x>y) ? puts("x>y") : printf("x is %d but y is %d", x, y));
// code result:
FlagX = 1
The first, second, and third items.
x is 1 but y is 2
// 其中上面5个宏一定会被定义,下面从__STDC__开始的宏不一定被定义,这些预定义宏不能被 #undef
// 这些宏经常用于输出调试信息。预定义宏一般以“__”作为前缀,所以用户自定义宏应该避开“__”开头
// 现代的C++程序设计原则不推荐适用宏定义常量或函数宏,应该尽量少的使用 #define ,如果可能,用 const 变量或 inline 函数代替
__cplusplus: 在C++98中定义为199711L,C++11中定义为201103L
__LINE__: 指示所在的源代码行数(从1开始),十进制常数
__FILE__: 指示源文件名,字符串字面值
__DATE__: 处理时的日期,字符串字面值,格式“Mmm dd yyyy”
__TIME__: 处理时的时刻,字符串字面值,格式“hh:mm:ss”
__STDC__: 指示是否符合Standard C,可能不被定义
__STDC_HOSTED__: 若是Hosted Implementation,定义为1,否则为0
__STDC_MB_MIGHT_NEQ_WC__: 见ISO/IEC 14882:2011
__STDC_VERSION__: 见ISO/IEC 14882:2011
__STDC_ISO_10646__: 见ISO/IEC 14882:2011
__STDCPP_STRICT_POINTER_SAFETY__: 见ISO/IEC 14882:2011
__STDCPP_THREADS__: 见ISO/IEC 14882:2011
// 示例如下:
#include
int main() {
#define PRINT(arg) std::cout << #arg": " << arg << '\n'
PRINT(__cplusplus);
PRINT(__LINE__);
PRINT(__FILE__);
PRINT(__DATE__);
PRINT(__TIME__);
#ifdef __STDC__
PRINT(__STDC__);
#endif
std::cin.get();
return 0;
}
// code result:
__cplusplus: 201703
__LINE__: 6
__FILE__: /cppcodes/run/a.cpp
__DATE__: Aug 26 2023
__TIME__: 21:11:36
__STDC__: 1
从 #line number ["filename"]
的下一行源代码开始, __LINE__
被重定义为从 number 开始,__FILE__
被重定义 "filename"
(可选),一个例子如下:
#include
int main() {
#define PRINT(arg) std::cout << #arg": " << arg << '\n'
#line 999 "WO"
PRINT(__LINE__);
PRINT(__FILE__);
std::cin.get();
return 0;
}
// code result:
__LINE__: 999
__FILE__: WO
#error [message]
指示编译器报告错误,一般用于系统相关代码,例如检测操作系统类型,用条件编译里 #error
报告错误。例子如下:
int main(){
#error "w"
return 0;
#error
}
// code result:
/cppcodes/run/a.cpp:2:2: error: "w"
#error "w"
^
/cppcodes/run/a.cpp:4:2: error:
#error
^
2 errors generated.
#pragma
预处理指令是C++标准给特定C++实现预留的标准,所以在不同的编译器上 #pragma 的参数及意义可能不同,如:
#pragma once
来指示源文件只被处理一遍,参见MSDN相关条目#pragma omp
指导语句,详见:OpenMP共享内存并行编程详解。#pragma
指令参见GCC文档相关条目。预处理的常见使用有:
关于用宏检测的定义如下链接,代码示例如下:
#ifdef _WIN64
//define something for Windows (64-bit)
#elif _WIN32
//define something for Windows (32-bit)
#elif __APPLE__
#include "TargetConditionals.h"
#if TARGET_OS_IPHONE && TARGET_IPHONE_SIMULATOR
// define something for simulator
#elif TARGET_OS_IPHONE
// define something for iphone
#else
#define TARGET_OS_OSX 1
// define something for OSX
#endif
#elif __linux
// linux
#elif __unix // all unices not caught above
// Unix
#elif __posix
// POSIX
#endif