为什么在C语言中获得“函数的冲突类型”?


76

我正在使用以下代码:

这里的实现do_something并不重要。当我尝试编译上面的代码时,出现以下两个异常:

错误:“ do_something”的类型冲突(在printf调用中)
错误:“ do_something”的先前隐式声明在此处(原型行)

为什么?


1
我遇到了同样的错误,但是我没有在任何地方调用该函数(在定义之前或之后),因此所选答案不适用于我的情况。原来问题是命名冲突-我的函数名称是“ mergesort”(我将与Skiena的“算法设计手册”一起使用),并且stdlib.h中已经有一个同名的函数。将我的函数重命名为“ merge_sort”可以解决问题。
Richie Thomas

Answers:


131

您试图在声明之前调用do_something。您需要在printf行之前添加一个函数原型:

或者您需要将函数定义移到printf行上方。在声明函数之前,不能使用它。


26

在“经典” C语言(C89 / 90)中,当您调用未声明的函数时,C假定它返回an,int并且还尝试从实际参数的类型中派生其参数的类型(不,它不假定正如之前有人建议的那样,它没有参数)。

在您的特定示例中,编译器将查看do_something(dest, src)call并隐式派生的声明do_something。后者看起来如下

但是,在后面的代码中,您显式声明do_something

如您所见,这些声明彼此不同。这是编译器不喜欢的。


在某些情况下,您可以合法地调用未声明的函数?对于此问题,出现“冲突类型”错误而不是“未在调用之前声明函数”错误的原因是什么?
intcreator's

2
@brandaemon:原始C语言(C89 / 90)没有“在调用之前未声明函数”错误。在C中调用未声明的函数是完全合法的。代替发出错误,需要使用语言来分析调用并“推断”该函数的隐式声明。这就是最初设计语言的方式。因此,是的,在C89 / 90中,您可以合法地调用未声明的函数,只要“推断的”函数声明与实际的声明兼容即可
AnT

2
@brandaemon:C99禁止调用未声明的函数。这意味着从C99开始,C中出现“未在调用之前声明函数”错误。但是,即使在C99(及更高版本)中,也可以声明没有原型的函数。即足以int foo()使函数foo正式声明。在C中(相对于C ++)(),函数声明仍然意味着未指定函数参数列表,并且必须由编译器完全如上所述来“推导”。即C编译器不再推论函数的返回类型,但仍可以推导参数列表。
AnT

好吧,我想我明白了。那么,这种特殊的编译器错误是故意留给类型的,还是可能被改写为单词(可能已经在某些编译器中了?我刚刚在gcc上这样看过),以便在特殊情况下出现函数声明问题?我主要是想看看如何更好地解释此特定错误,因为至少在这种情况下,它看起来不是很直观。
intcreator

1
@brandaemon:原始帖子中的编译器错误实际上是指函数类型。它说推导的函数类型是,int (char *, char *)实际的函数类型是char *(char *, const char *)。但是,在这种情况下违反的一般规则适用于所有类型的声明(不仅是“推导”的声明,不仅是函数声明)。例如,您可以先声明extern double a;然后定义,然后int a;您将得到关于冲突类型的相同错误。而且,显然,GCC的作者认为功能不应该分开对待。
AnT

9

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()最终将这样声明:

为什么得出结论呢?因为那是你的称呼!(一些C编译器可能会得出结论,这两者int do_something(int arg1, int arg2)或简直就是int do_something(...)两者都您想要的更远,但是重要的一点是,无论编译器如何猜测类型,它对它们的猜测都与您实际函数的用法不同。 )

稍后,当编译器向前扫描文件时,它将看到您的实际声明char *do_something(char *, char *)。该函数声明甚至与编译器猜测的声明都不接近,这意味着编译器编译该调用的行被错误地编译,并且该程序将无法正常工作。因此,它正确地打印出一个错误,告诉您您的代码无法按编写的方式工作。

