隐式函数声明和链接
最近我在C中学习了隐式函数声明。 主要观点很明确,但在这种情况下我对理解联系过程有些麻烦。
请考虑以下代码(文件ac ):
#include int main() { double someValue = f(); printf("%f\n", someValue); return 0; }
如果我尝试编译它:
gcc -c ac -std=c99
我看到关于函数f()
隐式声明的警告。
如果我尝试编译和链接:
gcc ac -std=c99
我有一个未定义的引用错误。 一切都很好。
然后我添加另一个文件(文件bc ):
double f(double x) { return x; }
并调用下一个命令:
gcc ac bc -std=c99
令人惊讶的是,一切都成功地联 当然在./a.out调用后我看到了垃圾输出。
所以,我的问题是:如何将隐式声明函数的程序链接起来? 在编译器/链接器的引擎下我的例子会发生什么?
我读了很多关于SO的话题, 这个和这个但仍然有问题。
首先,从C99
,函数的隐式声明将从标准中删除。 编译器可能会支持编译遗留代码,但这并不是强制性的。 引用标准前言,
- 删除隐式函数声明
也就是说,根据C11
,章节§6.5.2.2
如果使用不包含原型的类型定义函数,并且促销后的参数类型与促销后的参数类型不兼容,则行为未定义。
所以,在你的情况下,
-
函数调用本身是隐式声明(从C99开始变为非标准),
-
并且由于函数签名不匹配[ 假定函数的隐式声明具有
int
返回类型 ],您的代码将调用未定义的行为 。
只是为了添加更多的引用,如果在调用后尝试在同一个编译单元中定义函数,由于不匹配签名,您将收到编译错误。
但是,您的函数是在单独的编译单元中定义的(并且缺少原型声明),编译器无法检查签名。 编译之后,链接器获取目标文件,并且由于链接器中没有任何类型检查(并且目标文件中也没有信息),所以请愉快地链接它们。 最后,它将成功完成编译和链接以及 UB。
这是正在发生的事情。
- 如果没有
f()
的声明,编译器会假定一个隐式声明,如int f(void)
。 然后愉快地编译ac
。 - 在编译
bc
,编译器没有f()
任何先前声明,因此它从f()
的定义中直接得出它。 通常你会在头文件中放入一些f()
声明,并将它包含在ac
和bc
。 因为两个文件都会看到相同的声明,所以编译器可以强制执行一致性。 它会抱怨与声明不符的实体。 但在这种情况下,没有共同的原型可供参考。 - 在
C
,编译器不会在目标文件中存储有关原型的任何信息,并且链接器不会执行任何一致性检查(它不能)。 它看到的只是ac
未解析的符号f
和bc
定义的符号f
。 它愉快地解析符号,并完成链接。 - 然而事情在运行时会中断,因为编译器根据它在那里假设的原型在
ac
设置调用。 这与bc
的定义不匹配。f()
(来自bc
)将从堆栈中获取一个垃圾参数,并将其返回为double
,在ac
返回时将被解释为int
。
具有隐式声明函数的程序是如何链接的? 在编译器/链接器的引擎下我的例子会发生什么?
自C99起, 隐式int规则已被C标准取缔。 因此,具有隐式函数声明的程序是无效的 。
它自C99起无效。 在此之前,如果可见原型不可用,则编译器隐式声明一个具有int
返回类型的原型。
令人惊讶的是,一切都成功地联 当然在./a.out调用后我看到了垃圾输出。
因为你没有原型,所以编译器隐式声明了一个带有int
类型的f()
。 但f()
的实际定义返回一个double
。 这两种类型是不兼容的,这是未定义的行为 。
即使在C89 / C90中,隐式int规则有效也是未定义的,因为隐式原型与实际类型f()
返回不兼容。 所以这个例子是(带有ac
和bc
)在所有C标准中都是未定义的 。
具有隐式函数声明不再有用或有效。 因此,编译器/链接器处理方式的实际细节仅具有历史意义。 它可以追溯到K&R C的预标准时间,它没有函数原型,函数默认返回int
。 function原型在C89 / C90标准中添加到C中。 最后,您必须拥有有效C程序中所有函数的原型(或在使用前定义函数)。
编译之后,所有类型信息都会丢失(除了可能在调试信息中,但链接器不会注意这一点)。 唯一剩下的就是“在地址0xdeadbeef处有一个名为”f“的符号。
标题的要点是告诉C关于符号的类型,包括,对于函数,它需要什么参数以及它返回什么。 如果您将真实的那些与您声明的那些(显式或隐式)不匹配,则会得到未定义的行为。