0%

C++从源文件到可执行文件:预处理

C++ 程序从源文件到可执行文件,要经历9个翻译阶段。可以将一般认为的C++源文件到可执行文件的4个阶段模糊地和这9个翻译阶段相对应:

  • 预处理(prepressing):翻译阶段1-6
  • 编译(compile)、汇编(assembly):翻译阶段7-8
  • 链接(Linking):翻译阶段9

本文介绍翻译阶段1-6的全部过程:

  1. 源文件字符集到基本源字符集的映射
  2. 处理续行符
  3. 分解源文件,恢复始字符串字面量,去掉注释。
  4. 递归预处理
  5. 字面量从 源字符集执行字符集 的转换
  6. 拼接相邻的字符串字面量。

可以看到大部分阶段都和字符集的转换字面量的处理相关。
一个非原始字符串字面量字符串字面量或一个字符字面量,先从源文件字符集 映射 源字符集,之后又从源字符集 映射到 执行字符集


阶段一:源文件字符集到基本源字符集的映射

  • 将源文件的各个单独字节,映射为基本源字符集的字符。基本源字符集由96个字符组成,包括5个空白字符,52大小写英文字母,10个数字和29个标点符号。
  • 任何无法被映射到基本源字符集中的字符的源文件字符,均被替换为Unicode形式(用 \u\U 转义)。

gcc和clang当中,可以使用-finput-charset来指定源文件字符集的编码。Visual Studio中可以使用/source-charset来指定源文件字符集的编码。

gcc doc | 3.13 Options Controlling the Preprocessorgcc doc | 1.1 Character sets

The files input to CPP might be in any character set at all. CPP’s very first action, before it even looks for line boundaries, is to convert the file into the character set it uses for internal processing. That set is what the C standard calls the source character set. It must be isomorphic with ISO 10646, also known as Unicode. CPP uses the UTF-8 encoding of Unicode.

The character sets of the input files are specified using the -finput-charset=charset

Set the input character set, used for translation from the character set of the input file to the source character set used by GCC. If the locale does not specify, or GCC cannot get this information from the locale, the default is UTF-8. This can be overridden by either the locale or this command-line option. Currently the command-line option takes precedence if there’s a conflict. charset can be any encoding supported by the system’s iconv library routine.


阶段二:处理续行符

  • 当反斜杠出现于行尾(其后紧跟换行符)时,删除该反斜杠和换行符并将两个物理源码行组合成一个逻辑源码行。

宏定义为预处理指令,预处理指令要求在一行内完成,因此为了保证可读性,续行符\经常在宏的定义中被使用。

1
2
3
4
// include/linux/kernel.h
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})


阶段三:分解源文件,恢复始字符串字面量,去掉注释。

  • 将源文件分解为注释,空白字符和下列各种预处理记号:
    • 头文件名,如<iostream>
    • 标识符
    • 预处理数字
    • 字符与字符串字面量(包括用户定义的)
    • 运算符与标点
    • 不属于任何其他类别的单独非空白字符
  • 恢复在任何原始字符串字面量(的首尾双引号之间在阶段 1 和 2 期间进行的所有变换。
  • 以一个空格字符替换每段注释。

原始字符串字面量是指以R开头,形如R"tag(content)tag"形式的字符串字面量。其中tag是一个最多由16个字符组成的序列,不可以包含(\`。tag`可以为空。原始字符串字面量常被用于正则表达式的书写,以减少转义字符,增加可读性:

1
R"regexp((?:"(?:\\"|[^"])*"|'(?:\\'|[^'])*'))regexp";


阶段四:递归预处理

  • 预处理指令控制预处理器的行为。每个指令占据一行并拥有下列格式:
    • # 字符
    • 预处理指令(defineundefincludeififdefifndefelseelifendiflineerrorpragma 之一)
    • 实参(取决于指令)
    • 换行符
  • 预处理指令可以是空指令,不产生任何效果。
  • 预处理指令不得来自宏展开。

  • 预处理的过程

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


#pragma once vs #ifndef include guards

#pragma once

  • 优点:
    • #pragma once使得当前源文件在一次编译中只被#include一次。从原理来讲要比 include guards 更快,因为 include guards 还需要对重复的#ifdef #endif等等预处理指令进行预处理。不过在实践中几乎没有差别。
  • 缺点:
    • 非标准
    • gcc3.4以后才支持#pragma once

include guards


阶段五:字面量从 基本源字符集 到 执行字符集 的转换

  • 字符字面量字符串字面量中的所有字符从基本源字符集转换到执行字符集(gcc默认为UTF-8)
  • 字符字面量非原始字符串字面量中的转义序列和通用字符名展开,并转换到执行字符集

gcc和clang当中,可以使用-fexec-charset-fwide-exec-charset指定执行字符集的编码。Visual Studio中可以使用/execution-charset来指定执行字符集的编码。

gcc doc | 3.13 Options Controlling the Preprocessor

-fexec-charset=charset
Set the execution character set, used for string and character constants. The default is UTF-8. charset can be any encoding supported by the system’s iconv library routine.

-fwide-exec-charset=charset
Set the wide execution character set, used for wide string and character constants. The default is UTF-32 or UTF-16, whichever corresponds to the width of wchar_t. As with -fexec-charset, charset can be any encoding supported by the system’s iconv library routine; however, you will have problems with encodings that do not fit exactly in wchar_t.


阶段六:

拼接相邻的字符串字面量。
即将

1
2
const auto str = "this line might be too long, so that it has to be "
"wrapped."

合并为
1
const auto string = "this line might be too long, so that it has to be wrapped."


参考

《程序员的自我修养链接、装载与库》