通常很难区分范围和联系以及它们所扮演的角色。本文重点介绍作用域和链接,以及它们在 C 语言中的使用方式。
注意:所有 C 程序都在 64 位 GCC 4.9.2 上编译。此外,术语“标识符”和“名称”在本文中可以互换使用。
定义
- 范围:标识符的范围是程序中可以直接访问标识符的部分。在 C 中,所有标识符都是词法(或静态)范围的。
- 链接:链接描述了名称如何在整个程序或单个翻译单元中引用或不能引用同一实体。
以上听起来和Scope很像,但事实并非如此。为了理解上面的意思,让我们深入挖掘编译过程。 - 翻译单元:翻译单元是包含源代码、头文件和其他依赖项的文件。所有这些源都组合在一个文件中,因为它们用于生成一个单独的可执行对象。以有意义的方式将源链接在一起很重要。例如,编译器应该知道
printf
定义位于stdio
头文件中。
在 C 和 C++ 中,由多个源代码文件组成的程序一次编译一个。在编译过程之前,一个变量可以通过它的作用域来描述。只有当链接过程开始时,链接属性才会发挥作用。因此,作用域是编译器处理的属性,而链接是链接器处理的属性。
链接器在编译过程的链接阶段将资源链接在一起。链接器是一个程序,它以多个机器代码文件作为输入,并产生一个可执行的目标代码。它解析符号(即,获取诸如“+”等符号的定义)并在地址空间中排列对象。
链接是描述链接器应如何链接变量的属性。变量是否应该可供另一个文件使用?变量应该只在声明的文件中使用吗?两者都是由联动决定的。
因此,链接允许您在每个文件的基础上将名称耦合在一起,范围确定这些名称的可见性。
有两种类型的链接:
- 内部链接:实现内部链接的标识符在声明它的翻译单元之外不可访问。单元内的任何标识符都可以访问具有内部链接的标识符。它由关键字实现
static
。内部链接标识符存储在 RAM 的已初始化或未初始化段中。(注意:static
也有一个关于范围的含义,但这里不讨论)。
一些例子:
Animals.cpp
// C code to illustrate Internal Linkage
#include <stdio.h>
static int animals = 8;
const int i = 5;
int call_me(void)
{
printf("%d %d", i, animals);
}
上面的代码在 identifier 上实现了静态链接animals
。考虑Feed.cpp
位于同一翻译单元中。
Feed.cpp
// C code to illustrate Internal Linkage
#include <stdio.h>
int main()
{
call_me();
animals = 2;
printf("%d", animals);
return 0;
}
先编译 Animals.cpp 然后再编译 Feed.cpp,我们得到
Output : 5 8 2
现在,考虑 Feed.cpp 位于不同的翻译单元中。只有当我们使用#include "Animals.cpp"
.
考虑位于第三个翻译单元的 Wash.cpp。
Wash.cpp
// C code to illustrate Internal Linkage
#include <stdio.h>
#include "animal.cpp" // note that animal is included.
int main()
{
call_me();
printf("\n having fun washing!");
animals = 10;
printf("%d\n", animals);
return 0;
}
在编译时,我们得到:
Output : 5 8 having fun washing! 10
- 有 3 个翻译单元(Animals、Feed、Wash)正在使用
animals
代码。
这使我们得出结论,每个翻译单元都访问它自己的animals
. 这就是为什么我们有animals
= 8Animals.cpp
,animals
= 2Feed.cpp
和animals
= 10 的原因Wash.cpp
。一份文件。这种行为会消耗内存并降低性能。内部链接的另一个属性是它仅在变量具有全局范围时才实现,并且默认情况下所有常量都是内部链接的。用法:众所周知,内部链接的变量是通过副本传递的。因此,如果头文件有一个函数,
fun1()
并且包含它的源代码也有,fun1()
但定义不同,那么这两个函数不会相互冲突。因此,我们通常使用内部链接将翻译单元本地辅助函数隐藏在全局范围内。例如,我们可能会在一个文件中包含一个头文件,该文件包含从用户读取输入的方法,该文件可能描述从用户读取输入的另一种方法。这两个功能在链接时相互独立。 - 外部链接:实现外部链接的标识符对每个翻译单元都是可见的。外部链接标识符在翻译单元之间共享,并被认为位于程序的最外层。实际上,这意味着您必须在对所有人可见的地方定义一个标识符,以便它只有一个可见的定义。它是全局范围的变量和函数的默认链接。因此,具有外部链接的特定标识符的所有实例都引用程序中的相同标识符。关键字
extern
实现外部链接。当我们使用关键字时extern
,我们告诉链接器在别处寻找定义。因此,外部链接标识符的声明不占用任何空间。Extern
标识符通常存储在 RAM 的已初始化/未初始化或文本段中。请务必通过 在继续以下示例之前,请先了解 C 中的 extern 关键字。
可以extern
在局部范围内使用变量。这将进一步概述链接和范围之间的差异。考虑以下代码:
// C code to illustrate External Linkage
#include <stdio.h>
void foo()
{
int a;
extern int b; // line 1
}
void bar()
{
int c;
c = b; // error
}
int main()
{
foo();
bar();
}
Error: 'b' was not declared in this scope
解释 :该变量b
在函数中具有局部范围foo
,即使它是一个extern
变量。请注意,编译发生在链接之前;即范围是一个只能在编译阶段使用的概念。程序编译后就没有“变量范围”这样的概念了。
在编译期间,b
会考虑范围。它在foo()
. 当编译器看到extern
声明时,它相信b
某处有一个定义,并让链接器处理其余部分。
但是,同一个编译器会遍历bar()
函数并尝试查找变量b
。由于b
已声明extern
,编译器尚未为其分配内存;它还不存在。编译器会让链接器b
在翻译单元中找到定义,然后链接器将分配b
定义中指定的值。只有这样b
才会存在并被分配内存。但是,由于在编译时没有在 范围内bar()
,甚至在全局范围内给出声明,编译器会报出上述错误。
鉴于编译器的工作是确保所有变量都在其范围内使用,它会在看到b
inbar()
时抱怨,当b
在foo()
‘ 的范围内声明时。编译器将停止编译并且程序不会被传递给链接器。
我们可以b
通过将第 1 行移动到 foo
的定义前来声明为全局变量来修复程序。
让我们看另一个例子
// C code to illustrate External Linkage
#include <stdio.h>
int x = 10;
int z = 5;
int main()
{
extern int y; // line 2
extern int z;
printf("%d %d %d", x, y, z);
}
int y = 2;
Output: 10 2 5
我们可以通过观察外部链接的行为来解释输出。我们定义两个变量x
,并z
在全球范围内。默认情况下,它们都具有外部链接。现在,当我们声明y
as 时extern
,我们告诉编译器y
在同一个翻译单元中存在一个带有某些定义的a 。请注意,这是在编译时阶段,编译器信任extern
关键字并编译程序的其余部分。下一行extern int z
对 没有影响z
,因为z
当我们将它声明为程序外部的全局变量时,默认情况下它是外部链接的。当我们遇到printf
line 时,编译器会看到 3 个变量,这 3 个变量之前都已声明过,并且所有 3 个变量都在它们的范围内使用(在printf
功能)。因此程序编译成功,即使编译器不知道y
下一阶段是链接。链接器遍历编译后的代码x
并z
首先找到和。由于它们是全局变量,默认情况下它们是外部链接的。然后,链接器将整个翻译单元的值x
和z
整个翻译单元的值更新为 10 和 5。如果在翻译单元中的任何其他文件中有任何引用x
和引用z
,则将它们设置为 10 和 5。
现在,链接器开始extern int y
尝试y
在翻译单元中找到任何定义。它查看翻译单元中的每个文件以查找y
. 如果它没有找到任何定义,则会抛出链接器错误。在我们的程序中,我们给出了 outside 的定义main()
,它已经为我们编译好了。因此,链接器找到该定义并更新y
。