当定义写在源代码的末尾时,为什么需要用C语言对数据和函数进行“声明”?


15

考虑下面的“ C”代码:

#include<stdio.h>
main()
{   
  printf("func:%d",Func_i());   
}

Func_i()
{
  int i=3;
  return i;
}

Func_i()是在源代码的末尾定义的,并且在main()。中使用之前没有提供声明。在编译器Func_i()进入的时候main(),它出来main()并找到了Func_i()。编译器会以某种方式找到返回的值Func_i()并将其提供给printf()。我也知道,编译器无法找到返回类型Func_i()。它在默认情况下需要(猜测?)的返回类型Func_i()int。就是说,如果代码具有float Func_i(),编译器将给出错误:的类型冲突Func_i()

从上面的讨论中我们看到:

  1. 编译器可以找到由返回的值Func_i()

    • 如果编译器可以找到返回的值Func_i()由未来的出来main()和向下搜索源代码,那么为什么就不能找到Func_i(),它的类型明确提及。
  2. 编译器必须知道它Func_i()是float类型的-这就是为什么它会产生冲突类型的错误的原因。

  • 如果编译器知道该Func_i类型为float类型,那么为什么仍假定它Func_i()为int类型,并给出类型冲突的错误?为什么不强制使它Func_i()成为float类型。

我对变量声明也有同样的疑问。考虑下面的“ C”代码:

#include<stdio.h>
main()
{
  /* [extern int Data_i;]--omitted the declaration */
  printf("func:%d and Var:%d",Func_i(),Data_i);
}

 Func_i()
{
  int i=3;
  return i;
}
int Data_i=4;

编译器给出错误:未声明“ Data_i”(此函数中的首次使用)。

  • 当编译器看到 Func_i(),它会进入源代码以查找Func_()返回的值。为什么编译器不能对变量Data_i做同样的事情?

编辑:

我不知道编译器,汇编器,处理器等的内部工作的详细信息。我的问题的基本思想是,使用后,如果我在源代码中最后告诉(写)函数的返回值,然后,使用“ C”语言可以使计算机找到该值而不会出现任何错误。现在为什么计算机无法类似地找到类型。为什么无法找到Data_i的类型,因为找到了Func_i()的返回值。即使我使用extern data-type identifier;语句,也不会告诉该标识符(函数/变量)要返回的值。如果计算机可以找到该值,那么为什么找不到该类型。为什么我们完全需要前向声明?

谢谢。


7
编译器不会“查找” Func_i返回的值。这是在执行时完成的。
James McLeod 2014年

26
我没有投票,但是这个问题是基于对编译器如何工作的严重误解,您在注释中的回答表明您仍然需要克服一些概念上的障碍。
James McLeod 2014年

4
请注意,在过去的15年中,第一个示例代码不是有效的,符合标准的代码;C99在函数定义中没有返回类型,并且隐式声明了Func_iinvalid。从来没有规则隐式声明未定义的变量,因此第二个片段总是格式错误。(是的,编译器仍然接受第一个示例,因为它在C89 / C90下很有效,如果草率的话。)
Jonathan Leffler 2014年

19
@ user31782:问题的底线:为什么语言X执行/要求Y? 因为那是设计师做出的选择。您似乎在争论,有史以来最成功的语言之一的设计师应该在数十年前做出不同的选择,而不是试图根据做出的选择来理解这些选择。您的问题的答案:为什么我们需要向前声明?已经给出:因为C使用一个单遍编译器。对于大多数后续问题,最简单的答案是:因为那样的话,它将不是单遍编译器。
Mindor先生2014年

4
@ user31782您确实非常想读龙的书,以了解编译器和处理器的实际工作原理-不可能将所有必需的知识提取到一个SO答案中(甚至是100个)。对于任何对编译器感兴趣的人来说都是一本好书。
Voo

Answers:


26

