第20章 程序编译
编写的cpp文件和h文件是如何组织成二进制可执行文件的,这是本章要讨论的内容。除了使用变量定义、函数定义与调用以及类定义与类实现外,C++还提供了诸如“#inlcude”等预处理机制,用以辅助代码的编写,同时,本章将针对程序的编译和调试进行讲解。
本章主要涉及以下知识点。
❑程序的编译过程:介绍程序编译过程中的各关键环节。
❑预处理:介绍预处理中各个命令。
❑VC6调试入门:介绍如何用VC6进行程序调试。
❑其他调试方法:介绍另外几种常用的程序调试方法。
20.1 程序的编译流程
第1章中介绍了使用VC6开发环境编译运行C++程序的基本步骤,提及“选择‘Build’菜单中的‘Build工程名’命令,或直接按F7键即可实现对整个工程所有源代码文件的编译和链接。编译链接无误即可生成一个后缀为exe的可执行文件”。所有的工作都是由VC6开发环境完成的,对一个程序员来说,了解其内部运作细节有助于对程序编写有个全局性的把握,程序的编译流程大体可分为编辑、预处理、编译和链接4个步骤。
1.编辑
源文件是通过键盘输入计算机的,存储在硬盘上的程序文件,在DOS和Windows环境下其后缀名为cpp(定义文件)或h(头文件),一个程序可能包含很多源文件。将源文件输入计算机并进行修改和保存的过程就称为“编辑”。
2.预处理和编译
编译用于将每个编译单元翻译成二进制代码文件,在DOS和Windows环境下,二进制代码文件的后缀名为obj,在Unix环境下其后缀名为o。
首先来看下什么是编译单元,在很多C++教材中有这样的论述“头文件不参加编译,只有实现文件(cpp文件)才参加编译”,这种简单的表述是不科学的,容易给读者留下错误的印象。事实上,编译器处理的对象是由单个cpp文件和其中递归包含的头文件组成的编译单元。
当一个cpp文件在编译时,预处理器首先递归包含头文件,形成一个含有所有必要信息的单个源文件,这个源文件就是一个编译单元。这个编译单元会被编译成为一个与cpp文件名同名的目标文件(.o或是.obj)。
每个cpp文件对应一个编译单元,而每个编译单元都会生成一个二进制代码文件,所以,每个cpp文件对应着一个二进制代码文件。
预处理器(Preprocessor)是在真正的编译开始之前由编译器调用的独立程序。预处理器可以删除注释、包含文件以及执行宏替代,稍后会详细介绍编译预处理的相关内容。
3.链接
链接程序的作用是将编译得到的零散的二进制代码文件组合成二进制可执行文件,有以下两个意义。
(1)对应于某个编译单元的对象文件包含在其他编译单元中定义的函数引用或其他指定项的引用,而这些函数或项仍没有被解析。
例如,某个程序由两个cpp文件组成,分别为A.cpp和B.cpp,两个cpp文件和其中递归包含的头文件组成两个编译单元,经过预处理和编译生成二进制代码文件A.obj和B.obj,假设函数C是定义在B.cpp中,但在A.cpp中对函数C进行了说明和调用,这样,在A.obj中实际上仅包括对C函数的引用,其二进制定义代码需要从B.obj中提取,插入到A.obj的调用处,这个过程称为函数解析(Resolve),这个组合的过程是由链接器完成的。不仅是函数,变量(诸如有外部链接性的全局变量)也涉及解析的问题。
注意
当B.cpp没有定义函数C时,编译时不会产生错误,但链接时却会提示,有未解析的对象。因此,在代码错误分析时要弄清问题是出在编译阶段还是链接阶段。
(2)建立与库函数的链接。
出于商业考虑或保密需要,C++标准库函数和其他第三方库函数是以二进制代码形式提供的,库文件的后缀名为lib,如果在编译单元中声明并调用了库函数,便需要对库函数进行解析,链接器会从对应的库文件中将该函数的二进制代码抽出插入到调用处,当然,如果库中无此函数或找不到对应的库,也会发生未解析(unresolved)的错误。
在每个编译单元生成的obj文件中,变量和函数并未得到系统分配的绝对地址,因而无法执行。实质上链接是为程序中的变量和函数分配绝对地址,使二进制文件可执行的过程。此外,根据不同的操作系统,链接器会为组合后的二进制程序“添加”一些与操作系统有关的代码,使其可运行,这也解释了为什么在Windows环境下编译生成的可知性程序,如exe格式文件在Unix中无法运行。