声明说明变量的类型(这也有助于确定它需要多少空间)。
定义说明变量的类型,并为变量分配空间。
所以是的,每个定义都是一个声明。
这是一个声明:
extern int j;
在这个声明之后,编译器知道j 的类型是int。在这个声明之后,我们可以说类似
printf("%d\n", j);
我们可以这么说,编译器不会抱怨(至少,它不会马上抱怨),因为至少目前,编译器拥有它需要的所有信息。
但是,如果我们尝试编译这个完整的程序:
#include <stdio.h>
extern int j;
int main()
{
printf("%d\n", j);
}
我们可能会遇到错误。在我的编译器上,我得到了错误
Undefined symbols: "_j", referenced from: _main
ld: symbol(s) not found
该错误意味着尽管编译器知道 j 是什么,但最终的构建过程 (ld) 在任何地方都没有找到 j 的实际定义。
你说,
这里在上面的程序变量j刚刚声明,没有定义,
不,在您的程序中定义了j。
int *i, j是声明还是定义?
这是两个变量的定义。它定义了int 类型的j,以及int * 类型的i,或指向int 的指针。
看起来您已经开始使用指针,当您使用指针时,您必须更仔细地考虑内存分配。 (您可能还需要考虑静态分配和动态分配之间的区别。)
首先要了解的是编译器完全有能力分配内存。编译器一直为变量分配内存。只有当您开始使用指针时,您才需要开始担心内存分配,并自己进行内存分配。
正如我们所说,这条线
int *i;
是一个定义。它声明了一个名为i、类型为pointer-to-int的变量的类型并为其分配空间。
但在这种情况下,我们为指针分配了空间。我们还没有为指针指向的对象分配任何空间。事实上,这个指针还没有指向任何地方:它是未初始化的。
如果我们说
int j;
int *i = &j;
现在指针i 确实指向某个地方:它指向变量j。现在,我们不仅为指针i 分配了内存,还为它分配了指向的内存。
为指向的指针分配内存的另一种方法是进行动态内存分配,通常通过调用malloc。如果我们说
int *i = malloc(sizeof(int));
我们再次为i 及其指向的内容分配了内存。我们让编译器为i分配内存,我们调用malloc为i动态分配内存指向。
一些图片可能会有所帮助。这是第一段代码后的情况:
+-----------+
j: | |
+-----------+
^
|
+-----|-----+
i: | * |
+-----------+
这两个盒子都是编译器为我们分配的。
第二段代码后的情况如下:
+-----------+ +-----------+
i: | *-------------> | |
+-----------+ +-----------+
左边的盒子,i,是编译器分配给我们的,右边未命名的盒子是我们从malloc得到的。
声明呢?是什么
extern int j;
看起来像?我想我会这样画:
j:
有一个符号(名称)j,但它旁边还没有盒子(尽管如果有一个盒子,它的大小和形状应该可以容纳 int)。
在您提出的评论中,
给定int j; int *i=&j;,我们不知道j的内存位置,那么如何将其地址分配给i?
我不确定您所说的“我们不知道j 的内存位置”是什么意思。我们可能不知道地址,但编译器知道。编译器会尽一切努力确保在已知位置为j 分配空间。表达式&j 的字面意思是“获取变量j 的已知位置”。因为
int j;
是一个定义,它意味着j将被分配到一个已知位置的空间。
在所有这些中,我一直在说诸如“编译器为...分配内存”之类的东西,但实际上它比这要复杂一些。 实际内存的实际分配可能会在以后发生。
对于局部变量,编译器在堆栈上“分配”变量。它实际上(通常)所做的是在函数的堆栈帧中留出空间。从某种意义上说,局部变量的“地址”是它相对于堆栈帧底部的偏移量。在程序运行之前,变量的实际地址是未知的,函数被调用,并且它的栈帧被创建在堆栈上的特定地址。
对于全局变量,编译器分配我们可能称为暂定定义的内容。 C 被设置为单独编译,这意味着编译器可以在多个源文件上被多次调用,创建多个目标文件,然后由单独的“链接器”程序一起编译成单个可执行。编译器在单个目标文件的数据段中为每个定义的全局变量提供一个暂定地址。 (实际上它可能比这更复杂,如果编译器正在发出汇编语言,而汇编语言是由单独的汇编器汇编的。在这种情况下,编译器发出指令,导致汇编器执行这些暂定分配。)
当链接器将多个目标文件组合在一起时,它也必须将它们的部分暂定数据段组合在一起。此过程涉及为大多数/所有全局变量提供新地址。编译器注意发出额外的信息,称为“重定位信息”,不仅描述每个全局符号的定义,还描述代码中引用该符号的每个位置。这样,当链接器将全局符号移动到它们的最终位置时,它可以返回并调整所有引用——所有对全局函数的调用,以及所有全局变量的地址用& 获取的位置。 (换句话说,是的,链接器正在返回并修改编译器和/或汇编器生成的机器代码。)
在此方案中,外部声明导致目标文件中的信息略有不同。编译器不是临时分配(带有临时地址,通常稍后由链接器调整,以及可能需要重定位的引用列表),编译器发出自己的外部声明版本,告诉链接器它需要在其他一些目标文件中找到这个符号的实际定义。 (它本质上说该符号的暂定地址为 0,以后总会对其进行调整。)但是编译器会发出相同类型的重定位信息,因此当(如果)链接器在另一个目标文件中找到该符号时,它可以在引用它的所有位置填写该地址。
所以如果我们查看编译器为代码发出的低级代码
int *i = &j;
根据j 是本地变量、全局变量(即定义的全局变量)还是外部全局变量,我们会找到三件事之一。
1234563编译器知道),并将结果地址存储到i。
-
对于在同一源文件中定义的全局变量,编译器将发出代码以获取j 的暂定地址并将其存储到i。但它也会发出一条重定位记录,指向将j 的地址存储到i 中的指令,以便链接器可以将地址调整为j 的实际最终地址。
对于外部全局变量,编译器将发出代码以获取未知地址(通常是暂定地址“0”)并将其存储到i。它将发出一条重定位记录,指向将j 的地址存储到i 中的指令,以便链接器可以用j 的实际地址填充它。
(同样,在所有这些中,我一直在提到 堆栈帧,C 不需要,某些架构上的 C 程序也不一定使用。但它们是以体面的方式思考会发生什么,您的机器可能会使用它们。)