C++翻译流程与预处理指令

参考了C++语言构造手册,cppreference

翻译阶段复制自cppreference

翻译阶段

编译器处理 C++ 源文件时,如同严格按照以下顺序进行各个阶段的处理:

阶段 1

1) (以实现定义方式)将源文件的各个单独字节,映射为基本源字符集的字符。特别是,操作系统相关的行尾指示符均被替换为换行字符。基本源字符集由以下 96 个字符组成:

a) 5 个空白字符(空格 (space)、水平制表 (horizontal tab)、垂直制表 (vertical tab)、换页 (form feed)和 换行 (new-line))

b) 10 个数字字符,从 ‘0’ 到 ‘9’

c) 52 个字母,从 ‘a’ 到 ‘z’ 以及从 ‘A’ 到 ‘Z’

d) 29 个标点字符:_ { } [ ] # ( ) < > % : ; . ? * + - / ^ & | ~ ! = , \ “ ‘

2) 任何无法被映射到基本源字符集中的字符的源文件字符,均被替换为其通用字符名(用 \u\U 转义),或某种被等价处理的由实现定义的形式。

3) 将各个三标符序列替换为其对应的单字符表示。 (C++17 前)

阶段 2

1) 当反斜杠出现于行尾(其后紧跟换行符)时,删除该反斜杠和换行符并将两个物理源码行组合成一个逻辑源码行。这是单趟操作:如果有一行以两个反斜杠结束且后随一个空行,这三行不会合为一行。若于此阶段组成了通用字符名(\uXXXX),则行为未定义。

2) 若此步骤后,非空源文件不以换行符结束(无论是原本就无换行,还是以反斜杠结束),则其行为未定义 (C++11 前)在最后添加一个换行符 (C++11 起)。

阶段 3

1) 将源文件分解为注释,空白字符(空格、水平制表、换行、垂直制表和换页)的序列,和下列各种预处理记号

a) 头文件名,如 或 “myfile.h”

b) 标识符

c) 预处理数字

d) ,包含用户定义的 (C++11 起)字符字符串字面量

e) 运算符与标点(包括代用记号),如 +、<<=、<%、## 或 and

f) 不属于任何其他类别的单独非空白字符

2) 恢复在任何原始字符串字面量的首尾双引号之间在阶段 1 和 2 期间进行的所有变换。 (C++11 起)

3) 以一个空格字符替换每段注释。

保留换行符。未指明是否可将非换行空白字符序列缩减成单个空格字符。

若一个给定字符前的输入已被解析为预处理记号,下一个预处理记号通常会由能构成预处理记号的最长字符序列够成,即使这样处理会导致后续分析失败。这常被称为最大吞噬

1
2
3
4
5
int foo = 1;
int bar = 0xE+foo; // 错误:非法的预处理数字 0xE+foo
int baz = 0xE + foo; // OK

int quux = bar+++++baz; // 错误:bar++ ++ +baz,而非 bar++ + ++baz。

最大吞噬规则仅有的例外是:

若以下一个字符开头的字符序列可作为原始字符串字面量的前缀和起始双引号,则下个预处理记号应当为原始字符串字面量。该字面量由匹配原始字符串模式的最短字符序列组成。#define R "x" const char* s = R"y"; // 非良构的原始字符串字面量,而非 "x" "y" const char* s2 = R"(a)" "b)"; // 原始字符串字面量后随普通字符串字面量若接下来三个字符是 **<::**且后继字符不是 **:** 或者 **>**,则把 **<** 自身当做预处理记号(而非代用记号 <: 的首字符)。struct Foo { static const int v = 1; }; std::vector<::Foo> x; // OK,<: 未被当作 [ 的代用记号 extern int y<::>; // OK,同 extern int y[]。 int z<:::Foo::value:>; // OK,int z[::Foo::value]; (C++11 起)
  • 头文件名预处理记号仅在 #include 指令中形成。
1
std::vector<int> x; // OK,<int> 不是头文件名

阶段 4

1) 执行预处理器

2) #include 指令所引入的每个文件都经历阶段 1 到 4 的处理,递归执行。

3) 此阶段结束时,所有预处理器指令都应从源(代码)移除。

阶段 5

1) 将字符字面量字符串字面量中的所有字符从源字符集转换到执行字符集(可以是 UTF-8 这样的多字节字符集,只要阶段 1 中所列的基本源字符集的 96 个字符都拥有单字节表示即可)。

2) 将字符字面量和非原始字符串字面量中的转义序列和通用字符名展开,并转换到执行字符集。 若某个通用字符名所指定的字符不是执行字符集的成员,则结果是由实现定义的,但保证不是空(宽)字符。

注意:某些实现能以命令行选项控制此阶段所进行的转换:gcc 和 clang 用 -finput-charset 指定源字符集的编码,用 -fexec-charset 和 -fwide-exec-charset 指定无编码前缀的 (C++11 起)字符串和字符字面量中的执行字符集的编码,而 Visual Studio 2015 Update 2 及之后版本分别用 /source-charset 和 /execution-charset 指定源字符集和执行字符集。

阶段 6

拼接相邻的字符串字面量

阶段 7

进行编译:将各个预处理记号转换成记号。将所有记号当作一个翻译单元进行语法和语义分析并进行翻译。

阶段 8

