我正在使用以下代码:
char dest[5]; char src[5] = "test"; printf("String: %s\n", do_something(dest, src)); char *do_something(char *dest, const char *src) { return dest; }
这里的实施do_something
并不重要.当我尝试编译上面的内容时,我得到以下两个异常:
错误:'do_something'的冲突类型(在printf调用时)
错误:'do_something'的先前隐式声明在这里(在原型行)
为什么?
您在声明之前尝试调用do_something.您需要在printf行之前添加函数原型:
char* do_something(char*, const char*);
或者您需要将函数定义移动到printf行上方.在声明之前不能使用函数.
在"经典"C语言(C89/90)中,当您调用未声明的函数时,C假定它返回一个int
并且还尝试从实际参数的类型派生其参数的类型(不,它不假设它没有参数,正如之前有人建议的那样).
在您的具体示例中,编译器将查看do_something(dest, src)
调用并隐式派生声明do_something
.后者看起来如下
int do_something(char *, char *)
但是,稍后在代码中您明确声明do_something
为
char *do_something(char *, const char *)
如您所见,这些声明彼此不同.这是编译器不喜欢的.
在使用它之前,您没有声明它.
你需要类似的东西
char *do_something(char *, const char *);
在printf之前.
例:
#includechar *do_something(char *, const char *); char dest[5]; char src[5] = "test"; int main () { printf("String: %s\n", do_something(dest, src)); return 0; } char *do_something(char *dest, const char *src) { return dest; }
或者,您可以将整个do_something
函数放在printf之前.
您必须在使用它之前声明该函数.如果函数名称在声明之前出现,C编译器将遵循某些规则并自行声明.如果错了,你会得到那个错误.
您有两种选择:(1)在使用它之前定义它,或(2)使用前向声明而不实现.例如:
char *do_something(char *dest, const char *src);
注意最后的分号.
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编译器(主要是为了与早期代码向后兼容)只会警告缺少的原型,而不会将其视为错误。