AC 函数声明后台程序
在 C 中,函数声明不像在其他语言中那样工作:C 编译器本身不会在文件中向后和向前搜索以从您调用它的位置找到函数的声明,并且它不会扫描文件多次找出关系:编译器只在文件中从上到下准确地向前扫描一次。将函数调用连接到函数声明是链接器工作的一部分,并且仅在文件被编译为原始汇编指令之后才完成。
这意味着当编译器向前扫描文件时,编译器第一次遇到函数的名称时,必须是以下两种情况之一:要么是看到函数声明本身,在这种情况下编译器知道这个函数到底是什么,它接受什么类型作为参数,返回什么类型——或者它是对函数的调用,编译器必须猜测函数最终将如何声明。
(还有第三种选择,在函数原型中使用名称,但我们现在将忽略它,因为如果您首先看到这个问题,那么您可能没有使用原型。)
历史课
在 C 的早期,编译器必须猜测类型这一事实并不是真正的问题:所有类型或多或少都相同——几乎所有东西都是 int 或指针,它们是相同的大小。(事实上,在 C 之前的语言 B 中,根本没有类型;一切都只是一个 int 或指针,其类型完全取决于您如何使用它!)因此编译器可以安全地猜测任何类型的行为仅基于传递的参数数量的函数:如果传递了两个参数,编译器会将两个东西压入调用堆栈,并且可能被调用者声明了两个参数,并且所有参数都会排成一行。如果你只传递了一个参数,但函数需要两个,它仍然可以工作,第二个参数将被忽略/垃圾。如果您传递了三个参数而函数需要两个参数,它仍然可以正常工作,并且第三个参数将被函数的局部变量忽略并踩踏。(一些旧的 C 代码仍然希望这些不匹配的参数规则也能起作用。)
但是让编译器让你将任何东西传递给任何东西并不是设计编程语言的好方法。它在早期运行良好,因为早期的 C 程序员大多是巫师,他们知道不要将错误的类型传递给函数,即使他们确实弄错了类型,也总有类似的工具lint
可以进行更深入的双重检查您的 C 代码并警告您此类事情。
快进到今天,我们并不完全在同一条船上。C 语言已经发展起来,很多人都在用它进行编程,他们不是巫师,为了适应他们(并适应其他经常使用的人lint
),编译器已经具备了许多以前属于 C 语言的能力。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 编译器——主要是为了向后兼容早期代码——只会警告缺少原型而不认为它是错误。