C++学习笔记----10、模块、头文件及各种主题(三)---- 连接
本节讨论C++中连接的概念。C++源文件首先被预处理器处理,它处理所有的预处理指令,结果为翻译单元。所有的翻译单元独立地被编译为对象文件,包含了机器执行码,但是函数的引用还没有定义。解析这些引用是最后阶段,连接器,连接所有的对象文件一起形成最终的可执行文件。技术上讲,在编译过程中还有一些步骤,但对于我们讨论来讲,这个简化的观点已经足够了。
在C++翻译单元中的每个名字,包含了函数与全局变量,有连接或无连接,这指出了在哪儿可以定义该名字,从哪儿可以访问到它。有四种类型的连接:
- 无连接:名字只能在定义的范围内访问。
- 外部连接:名字在翻译单元访问。
- 内部连接(也叫做静态连接):名字只能从当前翻译单元访问,无法从其它翻译单元访问。
- 模块连接:名字从同一模块的翻译单元访问。
1、内部连接
缺省情况下,函数与全局变量有外部连接。然而,可以通过使用匿名命名空间来指定内部(或静态)连接。例如,假定有两个源文件:FirstFile.cpp与AnotherFile.cpp。下面是FirstFile.cpp:
void f();
int main()
{
f();
}
注意该文件提供了f()的原型,但是没有给出定义。下面是AnotherFile.cpp:
import std;
namespace
{
void f();
void f()
{
std::println("f");
}
}
该文件提供了f()的原型与定义。注意在两个不同的文件中写同一个函数的原型是合法的。如果把原型放到#include在每个源文件中的头文件中,这就是预处理器确切做的。在该示例中,没有使用头文件。原因是使用头文件易于维护(保持同步)原型的拷贝,但是现在C++有了模块的支持,推荐使用模块而不是头文件。
每个源文件编译成功,程序连接也没有问题:因为f()有外部连接,main()可以从不同的文件中进行调用。
然而,假定将f()函数放到AnotherFile.cpp中,使用匿名命名空间,让它进行内部连接,如下:
import std;
namespace
{
void f();
void f()
{
std::println("f");
}
}
匿名命名空间的实体有内部连接,因此按照在同一翻译单元的声明,可以在任何地方访问,有了这个修改,每个源文件仍然可以正常编译,但是连接步骤失败,因为f()有内部连接,在FirstFile.cpp中不可用。
另外一种方法是使用匿名命名空间给出一个名字进行内部连接用关键字static来前置声明。前面的匿名命名空间的例子可以书写如下。注意在f()定义的前面不需要重复static关键字。只要出现在函数名字的第一个实例前,就不需要重复。
import std;
static void f();
void f()
{
std::println("f");
}
代码的这个版本的语法也使用匿名命名空间完全一致。
警告:如果翻译单元 需要只在该翻译单元中要求的辅助实体,放到一个匿名命名空间给出内部连接。不鼓励使用static关键字。
2、extern关键字
相关的关键字:extern,看起来应该是static的对立面,指定了先于名字的外部连接,在特定场景下可以使用。例如,const与typedef缺省有内部连接。可以使用extern让它们进行外部连接。然而,extern有一些复杂。当指定一个名字为extern时,编译器认为这是一个声明,而不是一个定义。对于变量,这意味着编译器不会为这个变量分配空间。必须为这个变量提供一个不带有extern关键字的单独的定义。例如,下面是AnotherFile.cpp的内容:
extern int x;
int x { 3 };
在这种情况下extern没什么用,因为x缺省有外部连接。extern的真正用途是当你使用另外一个源文件中的x时,FirstFile.cpp:
import std;
extern int x;
int main()
{
std::println("{}", x);
}
这儿,FirstFile.cpp使用了一个extern声明,所以它可以使用x。编译器需要x的声明以便在main()中使用它。如果不用extern关键字声明x,编译器会认为这是一个定义会为x分配空间,使得连接步骤失败(因为在全局范围内有两个x变量)。有了extern,可以使得变量可以从多个源文件中进行全局访问。
警告:完全不推荐使用全局变量。它们令人迷惑且容易出错,特别是在大型程序中。要审慎使用它们!
唯一的例外是全局常数。不要定义同一个常数在所有地方:定义一次,随处使用。