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";

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

相关推荐
用户6120414922131 天前
C语言做的文本词频数量统计功能
c语言·后端·敏捷开发
小莞尔4 天前
【51单片机】【protues仿真】基于51单片机的篮球计时计分器系统
c语言·stm32·单片机·嵌入式硬件·51单片机
小莞尔4 天前
【51单片机】【protues仿真】 基于51单片机八路抢答器系统
c语言·开发语言·单片机·嵌入式硬件·51单片机
liujing102329294 天前
Day03_刷题niuke20250915
c语言
第七序章4 天前
【C++STL】list的详细用法和底层实现
c语言·c++·自然语言处理·list
l1t5 天前
利用DeepSeek实现服务器客户端模式的DuckDB原型
服务器·c语言·数据库·人工智能·postgresql·协议·duckdb
l1t5 天前
利用美团龙猫用libxml2编写XML转CSV文件C程序
xml·c语言·libxml2·解析器
Gu_shiwww5 天前
数据结构8——双向链表
c语言·数据结构·python·链表·小白初步
你怎么知道我是队长5 天前
C语言---循环结构
c语言·开发语言·算法