我们怎么能说“ int i”既是定义又是声明

时间:2018-07-28 12:48:02

标签: c declaration definition

这些是Dennis Ritchie C编程书中的直接内容:

  

声明是指声明变量性质但未分配存储空间的地方。 定义是指创建或分配变量的位置。


问题

“创建变量”和“声明变量”之间到底有什么区别?

在下面声明的,未定义的程序变量currentUser中,当我们将j的地址值分配给j时,不会出现编译错误,并且变量I尚未分配存储空间(在编译过程中)?

j

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

output: 140721741346812 是声明还是定义?我得到的是一个定义。但是在this link中,它既声明又定义。

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

请说明我要去哪里了?值分配是否推迟到运行时,并且在运行时将变量int *I, j分配为垃圾值,因此没有错误?

1 个答案:

答案 0 :(得分:4)

声明声明了变量的类型(这也有助于确定所需的空间)。

定义说明了变量的类型,还为变量分配了空间。

是的,每个定义都是声明。

这是一个声明:

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是声明还是定义?

这是两个变量的定义。它定义了j类型的inti类型的int *或指向int的指针。

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

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

正如我们所说的,行

int *i;

是一个定义。它声明类型为i的指针int的类型并为其分配空间。

但是在这种情况下,我们为指针分配了空间。我们没有为指针指向的分配任何空间。实际上,该指针尚未指向任何地方:它尚未初始化。

如果我们说

int j;
int *i = &j;

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

分配内存以供指向的指针的另一种方法是执行动态内存分配,通常是通过调用malloc来实现的。如果我们说

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

我们已经为i及其指向的内存分配了内存。我们让编译器为i分配内存,并调用malloci动态分配内存以指向。

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

   +-----------+
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将在已知位置分配空间。


在所有这些中,我一直在说诸如“编译器为...分配内存”之类的事情,但实际上,这要复杂得多。 actual 内存的 actual 分配可能会在以后发生。

对于局部变量,编译器在堆栈上“分配”变量。它实际上(通常)所做的是在函数的堆栈框架中留出空间。从某种意义上说,局部变量的“地址”是其相对于堆栈框架底部的偏移量。直到程序运行并且调用函数并在堆栈上的特定地址处创建变量的堆栈框架后,变量的实际地址才知道。

对于全局变量,编译器分配我们可能称为暂定定义的东西。为单独编译设置了C,这意味着可以在多个源文件上多次调用该编译器,从而创建多个目标文件,然后由一个单独的“链接器”程序将它们一起编译为一个文件可执行文件。编译器在单个目标文件的数据段中为每个定义的全局变量提供一个临时地址。 (实际上,如果编译器发出的汇编语言是由一个单独的汇编器进行汇编的,则可能比这更复杂。在那种情况下,编译器发出的指令将导致汇编器执行这些临时分配。)

当链接器将多个目标文件组合在一起时,它也必须将其部分的暂定数据段组合在一起。此过程涉及为大多数/所有全局变量提供新地址。编译器已谨慎地发出了称为“重定位信息”的额外信息,不仅描述每个全局符号的定义,而且描述代码中引用该符号的每个位置。这样,当链接程序将全局符号移动到其最终位置时,它可以返回并调整所有引用-所有对全局函数的调用,以及所有全局变量的地址用&表示的位置。 (换句话说,是的,链接器将返回并修改由编译器和/或汇编器生成的机器代码。)

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

因此,如果我们查看编译器为该代码发出的低级代码

int *i = &j;

根据j是局部变量,全局变量(即定义的全局变量)还是外部全局变量,我们将找到三件事之一。

  1. 对于局部变量,编译器将发出代码以获取堆栈帧的基地址(无论最终在运行时如何),并向其添加变量j的偏移量(编译器知道),并将结果地址存储到i中。

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

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

(再次,在所有这些中,我一直指的是堆栈框架,C不需要,某些架构上的C程序也不一定要使用。但是它们是考虑发生情况的一种不错的方法,您的计算机可能会使用它们。)