C ++中的内部和外部链接

祝大家有美好的一天!

我们为您提供了一篇有趣的文章的翻译,该文章已作为C ++开发人员课程的一部分为您准备。 我们希望它对您以及我们的听众有用且有趣。

走吧

您是否曾经遇到过内部和外部通信术语? 是否想知道extern关键字是用来做什么的,或者静态内容的声明如何影响全局范围? 然后,本文适合您。

简而言之

翻译单元(.c / .cpp)及其所有头文件(.h / .hpp)都包含在翻译单元中。 如果对象或函数在翻译单元内部具有内部绑定,则此符号仅在此翻译单元内部对链接器可见。 如果对象或函数具有外部链接,则在处理其他翻译单元时,链接器将能够看到它。 在全局名称空间中使用static关键字可提供字符内部绑定。 extern关键字提供外部绑定。
默认编译器为字符提供以下绑定:

  • 非常量全局变量-外部绑定;
  • 常量全局变量-内部绑定;
  • 功能-外部链接。



基础知识

首先,让我们讨论讨论绑定所需的两个简单概念。

  • 声明和定义之间的区别;
  • 广播单位。

还应注意名称:当链接器与之一起使用的任何“代码实体”(例如,使用变量或函数(或使用类/结构,但不关注它们))时,我们将使用“符号”的概念。

公告VS。 定义

我们简要讨论声明和符号定义之间的区别:声明(或声明)告诉编译器特定符号的存在,并在不需要确切的存储器地址或符号存储的情况下允许访问此符号。 该定义告诉编译器函数主体中包含什么或变量需要分配多少内存。

在某些情况下,例如,当类的数据元素具有链接或值类型(即,不是链接,也不是指针)时,声明对于编译器是不够的。 同时,允许使用指向已声明(但未定义)类型的指针,因为它需要固定数量的内存(例如,在64位系统中为8字节),无论其指向的类型是什么。 要通过此指针获取值,需要定义。 另外,要声明一个函数,您需要声明(但不定义)所有参数(无论它是由值,引用还是指针使用)和返回类型。 仅在定义函数时才需要确定返回值和参数的类型。

功能介绍

定义和声明函数之间的区别非常明显。

int f(); //  int f() { return 42; } //  

变数

对于变量,则有所不同。 声明和定义通常不共享。 主要的是:

 int x; 

不仅声明x ,而且定义它。 这是由于对默认构造函数int的调用。 (在C ++中,与Java不同,默认情况下,简单类型(例如int)的构造函数不会将值初始化为0。在上面的示例中,x等于编译器分配的内存地址中的所有垃圾)。

但是,您可以使用extern关键字显式分隔变量声明及其定义。

 extern int x; //  int x = 42; //  

但是,当初始化extern并将其添加到声明中时,表达式变成一个定义,并且extern关键字变得无用。

 extern int x = 5; //   ,   int x = 5; 

广告预览

在C ++中,存在预声明字符的概念。 这意味着我们声明符号的类型和名称,以用于不需要定义的情况。 因此,我们不需要明显不需要的字符的完整定义(通常是头文件)。 因此,我们减少了对包含定义的文件的依赖。 主要优点是,在更改带有定义的文件时,我们初步声明该符号的文件不需要重新编译(这意味着包括它在内的所有其他文件)。

例子

假设我们有一个f的函数声明(称为原型),该函数声明按值接受Class类型的对象:

 // file.hpp void f(Class object); 

立即包括Class的定义-天真。 但是,由于我们刚刚声明了f ,足以为编译器提供Class声明。 因此,编译器将能够通过其原型识别该功能,并且我们将能够摆脱file.hpp对包含Class定义的文件的依赖,比如class.hpp:

 // file.hpp class Class; void f(Class object); 

假设file.hpp包含在其他100个文件中。 假设我们在class.hpp中更改Class的定义。 如果将class.hpp添加到file.hpp,file.hpp和包含它的所有100个文件,将需要重新编译。 多亏了对Class的初步声明,唯一需要重新编译的文件将是class.hpp和file.hpp(假设在其中定义了f)。

使用频率

声明和定义之间的重要区别是,符号可以声明多次,但只能定义一次。 因此,您可以根据需要多次声明一个函数或类,但是只能有一个定义。 这称为一个定义规则 。 在C ++中,以下工作:

 int f(); int f(); int f(); int f(); int f(); int f(); int f() { return 5; } 

