重学C++ (二)

前文提到,编译器在对C++源代码进行处理时,Trigraph的替换是在Raw String之前的,即R"??("会被转化成R"["。然而运行一下就可以知道,这种替换似乎并没有发生。

查看C++的Working Draft,可以找到这么一段话,可知并不是第一步没有替换,而是替换后又被改回去了:

Any source file character not in the basic source character set is replaced by the universal-character-name that designates that character. … where this replacement is reverted in a raw string literal.

因此,要想知道自己的代码是否会像它看上去那样运行,需要适当了解一下C++代码编译链接的过程。

第一阶段

由于源码文件是普通的文本文件,因此可以根据其编码方式将其中的一些字符映射到C++语言的字符集(96个)。在这个字符集中,空白字符只有空格、水平制表符\t、垂直制表符\v、换页\f、换行符\n

不同系统下的换行标志可能会有所不同,因此这一步也要将所有的行末指示符都变成我们熟悉的换行符。

有些字符并不能对应到这96个字符,比如一个中文的字符串。此时它们被替换为Universal Character Name(UNC),一般会采取\uXXX的表示法来转化。这个UCN其实是个很有意思的东西,它甚至可以在标识符中出现!不过与本文内容无关,下次再提啦。

在遇到Trigraph Sequences时,会替换成相应的单字符表示。这个操作似乎从C++17开始就不执行了。

第二阶段

当一行代码写得太长时,为了美观我们可以在行尾加一个\进行折行。这个折行就是在第二阶段被复原的。当一个\跟着一个\n时,编译器会把二者同时删掉。

注意,这是个一次性的操作,只有源码行末的\会被删掉。一行行末的两个连续\若遇上了一个空行(对应于源码的两个连续\n),只会吞掉其中一个换行符。

要知道,这依旧是在识别任何token之前的操作。可以猜测一下这段代码的运行结果:

printf("\\
n");

标准中还提到,如果在替换完\后产生了一个UNC,结果将是未定义的。

而如果一个非空源码的末尾没有换行符(原本就没有或是被\吞掉),编译器应该在最后加上换行符。这在C++11之前是一个未定义的结果。然而我在gcc 4.8.3上得到了一个warning: backslash-newline at end of file [enabled by default],在Apple LLVM version 6.1.0上得到了一个error: expected unqualified-id。不知道是不是我理解有误orz

第三阶段

本阶段首先会区分出注释、分隔符(第一阶段的空白字符)和预处理token。

预处理token是编译前的最小词法单元,它包含六种元素:
1. 头文件名,例如<iostream>"stdio.h"等。
2. 标识符。
3. 数值常量。
4. 字符常量、字符串常量和用户自定义字面量(user-defined literals)。
5. 操作符和标点,包括前篇提到的Alternative tokens。
6. 其他不符合前五项的独立非空白字符。

接下来,就是对本位开头提到的Raw String进行恢复处理。在这个阶段识别出Raw String后,第一、第二阶段对它进行的所有更改都将会被恢复。

最后,每条注释都被替换成空格。

第四阶段

这个阶段是编译预处理的最后阶段,会产生一个单独的文件传递给编译器。

此阶段有一个单独的preprocessor的概念,它的行为由预处理指令行所引导。这些预处理指令行有着如下的特征:
1. 以#开头。
2. 预处理指令:define, undef, include, if, ifdef, ifndef, else, elif, endif, line, error, pragma
3. 参数(取决于具体指令)。
4. 换行

预处理器根据这些指令行采取的操作主要有四种:
1. 摘取所需要编译的代码段落,靠#if, #ifdef系列的条件选择。
2. 文本替换。#define, #undef和操作符#, ##
3. 头文件。#include,此时会递归地将这些头文件遍历这前四个阶段。
4. 触发一个error。#error
5. 编译器决定的一些行为。#pragma和操作符_Pragma控制的内部编译指令。
6. 文件名和行信息。#line会修改__LINE____FILE__的默认值,常用于其他语言生成的C++代码。

其实一个空指令(#直接跟一个换行)也是不会报错的,但这似乎并没有什么用。更详细的预处理指令用法,以后再写啦。

此阶段结束时,所有的预处理指令行都会被删除。

第五阶段

本阶段主要进行了字符的转换。将源码的字符常量和字符串常量转变为可执行字符集中的字符。所谓可执行字符集(execution character set)是用户可控且平台相关的,gcc默认为UTF-8。

和第一阶段不同,这个阶段主要针对字符常量和字符串常量中的字符,有可能包含C++字符集之外的字符,如UTF-8字符等。

本阶段还会将这些常量中的转义符真正的替换成它所代表的字符,比如字符串中的"\n"替换成换行符等等。

若无对应字符可以转换,它和编译器实现相关,若无规则对应则会变成一个空字符。

第六阶段

合并相邻字符串。

第七阶段

这也就开始了我们所熟悉的编译阶段。所有的预处理token都会经过词法分析啊语义分析之类的操作,被识别为一个具体的翻译单元(所谓的token),空白分隔符就没用啦。

第八阶段

这个阶段主要是进行模版的实例化。

模版(template)包括函数模版和类模版。既然被称作模版,那就是不能直接用的意思了嘛,所以必须被模版参数实例化。过程叙述起来也很简单,首先提取出需要实例化的模版列表,然后进行实例化产生普通类、函数或成员函数,最后检查是否有失败的实例化。

注意,这仅仅是一个很粗糙的叙述,模版的实例化其实是一个很大的课题。比如显式实例化和隐式实例化等等,以后有时间再写吧。

第九阶段

这个阶段主要处理的就是外部引用啦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值