在C语言中,什么是翻译单元?

22

通常所说的翻译单元的定义是在预处理之后(包括头文件包含、宏等与源文件一起的内容)。这个定义很清楚,C标准(5.1.1.1,C11)指出:

C程序不必同时被翻译。本国际标准中,程序文本保存在称为源文件(或预处理文件)的单位中。一个源文件加上通过预处理指令 #include 包含的所有头文件和源文件就被称为预处理翻译单元。在预处理之后,预处理翻译单元就称为翻译单元。

更仔细地阅读第一句话:

C程序不必同时被翻译。

这意味着(按我的理解),C程序可以在同一时间进行翻译,而不一定将它们拆分为多个预处理源文件。同样在该段落的末尾,标准还说:

可以单独翻译翻译单元然后将它们链接在一起以生成可执行程序。

这可以(并且通常被解释为)编译单个目标文件,然后最终将它们链接在一起以生成单个可执行程序。但是,如果将以上语句提问并问:这是否意味着实现可以将多个源文件视为单个翻译单元,特别是对于以下调用:

gcc file1.c file2.c -o out

编译器可以访问整个源代码的地方是哪里?

特别地,如果一个实现将file1.c+file2.c(上面的代码)视为单个翻译单位,那么它是否可以被认为是不符合标准的?


2
标准确实没有任何关于GCC对其命令行参数解释的说明。这完全超出了ISO C的范围,因此甚至不符合标准中“实现定义”的含义。 - Iwillnotexist Idonotexist
1
只要这两个文件被视为单独的翻译单元,编写符合规范的代码并进行编译和链接是完全可能的。但如果将它们视为一个翻译单元,则编译会失败。例如,这些文件可能会分别定义具有相同名称的静态函数;如果将其视为单个翻译单元,则组合体将导致该函数多次定义。 - Jonathan Leffler
1
@IwillnotexistIdonotexist GCC(加上库、平台等)是一种实现,而是否认为GCC(或任何编译器)这样做是不符合规范的问题。事实上,问题中没有询问GCC的行为,只是将其作为编译命令的示例。 - P.P
但是,@usr,无论gcc file1.c file2.c是否导致GCC将它们视为单个TU完全取决于GCC。我的意思是,cat file1.c file2.c将它们连接起来;如果gcc感觉提供类似cat的功能,那么这样做是合法的。用户手册可以记录该选择。C编译器仍然可以存在于不存在“文件系统”中的“文件”驻留的上下文中。标准只选择称呼这些“单元”为_文件_。它不要求它们实际上常见理解中术语的文件。 - Iwillnotexist Idonotexist
4个回答

14
在你引用的第二行中:
程序文本保存在称为源文件(或预处理文件)的单位中,在此国际标准中定义。
如果有两个源文件,则有两个预处理文件,因此有两个预处理翻译单元,因此有两个翻译单元。 每个源文件对应一个。
标准没有定义“源文件”。我想编译器可能会说:“我正在通过声明file1.cfile2.c不是源文件来创造自己的版本!”并将它们连接起来,但这与程序员的期望相悖。 我认为你很难争辩说file1.c不是源文件。

1
编译器不能任意地这样做。它可以记录下行为,这种情况下,仔细的程序员在部署新编译器之前总是会阅读文档并调整他们的期望(或回到具有更传统行为的编译器)。 - rici
1
@rici 这个问题是关于编译器是否违反标准。如果你把标准中的“源文件”解释为“连接命令行参数指示的文件的结果”,那么编译器可以这么做,这并不会违反标准。 - M.M
2
我认为编译器将源文件定义为参数的串联是合法的,但这是实现定义的行为,因此只有在有文档记录时才是合法的。同样,更传统的编译器必须指定命令行中每个命名文件都被视为单独的翻译单元,如果您查找,您可能会找到该语句。 - rici
嗯...我实际上看到过 #include "whatever.cpp",所以...是的...那种事情是可能发生的... - bolov
3
如果命令行是 gcc file1.c + file2.c -o out,则很少有异议将连接视为单个标准用途的源文件。但是,由于我们都同意命令行语法没有标准化,因此无法争论 gcc file1.c file2.c -o out 是否也是一个连接。 - MSalters
这是一个相当有说服力的论点。但是C标准中是否有任何明确或隐含禁止它的文本? - P.P

13
然而,如果有人能够从上述陈述中提出问题并问:这是否意味着实现可以将多个源文件视为单个翻译单位?不是的。定义很清楚:

一个源文件加上通过预处理指令#include包含的所有头文件和源文件称为预处理翻译单位。预处理后,预处理翻译单位称为翻译单位。

翻译单位是对一个源文件及其包含文件进行预处理的结果。您可能同时翻译两个翻译单位的事实并不意味着您可以将它们视为一个翻译单位。

7
编译器可以同时翻译多个源文件,但不能改变它们的语义。同时翻译多个文件可能会更快(因为编译器只启动一次),并且允许更好的整个程序优化:其他翻译单元中调用函数的源代码在调用点处从其他翻译单元中可用。编译器可以检查被调用的代码并使用信息,就像使用单个翻译单元一样。来自gcc 6.3.0手册
调用的函数可以检查别名的缺失、指向对象的事实常量等,使编译器能够执行在一般情况下是错误的优化。
当然,这样的函数可以内联。
但是,编译器必须遵守(预处理)翻译单元的语义(与标准引用中预处理后的源文件相对应)。Malcolm提到了其中一个问题,即文件静态变量。我直觉认为可能还有其他更微妙的问题涉及声明和声明顺序。
另一个明显的源代码范围问题涉及定义。从n1570草案6.10.3.5中可以看出:
宏定义持续时间(独立于块结构)直到遇到相应的#undef指令或(如果没有遇到)直到预处理翻译单元结束。 这两个问题都禁止简单的C源文件连接; 编译器必须另外应用一些基本逻辑。

5
一个翻译单元指的是一个C文件,包括它所关联的.h头文件。很少会使用#include指令添加其他文件类型或其他C文件。
静态变量只能在翻译单元内可见。通常有一些具有外部链接的公共函数和许多静态函数和数据项来支持。因此,C翻译单元有点像单例模式的C++类。如果编译器不能正确处理静态,则不符合规范。
通常为每个翻译单元创建一个目标文件,然后由链接器进行链接。这实际上并不是标准规定的方式,但在文件易于创建且编译相对较慢的环境中,这是自然而明显的做法。

1
有点挑衅性地说,我认为编译速度相对较快(多少个核心?),但文件创建很昂贵。实际上,这可能一直是这种情况:我曾经在我的Atari ST上使用RAM磁盘进行编译,因为大容量存储是软盘。 - Peter - Reinstate Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接