编译一个.c文件时,C预处理器(C Pre-Processor,CPP)首先(递归地)包含头文件,形成包含所有必要信息的单个源文件。之后,编译这个中间文件(称为编译单元)生成一个和根文件名字一样的.o文件(目标文件)。链接程序把各种编译单元内产生的符号链接起来,生成一个可执行程序。有两种不同的链接:内部链接和外部链接。链接类型将会直接影响我们如何将一个给定的逻辑构造合并到物理设计中。
如果一个名字对于它的编译单元来说是局部的,并且在链接时与在其他编译单元中定义的标识符名称不冲突,那么这个名字有内部链接。
内部链接意味着对这个定义的访问受到当前编译单元的限制。也就是说,一个有内部链接的定义对于任何其他编译单元都是不可见的,因此在链接过程中不能用来解析未定义的符号。例如:
虽然在文件作用域中定义,但是关键字static决定了链接是内部的。内部链接的另一个例子是枚举:
枚举是定义(不只是声明),但是它们自己绝对不会把符号引入到.o文件中。为了让内部链接定义影响一个程序的其他部分,它们必须放置在头文件而不是.c文件中。
用内部链接定义的一个重要的例子就是类的定义。类Point(见图1-1)的描述是一个定义,而不是一个声明;因此,它不能够在同一个作用域的编译单元内重复定义。在单个编译单元外部使用的类必须在一个头文件中定义。内联函数的定义(例如图1-1下方所显示的operator==的定义)是用内部链接定义的另一个例子。
如果一个名字有外部链接,那么在多文件程序中,在链接时这个名字可以和其他编译单元交互。
外部链接意味着这个定义不局限于单个的编译单元。在.o文件中,具有外部链接的定义产生外部符号,这些外部符号可以被所有其他的编译单元访问,用来解析其他编译单元中未定义的符号。这种外部符号在整个程序中必须是唯一的,否则这个程序不能链接。
非内联成员函数(包括静态成员)有外部链接,非内联、非静态free函数(即非成员函数)也一样。图1-2显示了一个外部链接的函数。
注意,我们提到非成员函数时,始终是指free函数而不是指友元函数。一个free函数不必是任何类的友元,无论如何它都应该是一个实现细节(见3.6节)。
C++编译器会在任何可能的地方把一个内联函数体直接替换成函数调用,而不将任何符号引入.o文件。有时(出于各种原因,例如递归或动态绑定)编译器会选择制定一个内联函数的静态拷贝。这个静态拷贝只将一个局部符号引入当前的.o文件,这个符号不能与外部符号交互。
一个声明只对当前的编译单元有用,所以声明本身根本未将任何内容引入到.o文件。请仔细阅读下面的这些声明:
这些声明本身并没有影响由此而产生的.o文件内容。每个声明只是命名了一个外部符号,让当前编译单元在需要的时候能够获取相应全局定义的访问。这实际上是符号名(例如,调用函数)的使用而不是声明本身,这将导致在.o文件中加入一个未定义的符号。正是这个事实允许早期的原型设计拥有如下特点:如果不需要缺少的功能,则在运行程序时使用部分实现的对象。
在前面的例子中,这三个声明每一个都可以激活对一个外部定义函数或对象的访问。我们可能会草率地认为这些“声明”有外部链接。但是,有其他类型的声明不能用来激活对外部定义的访问。我们将会常常把这些类型的声明当作有“内部”链接的。例如:
这是一个typedef声明。它没有在.o文件中加入任何符号,也没有访问一个带有外部链接的全局对象:它的链接是内部的。一种恰好带有内部链接的重要声明是类的声明:
上面所有这些声明都将引入的名字Point作为某种用户自定义的类型,效果完全相同;特定的声明类型(例如,类)不必与实际的定义类型匹配(例如,联合体):
这些声明的定义也可能指其有内部链接。这个属性把类声明与前面例子中的外部声明区分开。类声明和类定义对于.o文件都毫无用处,它们只对当前的编译单元有用。
另一方面,静态类数据成员(在类定义内声明)有外部链接:
静态类数据成员s_numPoints(如上所示)只是一个声明,但是它在.c文件中的定义有“外部”链接:
注意,按照语言规范,每个静态类数据成员在最终的程序中都必须只定义一次。
最后,C++语言处理枚举和类的方式不同:
在C++中,未定义就声明一个枚举是不可能的。如我们将看到的,类声明经常用来代替预处理#include指令,以便在未定义之前就声明一个类。