ANSI C 与 Unix
一位维基教科书用户认为此页面应该拆分为更小的页面,每个页面包含更窄的主题。 您可以通过将此大页面拆分为更小的页面来提供帮助。请确保遵循命名策略。将书籍分成更小的部分可以提供更多的重点,并允许每个部分专注于一件事,这将使每个人受益。 |
本书旨在用作大学二年级教科书。对主题内容的涵盖确保读者对高级语言有基本了解,并熟悉合理现代的程序开发环境。本书的目的是将那些已经熟悉编程的人介绍给更面向系统级的语言,这种语言为程序员提供了更大的自由度(因此也带来了更大的责任)。
我们的本科计算机科学课程要求我们的学生学习至少三种高级语言。学位课程的前两个学期使用 Java。本书所针对的课程紧随其后,在二年级早期开设。本书假设学生已经成功完成了前两门课程,因此本书不需要过多关注一般程序结构、流程控制或语法。在提及结构、语法和流程控制时,重点应放在 C 与 Java 的区别上。
开发本书的理由是为学生提供一种方式
用占位符代替。这些可能都是不同的页面
最简单的编译情况是当您的所有源代码都设置在一个文件中时。这将消除任何不必要的同步多个文件或过度思考的步骤。假设有一个名为 'single_main.c' 的文件,我们要编译它。我们将使用类似于以下的命令行来进行编译
cc single_main.c
请注意,我们假设编译器名为“cc”。如果你使用 GNU 编译器,你需要写 'gcc'。如果你使用 Solaris 系统,你可能需要使用 'acc',等等。每个编译器可能以不同的方式显示其消息(错误、警告等),但在所有情况下,如果编译成功完成,你都会得到一个名为 'a.out' 的文件。注意,一些较旧的系统(例如 SunOs)带有不理解 ANSI-C 的 C 编译器,而是使用较旧的 'K&R' C 风格。在这种情况下,你需要使用 gcc(希望它已安装),或者学习 ANSI-C 和 K&R C 之间的区别(如果你没有必要,不建议这样做),或者迁移到其他系统。
你可能会抱怨 'a.out' 是一个过于通用的名称(它究竟从哪里来?- 嗯,这是一个历史名称,由于在较旧的 Unix 系统上编译的程序使用了名为“a.out 格式”的东西)。假设你希望生成的程序名为“single_main”。在这种情况下,你可以使用以下命令行来编译它
cc single_main.c -o single_main
</source
Every compiler I've met so far (including the glorious gcc) recognized the '-o' flag as "name the resulting executable file 'single_main'".
== Running The Resulting Program ==
Once we created the program, we wish to run it. This is usually done by simply typing its name, as in:
<syntaxhighlight lang=shell>
single_main
但是,这要求当前目录位于我们的 PATH 中(这是一个告诉我们的 Unix shell 在哪里查找我们试图运行的程序的变量)。在许多情况下,此目录不会被放置在我们的 PATH 中。啊哈!- 我们说。然后让我们向这台计算机展示谁更聪明,因此我们尝试
./single_main
这一次我们明确地告诉我们的 Unix shell 我们想要从当前目录运行程序。如果我们足够幸运,这将足够了。然而,还有一个障碍可能会阻碍我们的路径——文件权限标志。
当系统中创建一个文件时,它会立即被赋予一些访问权限标志。这些标志告诉系统谁应该被授予访问文件的权限,以及将给予他们的哪种访问权限。传统的 Unix 系统使用 3 种类型的实体来授予(或拒绝)访问权限:拥有该文件的用户,拥有该文件的组,以及所有其他用户。每个实体都可能被授予读取文件 ('r')、写入文件 ('w') 和执行文件 ('x') 的权限。现在,当编译器为我们创建程序文件时,我们成为该文件的拥有者。通常,编译器会确保我们获得该文件的所有权限——读取、写入和执行。然而,可能发生了一些错误,权限设置不同。在这种情况下,我们可以使用类似以下的命令来正确设置文件的权限(文件的所有者通常可以更改文件的权限标志)
chmod u+rwx single_main
这意味着“用户 ('u') 应该被授予 ('+') 读取 ('r')、写入 ('w') 和执行 ('x') 对文件 'single_main' 的权限。现在我们肯定能够运行我们的程序了。同样,通常你不会在运行该文件时遇到任何问题,但如果你将其复制到不同的目录,或将其通过网络传输到不同的计算机,它可能会失去其原始权限,因此你需要像上面所示那样正确设置权限。还要注意,你不能只将文件移动到另一台计算机并期望它运行——它必须是一台具有匹配的操作系统(以理解可执行文件格式)和匹配的 CPU 架构(以理解可执行文件包含的机器语言代码)的计算机。
最后,运行时环境必须匹配。例如,如果我们在具有一个版本的标准 C 库的操作系统上编译了程序,并且我们尝试在具有不兼容的标准 C 库的版本上运行它,程序可能会崩溃,或者抱怨它找不到相关的 C 库。对于快速发展的系统(例如,使用 libc5 的 Linux 与使用 libc6 的 Linux)来说尤其如此,因此请注意。
创建调试准备就绪的代码
[edit | edit source]通常,当我们编写程序时,我们希望能够调试它——也就是说,使用调试器测试它,该调试器允许我们逐步运行程序,在执行给定命令之前设置断点,查看程序执行期间变量的内容,等等。为了使调试器能够在可执行程序和原始源代码之间建立关系,我们需要告诉编译器将信息插入到生成的可执行程序中,这些信息将帮助调试器。此信息称为“调试信息”。为了将它添加到我们的程序中,让我们以不同的方式编译它
cc -g single_main.c -o single_main
'-g' 标志告诉编译器使用调试信息,并且大多数编译器都识别它。你会注意到,生成的程序文件比没有使用 '-g' 标志创建的文件大得多。尺寸差异是由于调试信息造成的。我们仍然可以使用 strip 命令删除此调试信息,如下所示
strip single_main
你会注意到,现在文件的大小甚至比我们最初没有使用 '-g' 标志时还要小。这是因为即使没有使用 '-g' 标志编译的程序也包含一些符号信息(例如函数名称),strip 命令会删除这些信息。你可能需要阅读 strip 的手册页(man strip)以了解有关此命令功能的更多信息。
创建优化代码
[edit | edit source]在创建程序并正确调试之后,我们通常希望将其编译成高效的代码,并且生成的程序文件尽可能小。编译器可以通过优化代码来帮助我们,可以优化速度(更快地运行),也可以优化空间(占用更小的空间),也可以两者兼顾。创建优化程序的基本方法如下
cc -O single_main.c -o single_main
'-O' 标志告诉编译器优化代码。这也意味着编译将花费更长时间,因为编译器尝试将各种优化算法应用于代码。这种优化应该是保守的,因为它确保我们代码仍然能够执行与没有优化时相同的函数(嗯,除非我们的编译器存在错误)。通常,我们可以通过在 '-O' 标志后添加一个数字来定义优化级别。数字越大,生成的程序越优化,编译器完成编译的速度越慢。需要注意的是,由于优化以多种方式改变了代码,因此随着我们提高代码的优化级别,非正常优化实际上改变我们代码的可能性会更高,因为其中一些优化往往是非保守的,或者仅仅是相当复杂的,并且包含错误。例如,长期以来,众所周知,使用 gcc 时,使用高于 2 的编译级别会导致可执行程序中的错误。在收到警告后,如果我们仍然想使用不同的优化级别(例如 4),我们可以这样做
cc -O4 single_compile.c -o single_compile
我们已经完成了。如果你阅读编译器的手册页,你很快就会注意到它支持几乎无限数量的与优化相关的命令行选项。要正确使用它们,需要透彻了解编译理论和源代码优化理论,否则你可能会破坏生成的代码。一个好的编译理论课程(最好基于 Aho、Sethi 和 Ulman 撰写的“龙书”)对你会有帮助。
获取额外的编译器警告
[edit | edit source]通常,编译器只生成有关不符合 C 标准的错误代码的错误消息,以及有关通常会导致运行时错误的事物的警告。但是,我们通常可以指示编译器给我们更多警告,这有助于提高我们源代码的质量,并发现将来会困扰我们的错误。对于 gcc,这可以通过使用 '-W' 标志来完成。例如,要让编译器使用它熟悉的所有类型的警告,我们将使用类似以下的命令行
cc -Wall single_source.c -o single_source
这会首先让我们感到厌烦——我们会收到各种似乎无关紧要的警告。但是,消除警告比消除使用此标志要好。通常,此选项可以为我们节省更多时间,而不是浪费时间,并且如果始终如一地使用,我们将习惯于编写正确的代码,而无需过多考虑。还需要注意的是,一些在某个体系结构上使用一个编译器可以正常工作的代码,如果我们使用不同的编译器或不同的系统来编译代码,可能会中断。在第一个系统上开发时,我们永远不会看到这些错误,但在将代码迁移到其他平台时,错误会突然出现。此外,在许多情况下,我们最终会希望将代码迁移到新系统,即使我们最初没有这种意图。
请注意,有时 '-Wall' 会给你太多错误,然后你可以尝试使用一些更简洁的警告级别。阅读编译器的手册以了解各种 '-W' 选项,并使用那些对你最有益的选项。最初它们听起来可能太奇怪了,毫无意义,但如果你是一位(或将来会成为)更有经验的程序员,你会了解哪些选项对你很有用。
编译单个源“C++”程序
[edit | edit source]现在我们已经了解了如何编译 C 程序,向 C++ 程序的过渡相当简单。我们所要做的就是使用 C++ 编译器,代替我们迄今为止使用的 C 编译器。因此,如果我们的程序源代码存储在一个名为 'single_main.cc' 的文件中('cc' 表示 C++ 代码。一些程序员更喜欢使用 'C' 后缀表示 C++ 代码),我们将使用类似以下的命令
g++ single_main.cc -o single_main
或者在某些系统上,你将使用“CC”而不是“g++”(例如,使用 Sun 的 Solaris 编译器),或者使用“aCC”(HP 的编译器),等等。你会注意到,对于 C++ 编译器,命令行选项的统一性较差,部分原因是直到最近,该语言还在不断发展,没有达成一致的标准。但无论如何,至少对于 g++,你将使用“-g”表示代码中的调试信息,使用“-O”表示优化。
所以你已经学会了如何正确地编译单源程序(希望你现在已经玩过一些编译器,并尝试过一些你自己的例子)。然而,迟早你会发现,将所有源代码放在一个文件中是相当有限的,原因有几个
- 随着文件大小的增长,编译时间也会随之增长,而且对于每一个小改动,整个程序都需要重新编译。
- 以这种方式,几个人一起在一个项目上工作是很难的,如果不是不可能的话。
- 管理你的代码变得更加困难。撤销错误的更改几乎是不可能的。
解决这个问题的方法是将源代码分成多个文件,每个文件包含一组紧密相关的函数(或者,在C++中,包含单个类的所有源代码)。
编译多源C程序有两种可能的方法。第一种是使用单个命令行编译所有文件。假设我们有一个程序,它的源代码位于文件“main.c”,“a.c”和“b.c”中(位于本教程的“multi-source”目录中)。我们可以这样编译它
cc main.c a.c b.c -o hello_world
这将导致编译器分别编译每个给定的文件,然后将它们全部链接到一个名为“hello_world”的可执行文件中。关于这个程序有两个注释
- 如果我们在一个文件中定义一个函数(或一个变量),并试图从第二个文件中访问它们,我们需要在第二个文件中将它们声明为外部符号。这是使用C的“extern”关键字完成的。
- 命令行中呈现源文件的顺序可以改变。编译器(实际上是链接器)将知道如何从每个文件中获取相关代码到最终程序中,即使第一个源文件试图使用在第二个或第三个源文件中定义的函数。
这种编译方式的问题是,即使我们只在一个源文件中进行更改,当我们再次运行编译器时,所有文件都将被重新编译。
为了克服这个限制,我们可以将编译过程分为两个阶段——编译和链接。让我们先看看这是如何完成的,然后解释
cc -c main.cc
cc -c a.c
cc -c b.c
cc main.o a.o b.o -o hello_world
前3个命令分别接受一个源文件,并将其编译成一个名为“目标文件”的东西,具有相同的名称,但带有“.o”后缀。是“-c”标志告诉编译器只创建一个目标文件,而不是现在就生成最终的可执行文件。目标文件包含源文件的机器语言代码,但有一些未解析的符号。例如,“main.o”文件引用了一个名为“func_a”的符号,该符号是文件“a.c”中定义的函数。
当然,我们不能像那样运行代码。因此,在创建3个目标文件之后,我们使用第4个命令将3个目标文件链接到一个程序中。链接器(现在由编译器调用)从3个目标文件中获取所有符号,并将它们链接在一起——它确保当从目标文件“main.o”中的代码调用“func_a”时,目标文件“a.o”中的函数代码被执行。此外,链接器还将标准C库链接到程序中,在本例中,是为了正确解析“printf”符号。
为了了解这种复杂性实际上对我们有帮助,我们应该注意到,通常链接阶段比编译阶段快得多。当进行优化时,这一点尤其明显,因为这一步是在链接之前完成的。现在,假设我们更改源文件“a.c”,并且我们想要重新编译该程序。我们现在只需要两个命令
cc -c a.c
cc main.o a.o b.o -o hello_world
在我们的小例子中,很难注意到速度的提升,但在有几十个文件的情况下,每个文件包含几百行源代码,节省的时间是相当可观的;更不用说更大的项目了。
现在我们已经了解到编译不仅仅是一个简单的过程,让我们试着看看为了编译一个C程序,编译器所采取的完整步骤列表。1. 驱动程序——我们调用“cc”的东西。这实际上是“引擎”,它驱动着编译器所包含的整个工具集。我们调用它,它开始逐个调用其他工具,并将每个工具的输出作为下一个工具的输入。2. C预处理器——通常称为“cpp”。它接受一个C源文件,并处理所有预处理器定义(#include文件,#define宏,使用#ifdef的条件源代码包含等)。你可以单独调用它对你的程序进行处理,通常使用类似的命令
cc -E single_source.c
尝试一下,看看生成的代码是什么样的。3. C编译器——通常称为“cc1”。这是真正的编译器,它将输入文件转换成汇编语言。如你所见,我们使用了“-c”标志来调用它,以及C预处理器(以及可能还有优化器,请继续阅读),以及汇编器。4. 优化器——有时作为单独的模块提供,有时作为编译器模块的一部分。它处理对代码表示的优化,该代码表示是语言无关的。这样,你就可以对不同编程语言的编译器使用相同的优化器。5. 汇编器——有时称为“as”。它接受编译器生成的汇编代码,并将它转换成保存在目标文件中的机器语言代码。使用gcc,你可以告诉驱动程序只生成汇编代码,使用类似的命令
cc -S single_source.c
6. 链接器-加载器——这个工具接受所有目标文件(以及C库),并将它们链接在一起,形成一个可执行文件,该文件采用操作系统支持的格式。目前常见的格式称为“ELF”。在SunOs系统和其他旧系统中,使用了一种名为“a.out”的格式。这种格式定义了可执行文件的内部结构——数据段的位置,源代码段的位置,调试信息的位置等等。如你所见,编译被分成许多不同的阶段。并非所有编译器都采用完全相同的阶段,有时(例如,对于C++编译器)情况甚至更复杂。但基本思路是相同的——将编译器分成许多不同的部分,以便给程序员提供更大的灵活性,并允许编译器开发者在不同的编译器中尽可能多地重用模块,用于不同的语言(通过替换预处理器和编译器模块),或用于不同的架构(通过替换汇编器和链接器-加载器部分)。
存根
需要在这里包含
Petercooper 贡献了这本书的初始结构。 Raquibul Islam (Rana) 贡献了使用 gcc 的命令行编译部分。