据我所知,调用约定取决于编译器和体系结构。但是 C 和 C++ 调用约定之间有什么明显的区别吗?
5 回答
但是 C 和 C++ 调用约定之间有什么明显的区别吗?
一般来说,没有。C++ 被有意设计为尽可能与 C 兼容,特别是它在所有系统上使用 C 应用程序二进制接口。
但是 C ABI 不能满足 C++ 需要的许多特性(特别是重载、命名空间和函数模板),因此 C++ 编译器对函数的名称进行了一些更改。这称为名称修饰
因此,为了使 C 和 C++ 代码之间的函数调用能够正常工作,必须将此类函数声明为extern "C"
禁用名称修饰并确保调用约定是 C 所期望的(但我希望后一个方面会自动出现,即使标准没有强制要求)。
C++ 还为C 中不存在的成员函数(有时称为thiscall )提供了额外的调用约定。但是自由函数使用与 C 相同的调用约定,无论是在给定系统上的哪一个。
这两个标准都没有要求 C 和 C++ 调用约定在给定编译器上相同,除了声明的 C++ 函数extern "C"
必须使用与 C 函数相同的调用约定进行调用。
这就是为什么具有相同参数和返回类型的函数指针和带有 C 链接的函数指针具有不同类型的原因。当函数被调用时,编译器可以从类型知道调用它的调用约定,如果它们不同。
在实践中,我认为我从来没有故意处理过一个示例,该示例在具有和不具有 C 链接的自由函数之间使用不兼容的调用约定。通常,C++ 实现从它计划运行的系统的 ABI 中采用其调用约定,以便生成系统的其他用户可以根据 ABI 理解的可链接对象(可执行文件和库)。
这不是必需的——标准不关心是否存在系统 ABI,并且系统通常不关心如何在自包含的可执行文件中进行调用 [*]。除非有特殊的理由不这样做,否则这样做是明智的。给定系统上的系统 ABI 可能会或可能不会提及 C++——如果没有,那么 C++ 实现就非 C 链接函数而言是独立的,但正如我所说,制作可以实现的函数通常是明智的让 C 链接使用相同的调用约定,就好像它们确实有 C 链接一样。
我说“不兼容”而不是“不同”,因为当然有些东西在 C 调用约定中没有提到,但需要在 C++ 调用约定中指定。例如,如何传递指向成员函数的指针。很可能这不是由系统 ABI 确定的,因此留给 C++ 实现。这样做的原因是将指向成员函数的指针的实现留给 C++ 实现,而不是系统 ABI 做一些制造商认为是 C++ 编译器编写者的工作的事情。
不妨碍调用约定,请注意名称修饰在具有或不具有 C 链接的自由函数之间不可避免地是不同的。原因是 C++ 名称修改必须包含参数的类型(因为函数重载),而 C 名称修改不能(因为带有未指定参数的外部函数声明)。
[*] 虽然我已经看到使用“错误”调用约定确实会破坏事情的示例。ISTR 是一个 Windows 移动设备,如果您对堆栈的格式与操作系统预期的不同,那么某些硬件异常会取出设备,因为操作系统试图回溯有问题的线程的堆栈,但不能。因此,至少在 2005 年左右的那个操作系统版本上,如果您希望操作系统的诊断能够正常工作,那么您必须在内部使用 Windows 调用约定。或者无论如何,与堆栈帧格式有关的调用约定部分。当然,这完全是我们搞砸了,但我不知道我们是否可以通过安装我们自己的硬件异常处理程序来正确修复它,而不是通过不首先引起异常来解决它们。不过,这确实意味着用户模式进程可能会因堆栈溢出而轻易地关闭操作系统,而且调试我们的代码比在其他 Windows 上更难。所以我们稍微指责了操作系统。
据我所知,调用约定取决于编译器和体系结构。
是的。
但是 C 和 C++ 调用约定之间有什么明显的区别吗?
更好的问题是“有什么相似之处吗?”
是的:
在 C++ 应用程序中声明extern "C"
的函数与该架构上的 C 函数使用的调用约定相同。
您对调用约定所做的任何其他假设都是推测性的,并且可能碰巧是相同的,但您需要根据具体情况来担心这一点。
在语言之间改变调用约定的整个概念超出了任何给定的语言(及其规范)。这就是它应该的样子,这些事情与任何名副其实的语言的规范无关。基本上,您是对的,它属于规范的特定实现领域 - 编译器架构。当然,给定语言的规范/标准中讨论的一些概念(如链接规范)会影响调用约定,但这不是计划中的结果。
ABI 的职责是将这些概念标准化/形式化,然后再一次,人们没有义务尊重这些概念(可能是因为实现的调色板变化很大)
确实,规范需要忽略参数的传递方式,无论是最终在堆栈还是寄存器上,两者的组合,分配顺序等。这是从事实现工作的人和设计人员的工作实际的硬件,更重要的是指令集。
因此:无论是标准化的 C 还是 C++,都对如何实现内容没有任何发言权。因此,语言必须没有固有的差异。仅在它们的应用方式上。这就是编译器架构的领域。
没有 C 或 C++ 标准定义 ABI。这是一个有意的设计决策,它允许编译器编写者自由地创建尽可能高效的实现。
随着时间的推移,称为cdecl的 C 调用约定已成为 x86 架构上的事实标准。在 AMD64 机器上运行的 C 代码有类似的事实标准,但它们在 UNIX 和 Windows 操作系统之间有所不同。
在 Unix 机器上,基于 Intel 发布的 Itanium C++ ABI,有一些朝着通用 C++ ABI 发展。但是,由于 C++ 的额外复杂性,同一编译器的不同版本甚至不同的编译器切换通常会产生符合不兼容 ABI 的机器代码。
通常可以依靠使用extern "C" { ... }
来强制编译器使用事实上的 C ABI 实现函数,但 C++ 标准并不要求这样做。
如果您对当前 C++ ABI 的全面描述感兴趣,那么您应该查看 Agner Fog 的“不同 C++ 编译器和操作系统的调用约定”。