A C 函数声明后台程序
在 C 中,函数声明不像在其他语言中那样工作:C 编译器本身不会在文件中向后和向前搜索以从您调用它的位置找到函数的声明,它不会多次扫描文件以找出关系:编译器仅在文件中向前扫描一次,从上到下。将函数调用连接到函数声明是链接器工作的一部分,并且仅在文件被编译为原始汇编指令之后完成。
这意味着当编译器向前扫描文件时,编译器第一次遇到函数名时,必须满足以下两种情况之一:要么是看到函数声明本身,在这种情况下编译器确切地知道函数是什么,它作为参数的类型以及返回的类型——或者它是对函数的调用,编译器必须猜测函数最终将如何声明。
(还有第三种选择,在函数原型中使用名称,但我们现在将忽略它,因为如果您首先看到这个问题,那么您可能没有使用原型。)
历史课
在 C 的早期,编译器必须猜测类型这一事实并不是真正的问题:所有类型都或多或少相同——几乎所有东西都是 int 或指针,它们的大小相同。 (事实上,在 C 之前的语言 B 中,根本没有类型;一切都只是一个 int 或指针,它的类型完全取决于你如何使用它!)所以编译器可以安全地猜测任何类型的行为仅基于传递的参数数量的函数:如果传递两个参数,编译器会将两个东西压入调用堆栈,并且可能被调用者声明了两个参数,并且所有参数都会对齐。如果您只传递了一个参数但函数需要两个参数,它仍然可以工作,而第二个参数将被忽略/垃圾。如果您传递了三个参数而函数期望两个,它仍然可以工作,并且第三个参数将被函数的局部变量忽略并踩踏。 (一些旧的 C 代码仍然希望这些不匹配的参数规则也能起作用。)
但是让编译器让你将任何东西传递给任何东西并不是设计编程语言的好方法。它在早期运行良好,因为早期的 C 程序员大多是巫师,他们知道不要将错误的类型传递给函数,即使他们确实弄错了类型,也总有像 lint 这样的工具可以做得更深仔细检查你的 C 代码并警告你这些事情。
快进到今天,我们并不完全在同一条船上。 C 已经长大了,很多人都在用它编程,他们不是巫师,为了适应他们(也为了适应其他经常使用lint 的人),编译器已经具备了许多以前的能力以前是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(...),两者都与您想要的相比更远,但重要的一点是,无论编译器如何猜测类型,它猜测它们与你的实际函数使用的不同。)
稍后,当编译器在文件中向前扫描时,它会看到您的 actual 声明 char *do_something(char *, char *)。该函数声明甚至与编译器猜测的声明都不接近,这意味着编译器编译调用的行编译错误,程序无法运行。所以它正确地打印了一个错误,告诉你你的代码不能像写的那样工作。
您可能想知道,“为什么假定我返回的是 int?”好吧,它假定该类型,因为没有相反的信息:printf() 可以在其变量参数中接受 any 类型,因此如果没有更好的答案,int 与任何猜测一样好. (许多早期的 C 编译器总是为每个未指定的类型假定 int,并假定您的意思是 ... 为声明的每个函数的参数 f() — 而不是 void — 这就是为什么许多现代代码标准建议始终使用 @987654338 @ in 作为参数,如果真的不应该有的话。)
修复
函数声明错误有两个常见的修复方法。
这里很多其他答案都推荐的第一个解决方案是在源代码中上面第一次调用函数的地方放置一个原型。原型看起来就像函数的声明,但它的主体应该是分号:
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 编译器(主要是为了向后兼容早期代码)只会警告缺少原型,而不将其视为错误。