这不起作用:

 int f() { return 6; } int f() { return 9; } 

广播单位

程序员通常使用头文件和实现文件。 但不是编译器-它们与翻译单元(简称翻译单元,简称TU)一起使用,有时也称为编译单元。 这种单元的定义非常简单-在初步处理后将任何文件传输到编译器。 确切地说,这是由于扩展宏预处理器的工作而获得的文件,包括取决于#ifdef#ifndef表达式的源代码以及所有#include文件的复制粘贴。

可以使用以下文件:

header.hpp:

 #ifndef HEADER_HPP #define HEADER_HPP #define VALUE 5 #ifndef VALUE struct Foo { private: int ryan; }; #endif int strlen(const char* string); #endif /* HEADER_HPP */ 

program.cpp:

 #include "header.hpp" int strlen(const char* string) { int length = 0; while(string[length]) ++length; return length + VALUE; } 

预处理器将产生以下转换单元,然后将其传递给编译器:

 int strlen(const char* string); int strlen(const char* string) { int length = 0; while(string[length]) ++length; return length + 5; } 

通讯技术

在讨论了基础知识之后,就可以开始建立关系了。 通常,通信是处理文件时链接器字符的可见性。 通信可以是外部的也可以是内部的。

对外沟通

当符号(变量或函数)具有外部连接时,其他文件的链接程序将看到该符号,即“全局”可见,所有翻译单元均可访问。 这意味着您必须在一个翻译单元的特定位置(通常在实现文件(.c / .cpp)中)定义这样的符号,以便它只有一个可见的定义。 如果在声明符号的同时尝试同时定义符号,或者将定义放在声明的文件中,则可能会激怒链接器。 尝试将文件添加到多个实现文件中会导致将定义添加到多个翻译单元中-您的链接器会哭泣。

C和C ++中的extern关键字(明确地)声明字符具有外部连接。

 extern int x; extern void f(const std::string& argument); 

两个字符都有一个外部连接。 上面已经指出,默认情况下,const全局变量具有内部绑定,非const全局变量具有外部绑定。 这意味着int x; -和extern int x;一样,对吗? 不完全是 int x; 实际上类似于extern int x {}; (因为使用int x,所以使用通用/括号初始化语法来避免最不愉快的解析(最令人讨厌的解析)); 不仅声明,而且定义x。 因此,不要将extern加到int x上。 全局声明与在外部声明变量时定义变量一样糟糕:

 int x; //   ,   extern int x{}; //      . extern int x; //      ,   

不好的例子

让我们在file.hpp中声明一个带有外部链接的函数f ,然后在其中定义它:

 // file.hpp #ifndef FILE_HPP #define FILE_HPP extern int f(int x); /* ... */ int f(int) { return x + 1; } /* ... */ #endif /* FILE_HPP */ 

请注意,您不需要在此处添加extern,因为所有函数都是显式extern。 也不需要分隔声明和定义。 因此,让我们像这样重写它:

 // file.hpp #ifndef FILE_HPP #define FILE_HPP int f(int) { return x + 1; } #endif /* FILE_HPP */ 

可以在阅读本文之前或在酒精或重物质(例如肉桂卷)的影响下阅读之后编写此类代码。

让我们看看为什么这不值得。 现在我们有两个实现文件:a.cpp和b.cpp,都包含在file.hpp中:

 // a.cpp #include "file.hpp" /* ... */ 


 // b.cpp #include "file.hpp" /* ... */ 

