通常很难区分范围和链接以及它们所扮演的角色。本文重点介绍范围和链接,以及如何在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
static int animals = 8; const int i = 5; int call_me(void) { printf("%d %d", i, animals); } 上面的代码在标识符
animals
上实现了静态链接。考虑Feed.cpp
位于同一翻译单元中。Feed.cpp
// C code to illustrate Internal Linkage #include
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
#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
代码的翻译单位(动物,饲料,洗涤)。
这使我们得出结论,每个翻译单位都访问它自己的animals
副本。这就是为什么我们有animals
= 8Animals.cpp
,animals
= 2Feed.cpp
和animals
= 10Wash.cpp
。一份文件。此行为会消耗内存并降低性能。内部链接的另一个属性是,仅当变量具有全局作用域时才实现内部链接,并且所有常量默认情况下都是内部链接的。
用法:众所周知,内部链接变量是通过副本传递的。因此,如果头文件具有
fun1()
函数,并且头文件所包含的源代码也具有fun1()
但具有不同的定义,则这两个函数将不会相互冲突。因此,我们通常使用内部链接从全局范围隐藏翻译单元本地助手功能。例如,我们可能在一个文件中包含一个头文件,该头文件包含一种从用户读取输入的方法,该文件可以描述另一种从用户读取输入的方法。当链接时,这两个功能彼此独立。 - 外部链接:实现翻译的标识符对每个翻译单元都是可见的。外部链接的标识符在翻译单元之间共享,并且被认为位于程序的最外层。实际上,这意味着您必须在所有人都可见的位置定义一个标识符,这样该标识符只有一个可见的定义。它是全局范围内的变量和函数的默认链接。因此,具有外部链接的特定标识符的所有实例都引用程序中的相同标识符。关键字
extern
实现外部链接。当我们使用关键字
extern
,我们告诉链接器在其他地方查找定义。因此,外部链接标识符的声明不会占用任何空间。Extern
标识符通常存储在RAM的初始化/未初始化或文本段中。在继续以下示例之前,请务必通过C中的理解extern关键字进行。
可以在局部范围内使用extern
变量。这将进一步概述链接和范围之间的差异。考虑以下代码:// C code to illustrate External Linkage #include
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
在foo()
范围内声明时,它会在bar()
看到b
时抱怨。编译器将停止编译,并且程序不会传递给链接器。我们可以通过将
b
声明为全局变量,将第1行移至foo
的定义之前来修复程序。让我们看另一个例子
// C code to illustrate External Linkage #include
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
我们可以通过观察外部链接的行为来解释输出。我们在全局范围内定义2个变量
x
和z
。默认情况下,它们两个都具有外部链接。现在,当我们将y
声明为extern
,我们告诉编译器在同一个转换单元中存在一个带有某些定义的y
。请注意,这是在编译时阶段,在该阶段编译器信任extern
关键字并编译程序的其余部分。下一行extern int z
对z
没有影响,因为在我们将z
声明为程序外部的全局变量时,默认情况下z
是外部链接的。当我们遇到printf
行时,编译器会看到3个变量,所有3个变量都已在前面声明过,并且所有3个变量都在它们的范围内使用(在printf
函数)。这样,即使编译器不知道y
的定义,程序也可以成功编译。下一阶段是链接。链接器遍历已编译的代码,并首先找到
x
和z
。由于它们是全局变量,因此默认情况下它们是外部链接的。然后,链接程序将整个转换单元中x
和z
值更新为10和5。如果转换单元中任何其他文件中有对x
和z
引用,则将它们设置为10和5。现在,链接器进入
extern int y
并尝试在翻译单元中找到y
任何定义。它遍历翻译单元中的每个文件以查找y
定义。如果找不到任何定义,则将引发链接器错误。在我们的程序中,我们在main()
之外给出了定义,该定义已为我们编译。因此,链接器找到该定义并更新y
。想要从精选的最佳视频中学习和练习问题,请查看《基础知识到高级C的C基础课程》。