引言

C++中有很多问题在运行单个文件的时候不会出现,但是在多个文件编译链接的过程中会出现无法预料的结果和报错,问题的根源在于不同文件相互暴露的标识符名称很可能重复并引起ODR(one definition rule,一次定义规则)违规,或者是我们想要暴露的标识符被保留在了文件内部,无法被其他文件识别。C++提供了几个关键字来解决这些问题,包括inline,extern,static。

正文

首先我们知道,更内层的代码块里定义的变量会遮蔽外层代码块里定义的同名变量,同时生命周期也会被绑定至代码块,因此,想要让变量在不同文件间共享,必须把它定义成全局变量。

对于全局标识符,可以在其他文件中使用的,我们称其具有外部链接;只在本文件内部可见的,我们称其具有内部链接。

局部变量没有链接属性

int x;        // 全局变量:作用域从声明点持续到文件结束
int main(){}

全局变量的作用域从声明点开始一直持续到文件结束。

按照这个逻辑,可以发现不加修饰的函数都隐式具有外部链接,所以可以在其他文件中使用。

命名空间

但是,如果所有函数都可以被其他文件使用,名称重复而引起ODR违规的概率就很大,解决方法是使用命名空间。在命名空间内定义变量和函数,并使用作用域解析运算符::来解析作用域并使用标识符,可以很大概率上避免命名冲突。

使用不带命名空间名称的作用域解析符(前置的::),可以显式调用全局作用域的标识符。

namespace mymath{
    void add(int a, int b) {}   // 命名空间内的add
}

void add(int a, int b) {}       // 全局作用域的add,与mymath::add互不冲突

int main(){
    mymath::add(1, 2);          // 作用域解析运算符指定命名空间内的版本
    ::add(1, 2);                // 前置::显式选中全局版本
    return 0;
}

在使用其他文件中的命名空间成员时,必须在包含的头文件中声明该命名空间,并把成员放在命名空间内。

可以使用using声明,从而避免繁琐的解析作用域步骤。重要的是,应该避免使用using namespace XXX,以防造成命名冲突(这本来也是命名空间想要解决的问题)。

using std::cout;   // using声明:此后直接写cout即可,无需std::前缀

static

如果我们只是想要定义一个全局变量,但是不想让它被其他文件使用,可以使用static标记内部链接。

static int x = 10;      // static:显式内部链接
const int y = 20;       // const:隐式内部链接
constexpr int z = 30;   // constexpr:隐式内部链接

const和constexpr全局变量是隐式内部链接的,static对函数也有一样的效果。

static void add(int, int);   // 函数同样可以用static标记内部链接

在不同文件中各自定义的同名内部对象(和函数)被认为是独立的实体(即使它们的名称和类型相同),因此不违反一次定义规则。

在新标准中,static的这份职责正在被匿名命名空间取代。匿名命名空间内的所有成员都具有内部链接。

namespace{              // 匿名命名空间:所有成员都是内部链接
    void add(int, int);
}

inline命名空间的用途则恰好相反:它不是把成员藏进文件内部,而是把成员默认暴露给外围的命名空间。给某个嵌套命名空间标上inline后,它的成员可以像外围命名空间的成员一样,不加前缀直接使用。

inline namespace v1{     // inline命名空间:默认版本
    void add(int a, int b) {}
}
namespace v2{           // 普通命名空间:需要显式指定前缀
    void add(int a, int b) {}
}

int main(){
    v2::add(1, 2);      // 显式选择v2版本
    add(2, 1);          // 不加前缀,默认选中inline的v1版本
    return 0;
}

这常用于库的版本管理:把希望默认使用的版本标记为inline,使用者不加前缀就能用上它,想锁定其他版本时再显式写命名空间前缀。

另外,当static应用于局部变量时,变量会在首次执行到定义处时初始化一次,后续调用会跳过初始化,以实现在函数的不同调用之间记住这个值。

int counter(){
    static int n = 0;   // 只在首次执行到这一行时初始化一次
    return ++n;         // 之后每次调用都跳过初始化,在上次的值上累加
}

extern

我们可以用extern对变量赋予外部链接。

extern const int x{10};      // extern加初始化器:定义外部链接的const变量
extern constexpr int y{20};  // constexpr同样能通过extern获得外部链接

想要在其他文件中使用,我们需要在使用的文件中以相同方式前向声明,不带初始化器。

constexpr变量尽管可以赋予外部链接,但是不能进行前向声明,其他文件想使用它时只能声明成extern const。

仅将extern用于全局变量前向声明或 const 全局变量定义,不要将extern用于非 const 全局变量定义(它们隐式为extern)。

inline

inline历史上一般用于请求把小的函数调用直接替换成代码片段,以此来减小调用开销。然而,在现代 C++ 中,inline 关键字不再用于请求函数内联展开。这有很多原因,最主要的一点是现代的优化编译器通常比人类有更好的判断来决定是否内联,不需要程序员自己决定。

现代的inline已经演变成了允许多个定义的含义。这意味着声明inline允许我们在不同文件中定义相同的名称而不违反ODR,前提是每处的定义必须完全相同。

事实上,历史上,我们使用 inline 来请求内联展开,而 ODR 豁免是一个为了使此类函数能够在多个翻译单元中内联展开所需的细节。现在,我们使用 inline 来进行 ODR 豁免,并让编译器处理内联展开的事情。内联函数的工作机制没有改变,我们关注的重点改变了。

一般将内联函数或变量定义在头文件中,以便能在不同的文件中使用同一变量(或常量)和函数。

inline double pi() { return 3.1415926; }   // 定义在头文件中,包含它的每个文件共用这一份定义

以下函数是隐式内联的:

  • 在类、结构或联合类型定义内部定义的函数。
  • constexpr / consteval 函数。
  • 从函数模板隐式实例化的函数。

以下变量是隐式内联的:

  • 静态 constexpr 数据成员(C++17起)。

小结

在多文件的项目中,我们需要明确地知道哪些名称具有外部链接,哪些具有内部链接,并做好相应的防护措施。一个最优的实践是,尽可能使用局部变量而不是全局变量,除非你确切地知道并想要在文件间共享变量。