符号和连接
我们知道,声明是用于向代码中引入名字的语法,而定义是一种特殊的声明形式。我们目前学过的声明有变量声明、函数声明、类声明和别名声明等。
这些名字可以声明在命名空间作用域(俗称全局作用域),类作用域和复合语句(块)作用域当中。
int a; // 命名空间作用域的声明
struct A { // 命名空间作用域的声明
int b(); // 类作用域的声明
};
int main() { // 命名空间作用域的声明
int c; // 复合语句作用域的声明
}
对于每一个名字,标准还规定了它们拥有一个被称作“连接”的属性。每一个名字可以依据其连接属性分入以下三类:无连接(No linkage)、内部连接(Internal linkage)和外部连接(External linkage)。
注意用词上的区别:我用“链接”(Linking)来表示链接器所做的行为,“连接”(Linkage)表示一个名字固有的属性。
所有复合语句作用域的声明都是无连接的。其它作用域的声明可以根据一些规则归为内部连接或外部连接。
考虑多个文件同时参与翻译,比如本章开头提到的情形。每一次编译处理的文件称为一个翻译单元(Translation Unit):比如 f.cpp
g.cpp
和 main.cpp
分别是三个翻译单元。每个翻译单元都会各自执行自己的编译过程,互不干扰,最后在链接过程中合并为一个可执行文件。
那么重点来了:内部连接的名字是每个翻译单元独有、互不干扰的,而外部连接的名字则是所有翻译单元共享的。(无连接的名字由于作用域限制,不会在多文件翻译中有影响。)
比如,void f();
中,f
是一个外部连接的名字。那么,尽管在 f.cpp
这个翻译单元中出现了一次 f
的声明,在 main.cpp
中又出现了一次 f
的声明,但由于 f
是外部连接的,所以链接过程中将认为这两个名字指代的是同一个东西,从而把它们整合到一起。
再举一个内部连接的例子。假设你已经知道了 const int a{42};
中 a
是一个内部连接的名字。那么,如果
像这样,f.cpp
和 g.cpp
两个翻译单元中都出现了 a
这个名字,由于 a
是内部连接的,所以在链接时会认为 f.cpp
中的 a
和 g.cpp
中的 a
不是一个东西。也就是说,每一个翻译单元中内部连接的名字不会被别的翻译单元所使用。
在所有内部或外部连接的名字当中,函数名和变量名是比较重要的:它们与稍后提到的单一定义原则关系密切。为了令行文简便,称带有内部或外部连接的函数名和变量名为符号(Symbol)。
那么如何判断一个名字是内部连接还是外部连接的呢?先看以下几条大体上的规则:
- 类型(类、枚举、别名)总是外部连接的;
- 默认情形下,符号是外部连接的;
- 默认情形下,只读变量是内部连接的。
这基本上已经覆盖了大多数的情形。比如之前的两个例子:f
是函数,所以它是符号,所以它是外部连接的。而 a
是只读变量,所以它是内部连接的。
using Int = int; // 外部连接
class C {}; // 外部连接
int a{42}; // 外部连接
void f(); // 外部连接
const int b{42}; // 内部连接
constexpr int b{42}; // 内部连接(常量蕴含只读变量)
除了这些大体的规则,还有一些琐碎的细则:
- 用
static
修饰一个符号,可以让它成为内部连接的; - 用
extern
修饰一个符号,可以让它称为外部连接的; - 匿名命名空间可以让其中所有的名字(含类型名和符号)都成为内部连接的。
extern
是英文 external 的缩写,就表明这个声明引入的符号是外部连接的。对于非只读的声明,这个修饰是没有意义的;但对于只读变量声明,可以让它变成外部连接:
int a{42}; // 外部连接
extern int extA{42}; // 还是外部连接
const int b{42}; // 内部连接
extern const int extB{42}; // 外部连接
在 C/C++ 的语言体系内,有时可以将 static
视为 extern
的反义词(这是为了不引入多余的关键字而做出的牺牲),即 static
修饰表明一个符号是内部连接的。当然,对于只读变量来说是没有意义的:
const int a{42}; // 内部连接
static const int a{42}; // 还是内部连接
void f(); // 外部连接
static void f(); // 内部连接
最后一条细则是“匿名命名空间”(Anonymous namespace)。顾名思义,就是不提供名字的命名空间:
匿名命名空间中所有名字都会注入到上级命名空间,如同添加了一条 using namespace 匿名;
。
namespace {
void f() { /* [...] */ } // 内部连接
}
/* 如同添加了一条 using namespace 匿名; */
int main() {
f(); // OK,指代匿名命名空间中的 f
}
所以匿名命名空间和其它命名空间不同,不是用来解决命名冲突的。它的作用就是纯粹地让里面的名字变成内部连接的。
最后是类作用域声明的归类:类的成员函数和静态成员总是外部连接的,而类的非静态成员数据则是无连接的。所以,类的成员函数和静态成员也是符号。
非静态成员数据是无连接的,因为它们总是属于某个类的对象的一部分。而它们在链接时所表现的行为取决于整个对象的行为。所以,只有整个对象的连接性质,没有其中一个单独的非静态成员数据的连接性质。