符号和连接

我们知道,声明是用于向代码中引入名字的语法,而定义是一种特殊的声明形式。我们目前学过的声明有变量声明、函数声明、类声明和别名声明等。

这些名字可以声明在命名空间作用域(俗称全局作用域),类作用域和复合语句(块)作用域当中。

int a;       // 命名空间作用域的声明
struct A {   // 命名空间作用域的声明
    int b(); // 类作用域的声明
};
int main() { // 命名空间作用域的声明
    int c;   // 复合语句作用域的声明
}

对于每一个名字,标准还规定了它们拥有一个被称作“连接”的属性。每一个名字可以依据其连接属性分入以下三类:无连接(No linkage)、内部连接(Internal linkage)和外部连接(External linkage)。

注意用词上的区别:我用“链接”(Linking)来表示链接器所做的行为,“连接”(Linkage)表示一个名字固有的属性。

所有复合语句作用域的声明都是无连接的。其它作用域的声明可以根据一些规则归为内部连接或外部连接。

考虑多个文件同时参与翻译,比如本章开头提到的情形。每一次编译处理的文件称为一个翻译单元(Translation Unit):比如 f.cpp g.cppmain.cpp 分别是三个翻译单元。每个翻译单元都会各自执行自己的编译过程,互不干扰,最后在链接过程中合并为一个可执行文件。

那么重点来了:内部连接的名字是每个翻译单元独有、互不干扰的,而外部连接的名字则是所有翻译单元共享的。(无连接的名字由于作用域限制,不会在多文件翻译中有影响。)

比如,void f(); 中,f 是一个外部连接的名字。那么,尽管在 f.cpp 这个翻译单元中出现了一次 f 的声明,在 main.cpp 中又出现了一次 f 的声明,但由于 f 是外部连接的,所以链接过程中将认为这两个名字指代的是同一个东西,从而把它们整合到一起。

再举一个内部连接的例子。假设你已经知道了 const int a{42};a 是一个内部连接的名字。那么,如果

// f.cpp
const int a{42};
// g.cpp
const int a{56};

像这样,f.cppg.cpp 两个翻译单元中都出现了 a 这个名字,由于 a 是内部连接的,所以在链接时会认为 f.cpp 中的 ag.cpp 中的 a 不是一个东西。也就是说,每一个翻译单元中内部连接的名字不会被别的翻译单元所使用。

在所有内部或外部连接的名字当中,函数名和变量名是比较重要的:它们与稍后提到的单一定义原则关系密切。为了令行文简便,称带有内部或外部连接的函数名和变量名为符号(Symbol)。

那么如何判断一个名字是内部连接还是外部连接的呢?先看以下几条大体上的规则:

  1. 类型(类、枚举、别名)总是外部连接的;
  2. 默认情形下,符号是外部连接的;
  3. 默认情形下,只读变量是内部连接的。

这基本上已经覆盖了大多数的情形。比如之前的两个例子:f 是函数,所以它是符号,所以它是外部连接的。而 a 是只读变量,所以它是内部连接的。

using Int = int; // 外部连接
class C {};      // 外部连接

int a{42}; // 外部连接
void f();  // 外部连接

const int b{42};     // 内部连接
constexpr int b{42}; // 内部连接(常量蕴含只读变量)

除了这些大体的规则,还有一些琐碎的细则:

  1. static 修饰一个符号,可以让它成为内部连接的;
  2. extern 修饰一个符号,可以让它称为外部连接的;
  3. 匿名命名空间可以让其中所有的名字(含类型名和符号)都成为内部连接的。

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)。顾名思义,就是不提供名字的命名空间:

// 匿名命名空间
namespace {

// 以下所有声明的名字均为内部连接
void f();
class A{};
int a{42};
// [...]

}

匿名命名空间中所有名字都会注入到上级命名空间,如同添加了一条 using namespace 匿名;

namespace {
void f() { /* [...] */ } // 内部连接
}
/* 如同添加了一条 using namespace 匿名; */
int main() {
    f(); // OK,指代匿名命名空间中的 f
}

所以匿名命名空间和其它命名空间不同,不是用来解决命名冲突的。它的作用就是纯粹地让里面的名字变成内部连接的。

最后是类作用域声明的归类:类的成员函数和静态成员总是外部连接的,而类的非静态成员数据则是无连接的。所以,类的成员函数和静态成员也是符号。

非静态成员数据是无连接的,因为它们总是属于某个类的对象的一部分。而它们在链接时所表现的行为取决于整个对象的行为。所以,只有整个对象的连接性质,没有其中一个单独的非静态成员数据的连接性质。

最近更新:
代码未运行