您可能想知道,“为什么要假设我要归还int?” 好吧,它假定该类型是因为没有相反的信息: printf()可以在其可变参数中接受任何类型,因此,如果没有更好的答案,它int就会像任何类型一样好。(许多早期的C编译器始终假定int每个未指定的类型,并假定您要...为每个声明的函数指定参数,f()而不是void—这就是为什么许多现代代码标准建议始终void在确实不应该包含任何参数的情况下使用该参数)

修复

对于函数声明错误,有两个常见的修复程序。

第一个解决方案,这是由这里的许多其他的答案建议,是把一个原型的源代码上述其中函数首先被调用的地方。原型看起来像函数的声明一样,但是它有一个分号,主体应该在其中:

通过将原型放在首位,编译器便知道该函数最终将是什么样子,因此不必猜测。按照惯例,程序员通常将原型放在文件的顶部,位于#include语句的下面,以确保始终在将它们潜在使用之前就对其进行定义。

其他的解决方案,这在一些真实世界的代码也显示出来,是简单地重新排序功能,使该函数的声明总是任何呼叫他们!您可以将整个char *do_something(char *dest, const char *src) { ... }函数移到对其的第一个调用上方,然后编译器将确切知道该函数的外观,而不必猜测。

实际上,大多数人都使用函数原型,因为您还可以获取函数原型并将其移到标头(.h)文件中,以便其他.c文件中的代码可以调用这些函数。但是,任何一种解决方案都行得通,许多代码库都使用这两种解决方案。

C99和C11

值得注意的是,这些规则在C版本的较新版本中略有不同。在早期版本(C89和K&R)中,编译器实际上在函数调用时猜测类型(而K&R时代的编译器在输入错误时通常甚至不会警告您)。C99和C11都要求函数声明/原型必须在第一个调用之前,如果不是这样,则会出错。但是许多现代C编译器(主要是为了与早期代码向后兼容)只会警告缺少的原型,而不会将其视为错误。


谢谢您详尽周到的解答
Tycholiz '20

8

您在使用它之前没有声明它。

你需要类似的东西

在printf之前。

例:

或者,您可以将整个do_something功能放在printf之前。


6

使用该函数之前,必须先声明该函数。如果函数名称出现在声明之前,则C编译器将遵循某些规则并自行声明。如果不正确,您将得到该错误。

您有两个选择:(1)在使用它之前定义它,或(2)在不实现的情况下使用前向声明。例如:

注意最后的分号。



3

如果您在使用函数之前不提供该函数的原型,则C会假定它接受任意数量的参数并返回int。因此,当您首次尝试使用do_something时,这就是编译器正在寻找的函数类型。这样做会产生关于“隐式函数声明”的警告。

因此,在您的情况下,当您稍后确实声明函数时,C不允许函数重载,因此它会惹恼您,因为您已经为它声明了两个具有不同原型但名称相同的函数。

简短的答案:在尝试使用该函数之前,先对其进行声明。


3
不,它假定它接受任意数量的参数并返回一个int值。在C语言中,int foo()(采用任意数量的参数)与int foo(void)(不采用任何参数)之间存在很大差异。
亚当·罗森菲尔德

感谢您的澄清!更新:)
mrduclaw

3

再次观看:

专注于这一行:

您可以清楚地看到未声明do_something函数!

如果再往前看

您将看到使用该函数对其进行了声明。

您将需要使用以下代码修改此部分:

干杯;)


1

当您修改交流函数定义而忘记更新相应的标头定义时,通常会发生这种情况。


我发生了类似的事情。我正在针对Khronos cl.h头文件为openCL编写绑定。我正在从在线文档中复制和粘贴,以为函数原型/签名创建typedef。原来OpenCL 1.0的clGetContextInfo文档不正确。最后一个参数应为“ size_t *”,但应为“ size_t”。
twitchdotcom

0

确保首先声明函数声明中的类型。

/* start of the header file */



struct intr_frame{...}; //must be first!



void kill (struct intr_frame *);



/* end of the header file */

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.