在运行 c++ 代码前,编译器和连接器的需要完成它们的工作。至于编译器和连接器是如何运作,本文就不作叙述。如果想要程序能够顺利地跑起来,我们需要了解 C++ 中的三个至关重要的概念 - translation unit (编译单元,下面简称 TU) 、one definition rule (定义一次规则,下面简称 ODR) 和 linkage (连接) 。
所谓的 TU 就是每个 source file (.cpp 文件) 和 它所引用的 header files (.h 或者 .hpp 文件)。而 linkage 可以理解为一个变量或者一个函数是否只作用于某个 TU 。这里说的变量和函数,不包括类的成员变量和成员函数。它的作用域是 file scope (文件作用域),我们可以笼统地概括为 global scope (全局作用域)。
我们在开发或者练习的过程中偶尔都会遇到一些报错,例如 redefinition (重定义) 或者 multiple definitions (存在多次定义) 。为了清楚这些错误出现的原因,我们需要了解上面提到的第三个概念 linkage 。
而 linkage 又分为 internal linkage (内部连接) 和 external linkage (外部链接)。内部连接,顾名思义,如果一个变量是属于内部连接的话,那么它在每一个 TU 里面都是独一无二的。我们通过下面代码片段来探究 internal linkage 为何物。
// a.h
int* const i_ptr = new int(111); // 这里定义的是 const 指针,并不是指向 const 对象的指针
//a.cpp
#include "a.h"
void scope_a()
{
std::cout << *i_ptr << "\n";
}
// b.cpp
#include "b.h"
#include "a.h"
void scope_b()
{
*i_ptr = 222;
std::cout << *i_ptr << "\n";
}
// main.cpp
#include "a.h"
#include "b.h"
int main()
{
*i_ptr = 333;
std::cout << *i_ptr << "\n"; // 打印 333
scope_a(); // 打印 111
scope_b(); // 打印 222
}
从上面代码片段可以看出每个独立的 TU 都有自己的一份 i_ptr。而每一份独立存在的 i_ptr 并不会互相影响。换句话说就是,它们根本不知道对方的存在,因为它们只是基于它们所在的 TU 的 copy,谁都不是本尊,谁又都是本尊。当然,如果把 i_ptr 的声明从 a.h 文件中抹杀的话,那么所有 TU 的 i_ptr 都是非法存在。
以下的对象是默认属于内部连接[^1]:
- const object (如
const int*、const double等等) - constexpr objects (如
constexpr char*) - typedefs
- static objects in namespace scope (如全局
static int age)
请注意第 4 点:
static objects in namespace scope
很多刚学习 c++ 不久的开发者未必清楚 static 对于全局变量的影响。file scope 变量,包括 namespace 内的变量,一旦在声明前被加上 static 关键字,那么这个变量就属于内部连接,其效果同 int* const i_ptr 的例子完全一样。
现在我们已经清楚知道内部连接的作用,那么外部连接就相当于是内部连接的“相反数”。即属于外部连接的变量或者函数在每一个包含它的 TU 里面都是同一个对象。来看下面的例子:
// a.h
int num = 111;
void scope_a();
//a.cpp
#include "a.h"
void scope_a()
{
std::cout << *i_ptr << "\n";
}
// b.cpp
#include "b.h"
#include "a.h"
void scope_b()
{
std::cout << num << "\n";
}
// main.cpp
#include "a.h"
#include "b.h"
int main()
{
std::cout << num << "\n";
scope_a();
scope_b();
}
很遗憾,上面的代码发生了连接错误:
LNK2005 "int num" (?num@@3HA) already defined in A.obj
LNK2005 "int num" (?num@@3HA) already defined in A.obj
LNK1169 one or more multiply defined symbols found
num 的声明和定义是在 a.h 同时进行,所以出现这样的报错是正常的,因为违反了 ODR 。为了修正这样的错误,extern 关键字被 派上用场。值得注意的是,如果要使用 extern 去修饰一个变量的话,该变量的声明和定义必须在 source file (即 .cpp 文件) 内进行。
//a.cpp
#include "a.h"
int num = 111;
void scope_a()
{
std::cout << *i_ptr << "\n";
}
// b.cpp
#include "b.h"
#include "a.h"
extern int num;
void scope_b()
{
std::cout << num << "\n";
}
// main.cpp
#include "a.h"
#include "b.h"
extern int num;
int main()
{
num = 999;
std::cout << num << "\n"; // 打印 999
scope_a(); // 打印 999
scope_b(); // 打印 999
}
如果想让 const object 属于外部连接的话,那么必须在每一个 TU 里面的对应的变量或者函数前面加上 extern 关键字。用上面的代码作为例子,num 在 a.cpp 文件内的声明必须是
//a.cpp
extern const int num = 111;
像 translation unit (编译单元) 、one definition rule 还是 linkage (连接) 这种概念都很容易被人忽略,但它们是非常值得我们花时间去了解。
^1 :https://docs.microsoft.com/en-us/cpp/cpp/program-and-linkage-cpp?view=vs-2019