最近,我在C中学习了隐式函数声明,其主要思想是明确的,但在这种情况下,我在理解链接过程方面遇到了一些困难。
考虑以下代码(文件a.c):
#include <stdio.h>
int main() {
double someValue = f();
printf("%f\n", someValue);
return 0;
}如果我试图编译它:
gcc -c a.c -std=c99我看到一个关于函数f()的隐式声明的警告。
如果我试图编译和链接:
gcc a.c -std=c99我有一个未定义的引用错误。所以一切都很好。
然后添加另一个文件(B.C文件):
double f(double x) {
return x;
}并调用下一个命令:
gcc a.c b.c -std=c99令人惊讶的是,所有的东西都成功地连接起来了。当然,在./a.out调用之后,我看到了一个垃圾输出。
那么,我的问题是:具有隐式声明函数的程序是如何链接的?在我的例子中,编译器/链接器的外壳下发生了什么?
发布于 2016-01-04 19:15:59
首先,自C99以来,函数的隐式声明将从标准中删除。编译器可能支持这一点,以编译遗留代码,但这不是强制性的。引用标准前言,
尽管如此,根据C11,第6.5.2.2节
如果函数是用不包含原型的类型定义的,且升级后的参数类型与升级后的参数类型不兼容,则行为是未定义的。
所以,就你来说,
int返回类型不匹配,所以代码调用未定义行为。为了添加更多的引用,如果在调用后尝试在同一个编译单元中定义函数,您将得到一个由于不匹配签名而产生的编译错误。
但是,在单独的编译单元中定义函数(以及缺少原型声明),编译器无法检查签名。编译后,链接器获取对象文件,并且由于链接器中没有任何类型检查(也没有对象文件中的任何信息),因此很高兴地链接它们。最后,它将最终成功地编译并链接和 UB。
发布于 2016-01-04 19:52:11
以下是正在发生的事情。
f()的声明,编译器就会假设一个类似于int f(void)的隐式声明。然后愉快地编译a.c。b.c时,编译器对f()没有任何先前的声明,因此它从f()的定义中直观地看出了这一点。通常,您会将f()的一些声明放在头文件中,并将其包括在a.c和b.c中。因为两个文件都会看到相同的声明,所以编译器可以强制执行一致性。它将抱怨与声明不匹配的实体。但在这种情况下,没有通用的原型可供参考。C中,编译器不会在对象文件中存储关于原型的任何信息,链接器也不会执行任何一致性检查(它不能)。它看到的只是a.c中未解决的符号f和b.c中定义的符号f。它愉快地解析符号,并完成链接。a.c中设置调用。它与b.c中的定义不匹配。f() (来自b.c)将从堆栈中获取一个垃圾参数,并将其返回为double,这将被解释为在a.c中返回时的int。发布于 2016-01-04 19:19:00
如何将具有隐式声明功能的方案联系起来?在我的例子中,编译器/链接器的外壳下发生了什么?
从C99开始,C标准就宣布隐式int规则为非法。因此,使用隐式函数声明的程序是无效的。
这是无效的,因为C99。在此之前,如果一个可见的原型不可用,那么编译器将隐式声明一个int返回类型。
令人惊讶的是,所有的东西都成功地连接起来了。当然,在./a.out调用之后,我看到了一个垃圾输出。
因为您没有原型,所以编译器隐式声明了一个用于int类型的f()。但是f()的实际定义返回一个double。这两种类型是不兼容的,这是https://en.wikipedia.org/wiki/Undefined_behavior。
即使在隐式int规则有效的C89/C90中,这也是未定义的,因为隐式原型与f()返回的实际类型不兼容。因此,这个示例(使用a.c和b.c)在所有C标准中都没有定义。
使用隐式函数声明不再有用或有效。因此,编译器/链接器处理方式的实际细节只具有历史意义。它可以追溯到K&R C的预标准时间,它没有功能原型,函数默认返回int。在C89/C90标准中将功能原型添加到C中。总之,对于有效的C程序中的所有函数,您必须有原型(或在使用之前定义函数)。
https://stackoverflow.com/questions/34598082
复制相似问题