欢迎光临散文网 会员登陆 & 注册

【C/C++】在预处理之前,编译器做了什么事情?- 翻译阶段1&2

2023-03-31 21:07 作者:冒-_-泡  | 我要投稿

很多资料会告诉你,C或C++的编译过程包括预处理、编译、链接三个步骤,不过实际规定要更复杂一些,例如上篇提到的通用字符处理,就是在预处理之前

C或C++为代码的编译(广义概念)定义了一整套流程,称为翻译阶段(translation phases),由于具体到这个语言,编译(狭义概念)一般被认为是预处理和链接之间的事情,这里特别用了翻译一词,流程是从处理代码文本开始,从设备上读取源文件的IO流程不算在内

翻译分多个阶段,每个阶段负责处理相对独立的流程:

  • 1:字符相关

  • 2:行相关

  • 3:为预处理做准备的词法分割

  • 4:预处理(即#define、#include之类)

  • 5&6:处理字符串字面量(编码、字面量连接等)

  • 7:编译

  • 8:C链接;C++模板实例化

  • 9:C++链接

C语言有8个阶段,C++则是9个,之所以多一个是因为C++的模板实例化流程被独立出来了(不过一些具体实现也会和编译阶段混在一起)

参考:https://zh.cppreference.com/w/cpp/language/translation_phases

从第4阶段开始,就是我们比较熟悉的流程了,而第3阶段和预处理有关,且较为复杂,其中一些内容可以独立来说,所以先主要讲讲1和2阶段做了什么事情

阶段1

字符处理流程在上期已经提到了一部分,或者可以说是一大部分,除了通用字符名(\u或\U那些)和具体编译器扩展的字符功能(直接写汉字也算),还做了两件小事

一个是行尾指示符都被替换为标准的换行符,这里的“行尾指示符”是和操作系统相关,其实就是我们熟知的“\r\n”(dos/win)、“\n”(*nix)、“\r”(老mac),其中单独的“\r”目前已经被mac废弃了,所以可以说就是dos和unix两种风格(也叫CRLF和LF),如果按书面意思,就是根据所在平台,将其统一换成“\n”

不过实际测试的话,会发现即便在linux下,很多编译器也会将行尾的“\r\n”替换为“\n”,可以看做是一种兼容行为了,然而,这往往是一种编译器自行的扩展实现,不同编译器的处理可能是有差别的

(可能有人说,回车和换行都是空白字符,C和C++又不是换行敏感的,这里的差别会体现出来吗?有一个地方会体现,就是原始字符串,这个以后再细讲)

另一个字符处理是三标符序列,如果代码中存在“??”开头的三标符,则替换其实际对应的符号,对应关系:

这一点是因为早期字符集的问题,不过随着时代发展,这个语法在C++17开始已经废弃了,而在老的C++标准下,很多编译器也有默认关闭的情况,如果在老标准下写代码,或维护很老的代码,则需要注意一下

注意三标符的替换是在阶段1,是一个非常早期、简单粗暴的处理,这跟字符串中的转义不是一回事,和\u\U一样,它不只能用于字符串中,而是在正常代码中来表示被替换的字符

那么假如你的代码开发中是开启了这个功能,而你又想在字符串中使用三标符的原始样子该怎么办呢,两个办法:字符串字面量连接,和问号本身的转义:

前者是利用了翻译阶段6做的事情:相邻的字符串字面量会被连接在一起,从而在书面上规避了“??=”连写的情况,绕开了阶段1的这个处理,后者则可以说是C专门为问号准备的字符串中的转义符“\?”,其代表的就是问号本身,而唯一的作用就是在这里绕过阶段1的此处理

显然,由于阶段1并不解析字符串(解析字符串必须在词法阶段才行),以下写法是不行的:


阶段2

这个阶段做了两件事情,咱们先说其中简单的工作:如果一个源码文件(.h或.c或.cpp等)不是以换行符结尾,那么补上一个换行(当然并不会修改源文件,只是视为其有一个换行)

这可能是为了避免头文件被包含后,由于包含进来的文件末尾没有换行,可能会和后继的内容组成奇怪结果的一种规避措施

另外一件工作则是这个阶段处理的大头:如果一行是反斜杠结尾,则删除这个反斜杠和其后的换行。也就是说,将两行连接在一起,例:

很多人都知道C代码可以用反斜杠来折行,但估计不少初学者没想到的是,这个处理如此简单粗暴,整个流程可以就一行Python代码搞定:text.replace("\\\n", ""),就是普通的文本处理,连词法分析都没到

而在其他一些语言中,虽然支持反斜杠折行,但可能是放在编译阶段的,例如Python中:

显然Python是在做词法分析的时候处理这个的

这个替换流程是单趟操作,每次替换后,是从替换的位置开始继续向后处理,而并不会管替换本身产生的新的行尾反斜杠,例:

如果单看这个阶段本身是非常简单的,但是考虑到它是在阶段1之后进行,前后联系起来就有一些有意思的场景:

在阶段1,这个代码中的“??/”会被替换为反斜杠,然后在阶段2,第一行行尾是反斜杠,所以处理后变成了:

同样的道理,用这个方式折行的时候,需要保证反斜杠一定在行尾,原则上说,后面多一个空格都不行,不过GNUC对于这种情况,会给出一个警告然后忽略后面的空白符,还是当做折行来处理,是一个语法扩展

另外,“\\\r”在有的编译器中也会被视为折行连接,即便“\r”并非一个应该出现在代码文本中的字符,关于“\r”可能引发的问题在后面讨论原始字符串时也会提到

从阶段1的规定,我们知道可以用通用字符的转义,那么如果觉得转义码太长,是否可以折行呢:

在阶段1中,由于\u后面的格式不符合HHHH,不会做相关字符处理,而在阶段2中将两行连接起来后,形成了\u1234这样一个合法的通用字符名,那么编译器会回到阶段1将其继续按通用字符名来识别吗?

测试的话,gcc和clang是会识别的,但这个问题的标准答案其实是未定义行为,所以请不要这样写代码


【C/C++】在预处理之前,编译器做了什么事情?- 翻译阶段1&2的评论 (共 条)

分享到微博请遵守国家法律