现在,让编译器工作并为上述两个实现文件生成两个转换单元(请记住, #include字面意思是复制/粘贴):

 // TU A, from a.cpp int f(int) { return x + 1; } /* ... */ 

 // TU B, from b.cpp int f(int) { return x + 1; } /* ... */ 

此时,链接器进行干预(绑定在编译后发生)。 链接器使用字符f并搜索定义。 今天,他很幸运,他发现了多达两个! 一个在翻译单元A中,另一个在翻译单元B中。链接程序很高兴冻结,并告诉您以下内容:

 duplicate symbol __Z1fv in: /path/to/ao /path/to/bo 

链接器为一个f字符找到两个定义。 由于f具有外部绑定,所以在处理A和B时,链接程序都可以看到它。显然,这违反了一个定义规则,并且会导致错误。 更准确地说,这将导致重复的符号错误,当您声明一个符号但忘记定义它时,您将收到不少于未定义的符号错误。

使用方法

声明外部变量的标准示例是全局变量。 假设您正在制作一个自烤蛋糕。 当然,与蛋糕相关联的全局变量应该在程序的不同部分中可用。 假设您蛋糕内的可食用电路的时钟频率。 对于所有巧克力电子产品的同步操作,自然在不同的部件中都需要此值。 声明这样的全局变量的(邪恶的)C方法是作为宏:

 #define CLK 1000000 

厌恶宏的C ++程序员会写出更好的真实代码。 例如,这:

 // global.hpp namespace Global { extern unsigned int clock_rate; } // global.cpp namespace Global { unsigned int clock_rate = 1000000; } 

(现代C ++程序员将要使用分隔文字:unsigned int clock_rate = 1'000'000;)

对讲机

如果符号具有内部连接,则仅在当前翻译单元内部可见。 不要将可见性与访问权限(例如私有)混淆。 可见性意味着链接器仅在处理声明了该符号的翻译单元时才可以使用此符号,而在以后(如与外部通信的符号一样)才可以使用该符号。 实际上,这意味着在头文件中声明带有内部链接的符号时,包括该文件的每个广播单元将收到该符号的唯一副本。 好像您已经在每个翻译单元中预先确定了每个这样的符号。 对于对象,这意味着编译器将为每个翻译单元分配一个全新的唯一副本,这显然会导致较高的内存成本。

要声明一个互连符号,C和C ++中存在static关键字。 这种用法不同于在类和函数(或通常在任何块中)中使用静态方法。

例子

这是一个例子:

header.hpp:

 static int variable = 42; 

file1.hpp:

 void function1(); 

file2.hpp:

 void function2(); 

file1.cpp:

 #include "header.hpp" void function1() { variable = 10; } 


file2.cpp:

 #include "header.hpp" void function2() { variable = 123; } 

main.cpp:

 #include "header.hpp" #include "file1.hpp" #include "file2.hpp" #include <iostream> auto main() -> int { function1(); function2(); std::cout << variable << std::endl; } 

由于其内部连接,每个转换单元(包括header.hpp)都将获得该变量的唯一副本。 共有三个翻译单元:

  1. file1.cpp
  2. file2.cpp
  3. main.cpp

调用function1时,file1.cpp变量的副本获取值10。调用function2时,file2.cpp变量的副本获取值123。但是,在main.cpp中返回的值不会更改,并保持等于42。

匿名命名空间

在C ++中,还有另一种方法来声明一个或多个内部链接的字符:匿名名称空间。 这样的空间可确保声明在其中的字符仅在当前翻译单元中可见。 本质上,这只是声明多个静态字符的一种方法。 有一阵子,为了支持匿名名称空间,放弃了使用static关键字声明内部链接的字符。 但是,由于使用内部通信声明一个变量或函数的方便性,他们再次开始使用它。 还有其他一些细微的区别,我将不再赘述。

无论如何,这是:

 namespace { int variable = 0; } 

(几乎)做与以下事情相同的事情:

 static int variable = 0; 

使用方法

那么在什么情况下要使用内部连接呢? 将它们用于对象不是一个好主意。 由于每个翻译单元都进行复制,因此大型对象的内存消耗可能会非常高。 但基本上,它只会引起奇怪的,不可预测的行为。 假设您有一个单例(在一个类中,您仅创建一个实例的实例),然后突然出现了几个“单例”实例(每个翻译单元一个)。

但是,可以使用内部通信将翻译单元隐藏在本地助手功能的全局区域中。 假设您在file1.cpp中使用的file1.hpp中有一个foo helper函数。 同时,在file2.cpp中使用的file2.hpp中具有foo函数。 第一个和第二个foo互不相同,但是您不能使用其他名称。 因此,您可以将它们声明为静态。 如果您没有将file1.hpp和file2.hpp都添加到同一转换单元中,则这会将foo彼此隐藏。 如果不这样做,则它们将隐式具有外部连接,并且第一个foo的定义将遇到第二个foo的定义,从而导致链接器错误,违反了一个定义的规则。

结束

您随时可以在这里留下您的评论和/或问题,或者在开放日访问我们

Source: https://habr.com/ru/post/zh-CN432834/


All Articles