因为C是单通静态类型弱类型编译语言。

  1. 单次通过表示编译器不希望看到函数或变量的定义。由于编译器不会向前看,因此必须在使用该函数之前声明一个函数,否则编译器将不知道其类型签名是什么。但是,该函数的定义可以稍后放在同一文件中,甚至可以放在另一个文件中。参见第4点。

    唯一的例外是历史工件,假定未声明的函数和变量的类型为“ int”。现代实践是通过始终显式声明函数和变量来避免隐式类型化。

  2. 静态类型意味着所有类型信息都是在编译时计算的。然后,该信息将用于生成在运行时执行的机器代码。在运行时键入C中没有概念。一旦为int,则始终为int,一旦为float,则始终为float。然而,这一事实在接下来的几点中被模糊了。

  3. 弱类型意味着C编译器自动生成代码以在数字类型之间进行转换,而无需程序员明确指定转换操作。由于是静态类型,因此每次通过程序始终将以相同的方式执行相同的转换。如果将浮点值转换为代码中给定位置的int值,则浮点值将始终转换为代码中该位置的int值。在运行时无法更改。值本身可以从一个程序的执行更改为下一个执行,当然,条件语句可能会更改代码的哪个部分以什么顺序运行,但是没有功能调用或条件的给定单个代码部分将始终执行完全相同的操作。每次运行时都执行相同的操作。

  4. 编译是指在程序运行之前完全执行了分析人类可读源代码并将其转换为机器可读指令的过程。当编译器编译函数时,它不知道在给定的源文件中将进一步遇到什么。但是,一旦编译(和汇编,链接等)完成,完成的可执行文件中的每个函数都包含指向其在运行时将要调用的函数的数字指针。这就是为什么main()可以在源文件中进一步调用函数的原因。在main()实际运行时,它将包含一个指向Func_i()地址的指针。

    机器码非常非常具体。用于添加两个整数(3 + 2)的代码与用于添加两个浮点数(3.0 + 2.0)的代码不同。它们都不同于将int添加到float(3 + 2.0)中,依此类推。编译器为函数中的每个点确定在该点需要执行的精确运算,并生成执行该精确运算的代码。一旦完成,就不能在不重新编译功能的情况下对其进行更改。

将所有这些概念放在一起,之所以main()无法进一步“查看”以确定Func_i()的类型的原因是类型分析发生在编译过程的最开始。此时,仅读取和分析了源文件中直到main()定义为止的部分,并且编译器尚未知道Func_i()的定义。

main()可以“看到” Func_i()调用的原因是,调用在运行时发生,在编译已解析所有标识符的所有名称和类型之后,程序集已经转换了所有函数到机器代码的位置,链接已经在每个调用函数的位置插入了每个函数的正确地址。

当然,我遗漏了大多数细节。实际过程要复杂得多。希望我已提供足够的高层次概述来回答您的问题。

另外,请记住,我上面写的内容专门适用于C。

在其他语言中,编译器可能会多次遍历源代码,因此编译器可以选择Func_i()的定义而无需预先声明。

在其他语言中,可以动态键入函数和/或变量,因此可以保存单个变量,或者可以在不同时间传递或返回单个函数,整数,浮点数,字符串,数组或对象。

在其他语言中,键入可能会更强,需要明确指定从浮点到整数的转换。在其他语言中,键入可能会比较弱,从而允许自动执行从字符串“ 3.0”到浮点数3.0到整数3的转换。

而在其他语言中,代码可以一次解释一行,或者编译为字节码,然后进行解释,或者即时编译,或者通过多种其他执行方案进行处理。


1
谢谢您的综合回答。您和妮可的答案是我想知道的。例如Func_()+1:在编译时,编译器必须知道的类型,Func_i()以便生成适当的机器代码。也许,要么是没有可能的组件来处理Func_()+1通过调用运行时类型,或者是可能的,但这样做会使得程序缓慢的运行时间。我认为,这对我来说已经足够了。
user106313 2014年

1
C的隐式声明函数的重要细节:假定它们为int func(...)... 类型,即,它们采用可变参数列表。这意味着,如果您将函数定义为,int putc(char)但是却忘记声明它,它将被称为int putc(int)(因为通过可变参量列表传递的char被提升为int)。因此,尽管OP的示例因其签名与隐式声明匹配而起作用,但为什么不鼓励使用此行为(并添加适当的警告)也是可以理解的。
uliwitness

37

C语言的设计约束是它应该由单遍编译器进行编译,这使其非常适合内存受限的系统。因此,编译器在任何时候都只知道前面提到的内容。编译器无法在源代码中向前跳过以找到函数声明,然后返回以编译对该函数的调用。因此,所有符号都应在使用前声明。您可以预先声明类似

int Func_i();

