AC功能声明背景
在C语言中,函数声明无法像其他语言那样工作:C编译器本身不会在文件中前后搜索以从调用位置查找函数的声明,并且它不会扫描文件。多次找出两者之间的关系:编译器仅在文件中从上到下向前扫描一次。将函数调用连接到函数声明是链接器工作的一部分,并且仅在将文件编译为原始汇编指令之后才能完成。
这意味着在编译器向前浏览文件时,编译器第一次遇到函数名称时,必须是以下两种情况之一:它要么看到函数声明本身,要么在这种情况下编译器知道确切地说,函数是什么,将其用作参数的类型以及返回的类型—或者是对函数的调用,编译器必须猜测函数最终将如何声明。
(还有第三个选项,其中名称在函数原型中使用,但是我们现在将其忽略,因为如果您首先看到此问题,则可能未使用原型。)
历史课
在C语言的早期,编译器不得不猜测类型并不是一个问题:所有类型或多或少都是相同的-几乎所有东西都是int或指针,它们都是一样的大小。(实际上,在C之前的语言B中,根本没有类型;所有内容都只是一个int或指针,并且其类型完全取决于您的使用方式!)因此,编译器可以安全地猜测任何类型的行为。函数仅基于传递的参数数量:如果传递了两个参数,则编译器会将两件事压入调用堆栈,并且可能被调用者声明了两个参数,并且所有参数都会对齐。如果您仅传递了一个参数,但该函数希望传递两个参数,则该参数仍然可以正常工作,而第二个参数将被忽略/垃圾处理。如果您传递了三个参数,而函数希望传递两个参数,则它仍然可以正常工作,并且第三个参数将被函数的局部变量忽略并踩踏。(一些旧的C代码仍然期望这些不匹配的参数规则也将起作用。)
但是让编译器让您将任何东西传递给任何东西并不是设计编程语言的好方法。它在早期很有效,因为早期的C程序员大多是向导,他们知道不要将错误的类型传递给函数,即使他们确实输入了错误的类型,总有类似的工具lint
可以进行更深入的双重检查。的C代码,并警告您类似的事情。
快进到今天,我们不在同一条船上。C已经长大了,很多人都在其中进行编程,这些人不是向导,为了适应它们(并适应经常使用的其他所有人lint
),编译器已经采用了以前属于Windows的许多功能。lint
-特别是他们检查您的代码以确保其类型安全的部分。早期的C编译器允许您编写代码int foo = "hello";
,并且只是巧妙地将指针分配给整数,并且要确保您没有做任何愚蠢的事情。当您输入错误的类型时,现代C编译器会大声抱怨,这是一件好事。
类型冲突
那么,这与函数声明中的神秘冲突类型错误有什么关系呢?正如我前面所说,C编译器还必须要么知道或者猜到什么名字的意思是他们第一次看到这个名字,因为他们扫描整个文件转发:他们可以知道这意味着什么,如果它是一个真正的函数声明本身(或函数“原型”,稍后会对此进行详细介绍),但如果只是对函数的调用,则必须猜测。而且,可悲的是,这种猜测常常是错误的。
当编译器看到您对的调用时do_something()
,它查看了如何调用它,并得出结论,do_something()
最终将这样声明:
int do_something(char arg1[], char arg2[])
{
...
}
为什么得出结论呢?因为那是你的称呼!(一些C编译器可能会得出结论,这两者int do_something(int arg1, int arg2)
或简直就是int do_something(...)
两者都离您想要的更远,但是重要的一点是,无论编译器如何猜测类型,它对它们的猜测都与您实际函数的用法不同。 )
稍后,当编译器向前扫描文件时,它将看到您的实际声明char *do_something(char *, char *)
。该函数声明甚至与编译器猜测的声明都不接近,这意味着编译器编译该调用的行被错误地编译,并且该程序将无法正常工作。因此,它正确地打印出一个错误,告诉您您的代码无法按编写的方式工作。
您可能想知道,“为什么要假设我要归还int
?” 好吧,它假定该类型是因为没有相反的信息: printf()
可以在其可变参数中接受任何类型,因此,如果没有更好的答案,它int
就会像任何类型一样好。(许多早期的C编译器始终假定int
每个未指定的类型,并假定您要...
为每个声明的函数指定参数,f()
而不是void
—这就是为什么许多现代代码标准建议始终void
在确实不应该包含任何参数的情况下使用该参数)
修复
对于函数声明错误,有两个常见的修复程序。
第一个解决方案,这是由这里的许多其他的答案建议,是把一个原型的源代码上述其中函数首先被调用的地方。原型看起来像函数的声明一样,但是它有一个分号,主体应该在其中:
char *do_something(char *dest, const char *src);
通过将原型放在首位,编译器便知道该函数最终将是什么样子,因此不必猜测。按照惯例,程序员通常将原型放在文件的顶部,位于#include
语句的下面,以确保始终在将它们潜在使用之前就对其进行定义。
其他的解决方案,这在一些真实世界的代码也显示出来,是简单地重新排序功能,使该函数的声明总是前任何呼叫他们!您可以将整个char *do_something(char *dest, const char *src) { ... }
函数移到对其的第一个调用上方,然后编译器将确切知道该函数的外观,而不必猜测。
实际上,大多数人都使用函数原型,因为您还可以获取函数原型并将其移到标头(.h
)文件中,以便其他.c
文件中的代码可以调用这些函数。但是,任何一种解决方案都行得通,许多代码库都使用这两种解决方案。
C99和C11
值得注意的是,这些规则在C版本的较新版本中略有不同。在早期版本(C89和K&R)中,编译器实际上会在函数调用时猜测类型(而K&R时代的编译器在输入错误时通常甚至不会警告您)。C99和C11都要求函数声明/原型必须在第一个调用之前,如果不是这样,则会出错。但是许多现代C编译器(主要是为了与早期代码向后兼容)只会警告缺少的原型,而不会将其视为错误。