检验每个翻译单元,产生所要求的模板实例化的列表,其中包括显式实例化所要求的实例化。定位模板定义,并进行所要求的实例化,以产生实例化单元

阶段 9

将翻译单元、实例化单元和为满足外部引用所需的库组件汇集成一个程序映像,它含有在其执行环境中执行所需的信息。

注意

某些编译器不实现实例化单元(又称为模板仓库模板注册表),而是简单地在阶段 7 编译每个模板实例化,存储代码于其所显式或隐式要求的对象文件中,然后由连接器于阶段 9 将这些编译后的实例化缩减到一个。

引用

  • C++11 standard (ISO/IEC 14882:2011):
  • C++98 standard (ISO/IEC 14882:1998):

预编译指令

基本的预编译指令

  • $#$ 空指令
  • $#define$ 定义宏
  • $#include$ 包含一个源代码文件
  • $#undef$取消已经定义的宏
  • $#if$如果给定条件为真,则编译下面的代码
  • $#ifdef$如果宏已经定义就编译下面的代码
  • $#ifndef$如果宏没有定义,就编译下面的代码
  • $#elif$如果前面的$#if$给定条件不为真,当前条件为真,就编译下面的代码
  • $#endif$结束一个$#if….#else$条件编译块
  • $#error$停止编译并且显示错误信息

$# define$ 的一些使用

预处理过程会把源代码中出现的宏标识符替换成宏定义时的值。记住仅仅是进行标识符的替换。

例子如下:

  • 用#define实现求最大值和最小值的宏

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <stdio.h>
    #define MAX(x,y) (((x)>(y))?(x):(y))
    #define MIN(x,y) (((x)<(y))?(x):(y))
    int main(void)
    {
    #ifdef MAX //判断这个宏是否被定义
    printf("3 and 5 the max is:%d\n",MAX(3,5));
    #endif
    #ifdef MIN
    printf("3 and 5 the min is:%d\n",MIN(3,5));
    #endif
    return 0;
    }
    /*
    * (1)三元运算符要比if,else效率高
    * (2)宏的使用一定要细心,需要把参数小心的用括号括起来,
    * 因为宏只是简单的文本替换,不注意,容易引起歧义错误。
    */
  • 宏定义错误使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <stdio.h>
    #define SQR(x) (x*x)
    int main(void)
    {
    int b=3;
    #ifdef SQR//只需要宏名就可以了,不需要参数,有参数的话会警告
    printf("a = %d\n",SQR(b+2));
    #endif
    return 0;
    }
    /*
    *首先说明,这个宏的定义是错误的。并没有实现程序中的B+2的平方
    * 预处理的时候,替换成如下的结果:b+2*b+2
    * 正确的宏定义应该是:#define SQR(x) ((x)*(x))
    * 所以,尽量使用小括号,将参数括起来。
    */
  • 宏参数的连接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <stdio.h>
    #define STR(s) #s
    #define CONS(a,b) (int)(a##e##b)
    int main(void)
    {
    #ifdef STR
    printf(STR(VCK));
    #endif
    #ifdef CONS
    printf("\n%d\n",CONS(2,3));
    #endif
    return 0;
    }
    /* (绝大多数是使用不到这些的,使用到的话,查看手册就可以了)
    * 第一个宏,用#把参数转化为一个字符串
    * 第二个宏,用##把2个宏参数粘合在一起,及aeb,2e3也就是2000
    */
  • 用宏得到一个字的高位或低位的字节

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <stdio.h>
    #define WORD_LO(xxx) ((byte)((word)(xxx) & 255))
    #define WORD_HI(xxx) ((byte)((word)(xxx) >> 8))
    int main(void)
    {
    return 0;
    }
    /*
    * 一个字2个字节,获得低字节(低8位),与255(0000,0000,1111,1111)按位相与
    * 获得高字节(高8位),右移8位即可。
    */
  • 用宏定义得到一个数组所含元素的个数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <stdio.h>
    #define ARR_SIZE(a) (sizeof((a))/sizeof((a[0])))
    int main(void)
    {
    int array[100];
    #ifdef ARR_SIZE
    printf("array has %d items.\n",ARR_SIZE(array));
    #endif
    return 0;
    }
    /*
    *总的大小除以每个类型的大小
    */

$#ifdef$,$#ifndef$,$#endif$…的使用

以上这些预编译指令,都是条件编译指令,也就是说,将决定那些代码被编译,而哪些不被编译

  • 实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #include <stdio.h>
    #include <stdlib.h>
    #define DEBUG
    int main(void)
    {
    int i = 0;
    char c;
    while(1)
    {
    i++;
    c = getchar();
    if('\n' != c)
    {
    getchar();
    }
    if('q' == c || 'Q' == c)
    {
    #ifdef DEBUG//判断DEBUG是否被定义了
    printf("We get:%c,about to exit.\n",c);
    #endif
    break;
    }
    else
    {
    printf("i = %d",i);
    #ifdef DEBUG
    printf(",we get:%c",c);
    #endif
    printf("\n");
    }
    }
    printf("Hello World!\n");
    return 0;
    }
    /*#endif用于终止#if预处理指令。*/

其他指令

1
2
3
4

#error指令将使编译器显示一条错误信息,然后停止编译。
#line指令可以改变编译器用来指出警告和错误信息的文件号和行号。
#pragma指令没有正式的定义。编译器可以自定义其用途。典型的用法是禁止或允许某些烦人的警告信息。