在顶部或头文件中以帮助编译器。

在示例中,使用了C语言的两个可疑功能,应避免使用:

  1. 如果在正确声明之前使用了函数,则将其用作“隐式声明”。编译器使用立即上下文找出函数签名。编译器将不会扫描其余的代码来找出真正的声明是什么。

  2. 如果声明的内容没有类型,则该类型为int。例如,对于静态变量或函数返回类型就是这种情况。

因此,在其中printf("func:%d",Func_i()),我们有一个隐式声明int Func_i()。当编译器到达函数定义时Func_i() { ... },它与类型兼容。但是,如果您float Func_i() { ... }在此时进行编写,则将具有隐式声明int Func_i()和显式声明float Func_i()。由于两个声明不匹配,编译器会给您一个错误。

消除一些误解

  • 编译器找不到由返回的值Func_i。没有显式类型意味着int默认情况下返回类型是。即使您这样做:

    Func_i() {
        float f = 42.3;
        return f;
    }

    然后类型将是int Func_i(),返回值将被静默截断!

  • 编译器最终了解的实类型Func_i,但在隐式声明期间不知道实类型。只有在稍后到达真实声明时,它才能发现隐式声明的类型是否正确。但是到那时,用于函数调用的程序集可能已经编写好了,并且不能在C编译模型中进行更改。


3
@ user31782:代码的顺序在编译时很重要,但在运行时不重要。程序运行时,编译器无法显示图片。在运行时,该函数将被组装和链接,其地址将被解析并停留在调用的地址占位符中。(比这要复杂一些,但这是基本思想。)处理器可以向前或向后分支。
Blrfl 2014年

20
@ user31782:编译器不会打印该值。您的编译器无法运行该程序!
与莫妮卡(Monica)进行的轻度比赛

1
@LightnessRacesinOrbit我知道。我在上面的评论中错误地编写了编译器,因为我忘记了名称处理器
user106313 2014年

3
@Carcigenicate C受B语言的影响很大,B语言只有一种类型:字宽整数数字类型,也可以用于指针。C最初复制了这种行为,但是自C99标准以来,它已被完全取缔。Unit从类型理论的角度来看,它是一个很好的默认类型,但是在接近B和C所设计的金属系统编程的实用性方面却失败了。
阿蒙2014年

2
@ user31782:编译器必须知道变量的类型,以便为处理器生成正确的程序集。当编译器找到隐式时Func_i(),它会立即生成并保存代码,以使处理器跳转到另一个位置,然后接收一些整数,然后继续。以后,当编译器找到Func_i定义时,它将确保签名匹配,如果匹配,则将的程序集Func_i()放在该地址,并告诉它返回一些整数。当您运行该程序时,处理器将遵循这些指令并带有值3
Mooing Duck

10

首先,您的程序对C90标准有效,但对随后的程序无效。隐式int(允许声明一个函数而不给出其返回类型),以及隐式声明函数(允许使用一个函数而无需声明它)不再有效。

其次,这不符合您的想法。

  1. 结果类型在C90中是可选的,没有给出一个均值 int。变量声明也是如此(但您必须提供一个存储类staticextern)。

  2. Func_i没有预先声明的情况下调用时,编译器的工作是假设存在一个声明

    extern int Func_i();

    在代码中无需进一步了解效果如何 Func_i声明的。如果Func_i未声明或定义,则编译器在编译时不会更改其行为main。隐式声明仅用于函数,而对于变量则没有。

    请注意,声明中的空参数列表并不意味着该函数不接受参数(您需要为此指定(void)),这意味着编译器不必检查参数的类型,并且将保持不变。隐式转换,应用于传递给可变参数的参数。


如果编译器可以通过从main()中出来并搜索源代码来找到Func_i()返回的值,那么为什么编译器无法找到显式提到的Func_i()的类型。
user106313 2014年

1
@ user31782如果以前没有Func_i声明,则当在调用表达式中看到使用Func_i时,其行为就好像有一个func_i一样extern int Func_i()。它在任何地方都看不到。
AProgrammer 2014年

1
@ user31782,编译器不会在任何地方跳转。它将发出代码以调用该函数;返回的值将在运行时确定。好吧,在同一个编译单元中存在如此简单的函数的情况下,优化阶段可以内联该函数,但是考虑语言规则时,您不必考虑这一点,它是一种优化。
AProgrammer 2014年

