我们怎么能说 "int i" 是定义也是声明

How can we say that "int i" is definition as well as a declaration

这些是 Dennis Ritchie C 编程书中的直线:

Declaration refers to the place the nature of the variable is stated but no storage is allocated. Definition refers to the place the variable is created or assigned storage.


问题

"variable is created" 和 "variable is stated" 到底有什么区别?

下面程序中的变量j只是声明的,没有定义,怎么没有编译错误呢,因为我们是把j的地址赋值给I, 变量 j 还没有被分配存储空间(在编译期间)?

int main()
{
    int *I, j;
    I = &j; 
    printf("%lu", I);
}

output: 140721741346812 

int *I, j是声明还是定义?我得到的是,这是一个定义。但在 this link 中,它既是声明又是定义。

我对 link 和 Ritchie 的书中给出的两个截然不同的概念感到困惑。

请说明我哪里错了?是不是赋值被推迟到运行-时间,并且在运行-时间变量j被分配为垃圾值,因此没有错误?

声明说明变量的类型(这也有助于确定需要多少 space)。

一个定义说明了变量的类型,也为变量分配了space。

所以是的,每个定义都是一个声明。

这是一个声明:

extern int j;

在这个声明之后,编译器知道 jint 类型。在这个声明之后,我们可以这样说

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 的实际定义。

你说,

here in the above program variable j just declared, not defined,

没有,在你的程序中定义了j

int *i, j is declaration or definition?

这是两个变量的定义。它定义 int 类型的 jint * 类型的 i 或指向 int.

的指针

看起来你已经开始使用指针了,当你使用指针时,你必须更仔细地考虑内存分配。 (您可能还需要考虑静态和动态分配之间的区别。)

首先要了解的是编译器完全有能力分配内存。编译器一直在为变量分配内存。只有当你开始使用指针时,你才开始担心内存分配,并自己进行内存分配。

正如我们所说,行

int *i;

是一个定义。它声明类型 space 并分配 space 给一个名为 i 的变量,类型为指向 int.

的变量

但在这种情况下,我们分配space的是指针。我们没有为指针指向分配任何space。事实上,这个指针还没有指向任何地方:它是未初始化的。

如果我们说

int j;
int *i = &j;

现在指针 i 确实指向某处:它指向变量 j。现在,我们不仅为指针i分配了内存,还为其指向分配了内存。

为指向的指针分配内存的另一种方法是进行动态内存分配,通常是通过调用 malloc。如果我们说

int *i = malloc(sizeof(int));

我们再次为 i 及其指向的内容分配了内存。我们让编译器为i分配内存,我们调用malloc动态分配内存给i指向

一些图片可能会有所帮助。这是第一段代码后的情况:

   +-----------+
j: |           |
   +-----------+
         ^
         |
   +-----|-----+
i: |     *     |
   +-----------+

这两个盒子都是编译器为我们分配的。

这是第二段代码后的情况:

   +-----------+         +-----------+
i: |     *-------------> |           |
   +-----------+         +-----------+

左边的框,i,是编译器给我们分配的,右边的未命名框是我们从malloc.[=74=得到的]

声明呢?什么

extern int j;

长什么样子?我想我会这样画:

j:

有一个符号(一个名字)j,但它旁边还没有盒子(尽管如果有一个盒子,它的大小和形状将适合容纳 int).


在您提出的评论中,

given int j; int *i=&j; we are not knowing the memory location of j, so how can we allocate its address to i?

我不确定你所说的 "we are not knowing the memory location of j" 是什么意思。我们可能不知道地址,但编译器知道。编译器会尽一切努力确保在已知位置为 j 分配 space。 &j 的字面意思是 "fetch the known location of the variable j"。自从

int j;

是一个定义,表示j会被分配space在一个已知的位置。


在所有这些中,我一直在说 "the compiler allocates memory for..." 之类的话,但实际上它比那要复杂一点。 actual 分配 actual 内存可能稍后发生。

对于局部变量,编译"allocates"变量在栈上。它实际上(通常)所做的是在函数的堆栈框架中预留 space 。从某种意义上说,局部变量的 "address" 是它相对于堆栈帧基址的偏移量。直到程序 运行ning,函数被调用,它的栈帧在栈上的特定地址被创建,变量的实际地址才会被知道。

对于全局变量,编译器会分配我们所谓的暂定定义。 C 设置为单独编译,这意味着编译器可以在多个源文件上多次调用,创建多个目标文件,这些文件稍后由单独的[=187=编译在一起] 程序转换为单个可执行文件。编译器在单个目标文件的数据段中为每个定义的全局变量提供一个暂定地址。 (实际上它可能比这更复杂,如果编译器发出汇编语言,它由一个单独的汇编器组装。在这种情况下,编译器发出指令导致汇编器执行这些试探性分配。)

当链接器将几个目标文件组合在一起时,它也必须将它们的部分暂定数据段组合在一起。此过程涉及为全局变量的 most/all 提供新地址。编译器已注意发出称为 "relocation information" 的额外信息,不仅描述了每个全局符号的定义,还描述了代码中引用该符号的每个位置。这样,当链接器将全局符号移动到它们的最终位置时,它可以返回并调整所有引用——所有对全局函数的调用,以及全局变量的地址用 & 占用的所有位置。 (换句话说,是的,链接器正在返回并修改编译器 and/or 汇编程序生成的机器代码。)

在此方案中,外部声明导致目标文件中的信息略有不同。编译器发出自己版本的外部声明,而不是试探性分配(带有试探性地址,链接器通常稍后会调整它,以及可能需要重新定位的引用列表),告诉链接器它需要在其他一些目标文件中找到该符号的实际定义。 (它本质上是说该符号的暂定地址为 0,以后总是会对其进行调整。)但是编译器会发出相同类型的重定位信息,因此当(如果)链接器在另一个目标文件中找到该符号时,它可以在所有引用它的地方填写该地址。

所以如果我们查看编译器为代码发出的低级代码

int *i = &j;

根据 j 是局部变量、全局变量(即已定义的全局变量)还是外部全局变量,我们会找到以下三种情况之一。

  1. 对于局部变量,编译器将发出代码以获取堆栈帧的基地址(无论最终在 运行-time 是什么),加上偏移量变量 j(编译器知道),并将结果地址存储到 i.

  2. 对于在同一源文件中定义的全局变量,编译器将发出代码以获取 j 的暂定地址并将其存储到 i 中。但它也会发出重定位记录,指向 j 的地址存储到 i 的指令,以便链接器可以将地址调整为 j 的实际最终地址地址.

  3. 对于外部全局变量,编译器将发出代码以获取未知地址(通常是暂定地址'0')并将其存储到i。它会发出一个重定位记录,指向 j 的地址存储到 i 的指令,以便链接器可以用 j 的实际地址填充它。

(同样,在所有这些中,我一直指的是 堆栈框架 ,C 不需要,某些体系结构上的 C 程序不一定使用。但它们是思考会发生什么的好方法,您的机器可能会使用它们。)