探索 C++ 中的内部连接和外部连接

在运行 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

友情链接更多精彩内容