10
@ user31782,您对程序的工作方式有严重的误解。太严重了,我认为p.se并不是纠正它们的好地方(也许是聊天,但我不会尝试这样做)。
AProgrammer 2014年

1
@ user31782:编写一个小片段并进行编译-S(如果使用的话gcc)将使您能够查看由编译器生成的汇编代码。然后,您可以了解在运行时如何处理返回值(通常使用处理器寄存器或程序堆栈上的某些空间)。
Giorgio 2014年

7

您在评论中写道:

逐行执行。查找Func_i()返回的值的唯一方法是跳出main

这是一个误解:执行不是逐行进行的。编译是逐行完成的,名称解析是在编译过程中完成的,它仅解析名称,而不返回值。

一个有用的概念模型是:编译器读取以下行时:

  printf("func:%d",Func_i());

它发出的代码等效于:

  1. call "function #2" and put the return value on the stack
  2. put the constant string "func:%d" on the stack
  3. call "function #1"

编译器还会在一些内部表中进行注释,该表function #2是一个尚未声明的函数,名为Func_i,该接受未指定数量的参数并返回int(默认值)。

稍后,当它解析此内容时:

 int Func_i() { ...

编译器查找 Func_i在上述表格中并检查参数和返回类型是否匹配。如果没有,则停止并显示错误消息。如果这样做,它将当前地址添加到内部功能表中,并继续到下一行。

因此,编译器没有“寻找” Func_i在解析第一个引用时。它只是在某个表中做一个记录,就继续解析下一行。在文件的末尾,它具有一个目标文件和一个跳转地址列表。

稍后,链接器将所有这些都接受,并使用实际的跳转地址替换所有指向“功能#2”的指针,因此它发出类似以下内容的信息:

  call 0x0001215 and put the result on the stack
  put constant ... on the stack
  call ...
...
[at offset 0x0001215 in the file, compiled result of Func_i]:
  put 3 on the stack
  return top of the stack

以后,运行可执行文件时,跳转地址已经解析,计算机可以跳转到地址0x1215。无需名称查找。

免责声明:正如我所说,这是一个概念模型,现实世界更加复杂。如今,编译器和链接器进行各种疯狂的优化。他们甚至可能 “跳下来”寻找Func_i,尽管我对此表示怀疑。但是C语言的定义方式是,您可以像这样编写一个超简单的编译器。因此,在大多数情况下,这是一个非常有用的模型。


谢谢您的回答。编译器无法发出代码:1. call "function #2", put the return-type onto the stack and put the return value on the stack?
user106313 2014年

1
(续)另外:如果编写printf(..., Func_i()+1);了该怎么办-编译器必须知道的类型Func_i,因此它可以决定应发出add integer还是add float指令。您可能会发现一些特殊情况,编译器可以在没有类型信息的情况下继续工作,但是编译器必须在所有情况下都可以工作。
nikie 2014年

4
@ user31782:通常,机器指令非常简单:添加两个32位整数寄存器。将存储器地址加载到16位整数寄存器。跳转到一个地址。另外,没有类型:您可以将表示32位浮点数的内存位置愉快地加载到32位整数寄存器中,并对其进行一些算术运算。(这几乎没有道理。)因此,您不能直接发出这样的机器代码。您可以编写一个编译器来执行所有这些操作,包括运行时检查和堆栈上的其他类型数据。但这不是C编译器。
nikie 2014年

1
@ user31782:取决于IIRC。float值可以存在于FPU寄存器中-则根本没有指令。编译器仅跟踪在编译期间哪个值存储在哪个寄存器中,并发出诸如“将常数1添加到FP寄存器X”之类的信息。如果没有可用的寄存器,它也可以存在于堆栈中。然后将出现“将堆栈指针增加4”指令,并且将值“引用”为类似于“堆栈指针-4”的内容。但是,只有在编译时就知道堆栈上所有变量(前后)的大小时,所有这些事情才起作用。
nikie 2014年

1
从所有讨论中,我已经达到了这种理解:为了使编译器为任何包含Func_i()or / and的语句编写合理的汇编代码Data_i,它必须确定其类型;汇编语言无法调用数据类型。我需要自己详细研究事情,才能放心。
user106313 2014年

5

C和其他需要声明的语言是在处理器时间和内存昂贵的时代设计的。C和Unix的发展相伴相伴了相当长的时间,而后者直到1979年3BSD出现时才拥有虚拟内存。如果没有额外的工作空间,编译器将倾向于单通道事务,因为它们没有需要能够一次将整个文件的某些表示形式保留在内存中。

像我们一样,单遍编译器也无能为力。这意味着他们唯一可以肯定知道的事情是在编译代码行之前已明确告知他们。对我们两个人来说,Func_i()在源文件中稍后声明都是很,但是一次只能处理一小段代码的编译器并不知道它即将到来。

在早期的C语言(AT&T,K&R,C89)中,foo()在声明之前使用函数会导致事实或隐式声明int foo()。您的示例在Func_i()声明时有效,int因为它与代表您声明的编译器匹配。将其更改为任何其他类型将导致冲突,因为它不再与编译器在没有显式声明的情况下选择的内容匹配。此行为已在C99中删除,在C99中,使用未声明的函数成为错误。

那么返回类型呢?

在大多数环境中,目标代码的调用约定只需要知道被调用函数的地址即可,这对于编译器和链接器来说相对容易处理。执行跳转到函数的开头,并在返回时返回。其他任何事情,尤其是传递参数和返回值的安排,都完全由调用方和被调用方在称为a的安排中确定。调用约定。只要它们共享相同的约定集,程序就可以调用其他目标文件中的函数,而不论它们是用共享那些约定的任何语言编译的。(在科学计算中,您会遇到很多C调用FORTRAN的情况,反之亦然,而执行此操作的能力则来自于调用约定。)

早期C语言的另一个特点是,我们现在所知道的原型不存在。您可以声明函数的返回类型(例如,int foo()),但不能声明其参数(即int foo(int bar)不是选项)。之所以存在该问题,是因为如上所述,该程序始终遵循可以由参数确定的调用约定。如果您使用错误的参数类型调用了函数,那将是无用尽,无用尽的情况。

因为目标代码具有返回但没有返回类型的概念,所以编译器必须知道返回类型才能处理返回的值。当您运行机器指令时,它们只是位而已,处理器并不在乎您要比较的内存中是否double实际上有一个int。它只是按照您的要求进行操作,如果您破坏了它,则您将拥有这两个部分。

考虑以下代码位:

double foo();         double foo();
double x;             int x;
x = foo();            x = foo();

左侧的代码向下编译为一个调用,foo()然后将通过调用/返回约定提供的结果复制到x存储的任何位置。那是简单的情况。

右边的代码显示了类型转换,这就是为什么编译器需要知道函数的返回类型的原因。浮点数不能转储到其他代码希望看到的内存中,int因为不会发生魔术转换。如果最终结果必须是整数,则必须有指令来指导处理器在存储之前进行转换。如果不foo()提前知道返回类型,编译器将不知道转换代码是必需的。

多遍编译器可实现各种功能,其中之一是在首次使用变量,函数和方法后就可以声明它们。这意味着,当编译器开始编译代码时,它已经看到了未来并知道该怎么做。例如,由于Java的语法允许在使用后声明,因此Java要求多次通过。


感谢您的回答(+1)。我不知道编译器,汇编器,处理器等内部工作的详细信息。我的问题的基本思想是,如果我在使用后最后在源代码中告诉(写)了函数的返回值,然后,该语言允许计算机找到该值而不会给出任何错误。现在为什么计算机无法类似地找到类型。为什么找不到Data_i 的类型,因为找到了Func_i()返回值。
user106313 2014年

我仍然不满意。double foo(); int x; x = foo();简单地给出错误。我知道我们做不到。我的问题是,在函数调用中,处理器仅找到返回值。为什么也不能找到返回类型呢?
user106313 2014年

1
@ user31782:不应该。有一个的原型foo(),因此编译器知道该如何处理。
Blrfl 2014年

2
@ user31782:处理器没有任何返回类型的概念。
Blrfl 2014年

1
@ user31782有关编译时的问题:可以编写一种语言,在编译时可以使用所有这种类型的分析。C不是这样的语言。C编译器无法执行此操作,因为它并非旨在执行此操作。可以设计不同吗?当然可以,但是这样做需要更多的处理能力和内存。底线是不是。它以当今计算机最能处理的方式进行设计。
Mindor先生2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.