C语言篇:翻译阶段

C语言标准把从源文件到可执行文件的处理过程分为多个阶段,称为翻译阶段。

翻译阶段是个概念模型,编译器实现可能进行优化,或多个阶段同时进行,只要保证最终结果与依次执行翻译阶段一致即可。

阶段1

把源文件中的所有字符映射为源字符集 中的对应字符,并且把行尾指示符 转换为换行符LF

源字符集是C语言标准发明的一个概念,它没有说明到底是什么字符集,而是说它必须包含基本字符集 。基本字符集也是C语言标准发明的一个概念,它包括52个英文大小写字母、10个数字、29个英文标点符号和5个空白字符,也就是C语言代码必须的最小字符集。基本字符集里的所有字符都必须编码为1个字节

其实关于字符集和编码,C语言标准也没讲明白,甚至左右脑互搏。一边定义源文件使用的字符集就是源字符集,一边又要从源文件映射到源字符集。我之后会专门写一篇文章讲C语言的字符集和编码。目前我们不用纠结这个概念,关注编译器是怎么处理的就好。

gcc 会根据locale 确定源文件的编码,如果无法从locale确定,则默认编码是UTF-8 ,可以通过参数-finput-charset指定源文件编码。

gcc的源字符集就是UTF-8编码。如果经过上述步骤确定的源文件编码不是UTF-8,则会将源文件从该编码转换为UTF-8编码。

这个阶段还有一个操作是替换三字符序列 ,不过该特性的使用场景极其罕见并且已经在C23被移除,所以就不介绍了。

阶段2

如果某一行以反斜杠\结尾,则删除反斜杠和换行符,从而把两行合并成一行。

该操作可以通过两两合并把多行合并成一行,如下所示:

c 复制代码
a\
b\
c

处理后:

c 复制代码
abc

该操作不会重复扫描,如下所示:

c 复制代码
a\\

b

只有第二个\会被处理,尽管处理之后第一个\成为了该行最后一个字符,也不会折返回来处理第一个\

这个阶段还要求处理后的源文件必须以换行符结尾,但几乎所有编译器实现都不做此限制。

阶段3

源文件被分解为预处理标记空白字符序列注释

每条注释都被替换为1个空格

所有换行符都如数保留,其他空白字符序列由实现决定是否合并成1个空格。

源文件不能以不完全的预处理标记或注释结尾,比如:

c 复制代码
/* comment

缺少*/,编译会报错。

gcc会保留缩进空白字符,删除所有尾随空白字符,并且合并非空白字符之间的空白字符,如下所示:

c 复制代码
a     b       

    
        c     d

处理后:

c 复制代码
a b


        c d

阶段4

执行预处理

所有通过#include指令包含进来的文件都递归执行阶段1-4。

删除所有预处理指令和预处理操作符。

预处理是个非常复杂的过程,我会专门写一篇文章来讲。

阶段5

没有编码前缀的字符常量字符串字面量 中的字符和转义序列从源字符集 转换为执行字符集

执行字符集也是C语言标准发明的一个概念,它也包含一个基本字符集,要求跟源字符集的基本字符集一样,额外多了几个控制字符。

编码前缀指的是u8uUL,这些可以加在字符常量和字符串字面量之前,为其指定运行时编码。这些字符在阶段7进行处理。

只有通用转义序列和Unicode转义序列才会转换为执行字符集,比如\n\u4e00。八进制和十六进制转义序列会转换为其对应的单字节数据,比如\33

执行字符集决定字符常量和字符串字面量在程序运行时的编码,从而影响程序输出的编码。因此该阶段只转换这两者而不是整个源文件,因为这两者会作为数据在程序执行时使用,而其他部分只用于编译。

只有当执行字符集与终端的编码相同时,源代码中的字符数据才能被正确显示。然而执行字符集必须在编译时确定,此时不可能知道运行时的终端编码,所以依赖执行字符集的C语言程序只能在特定的单一编码环境中正确运行。

C语言的国际化一直非常糟糕,即使后面推出了宽字符和Unicode支持也没有改善太多。

gcc 的默认执行字符集是UTF-8 ,可以通过参数-fexec-charset指定执行字符集。

gcc的执行字符集不受C语言标准的限制。比如UTF-16不存在单字节编码的字符,不符合基本字符集的要求,但gcc仍然可以把它作为执行字符集。不过这会导致一些问题。在阶段7字符串字面量会被转换为数组,不管执行字符集是什么,没有编码前缀的字符串字面量都是在末尾加一个单字节空字符,由于它不是个合法的宽字符串,用宽字符函数处理会导致内存越界。而且执行字符集把任何值为0的字节视为空字符,不管这个字节在编码中的位置,所以用普通函数处理会导致提前结束。

阶段6

合并相邻的字符串字面量。

c 复制代码
char *s = "abc"
          "de";

等价于:

c 复制代码
char *s = "abcde";

阶段7

空白字符被忽略。所有预处理标记作为一个翻译单元进行语法和语义分析,翻译成目标文件。

这一步就是我们常说的编译。

阶段8

解析外部对象和函数引用,链接库以满足外部引用。所有目标文件整合成单个可执行文件。

这一步就是我们常说的链接。

C语言不具备自动查找必要库的能力,编译器会默认链接某些库,而其他库都需要通过编译器参数手动链接。

翻译阶段的意义

翻译阶段明确了广义的编译过程中每种操作在逻辑上的先后顺序,从而尽量避免歧义。同时也从语法无法描述的角度对代码进行规范。

比如下面的代码:

c 复制代码
#define SWAPINT(A, B) ((A)^=(B),\
                      (B)^=(A),\
                      (A)^=(B))

因为处理行尾\在预处理之前,所以就实现了语法上不允许的多行预处理指令。

而下面的代码:

c 复制代码
// this is a comment\
printf("hello, world\n");

同样因为处理行尾\在处理注释之前,所以一个意外的\字符就能让行注释影响到代码。

再看下面的代码:

c 复制代码
char *s = "abc" // first line
          "de";

因为处理注释在合并字符串字面量之前,所以这两个字符串字面量依然是相邻的,可以合并。

相关推荐
lingggggaaaa2 小时前
免杀对抗——C2远控篇&PowerShell&有无文件落地&C#参数调用&绕AMSI&ETW&去混淆特征
c语言·开发语言·笔记·学习·安全·microsoft·c#
口袋物联2 小时前
设计模式之建造者模式在 C 语言中的应用(含 Linux 内核实例)
c语言·设计模式·建造者模式
切糕师学AI2 小时前
位带操作(Bit-Banding)是什么?
c语言·arm·嵌入式开发·cortex-m·位带操作
学习路上_write2 小时前
嵌入式系统bringup指南:软硬件调试
c语言·单片机·嵌入式硬件
say_fall2 小时前
C语言编程实战:每日一题 - day7
c语言·开发语言
小龙报3 小时前
《算法通关指南数据结构和算法篇(2)--- 链表专题》
c语言·数据结构·c++·算法·链表·学习方法·visual studio
醉颜凉3 小时前
环形房屋如何 “安全劫舍”?动态规划解题逻辑与技巧
c语言·算法·动态规划
望眼欲穿的程序猿4 小时前
Win系统Vscode+CoNan+Cmake实现调试与构建
c语言·c++·后端
星轨初途5 小时前
数据结构排序算法详解(2)——选择排序(附动图)
c语言·数据结构·经验分享·笔记·b树·算法·排序算法
合作小小程序员小小店5 小时前
游戏开发,桌面%小游戏,俄罗斯方块%demo,基于vs2022,c语言,背景音乐,easyX,无数据库,
c语言·开发语言