C语言 为什么实现文件中的自由函数在默认情况下没有内部链接?

bhmjp9jg  于 2023-10-16  发布在  其他
关注(0)|答案(6)|浏览(110)

当涉及到函数(C++中的非成员函数)时,将它们标记为 static 会给它们内部链接。这意味着它们在翻译单元之外不可见。为什么这不是默认值?我没有一个好的统计数据,但从我所看到的实现文件中的大多数函数应该被标记为静态的。
我相信大家的共识是把功能分成更小的单元。因此,一般来说,在实现文件中不应该在其他翻译单元中可见的类似“实用程序”的函数的数量大于只是公共接口实现的函数的数量是有道理的。
在这种情况下,他们默认使用“导出所有内容”的原因是什么?

laik7k3q

laik7k3q1#

在C/C编译模型中,预处理器在所有其他操作之前运行,并将#include s替换为它们的内容。
因此,在.cpp文件中定义的函数和在它包含的头文件中定义的函数之间没有区别。
你的建议会让函数默认定义在头文件static中(这将删除“多重定义”链接错误),这将是非常糟糕的,因为如果你忘记了inline(在C
中),或者如果你不知道你不应该在头文件中定义函数(在C中),它会在结果二进制文件中导致无声的代码重复。

cgyqldqp

cgyqldqp2#

  1. C零件
    C语言现在是一种非常古老的语言(从20世纪70年代开始),并且非常保守。包含文件只是意味着在源代码级别被包含。C11的草案n1570明确指出:
    一个源文件连同所有通过预处理指令#include包含的头文件和源文件一起被称为预处理翻译单元。在预处理之后,预处理翻译单元被称为翻译单元。
    这意味着一个符合标准的C编译器不会对来自包含文件和源文件的内容进行任何区分,因为包含发生在编译阶段之前。
    这足以让函数默认接收外部链接(不声明为 static)。
  2. C部分
    尽管是一种完全不同的语言,但C
    仍然继承了C。具体来说,C标准库仍然是C标准库的一部分。
    这可能足以让非成员函数在默认情况下接受与C中相同的处理。当然,这在C语言中远没有那么重要,因为C函数实际上被声明为extern C。但另一方面,非成员函数也被称为命名空间作用域函数是有原因的。在C
    中,作用域是处理命名空间污染的正确方法。
    我的观点是,最佳实践应该推荐范围的一切。您只需使用命名作用域来获得外部链接,使用匿名作用域来将作用域限制在本地单元。这足以不需要更改非成员函数的C默认值。
t0ybt7op

t0ybt7op3#

你很难找出 * 为什么 * 默认值是“导出所有内容”。自20世纪70年代成立以来,该语言及其编译器都发生了巨大的变化,当时互联网上没有发行说明或“工作组”讨论。“结构化编程”和goto语句是当时的主流;很少有人考虑使用封装来最小化共享状态复杂性问题。Fortran还使函数公开可见。
我怀疑随着语言的流行,出现了更大的系统,这些系统可能破坏了链接器的早期版本。因此,需要引入一些规避这一点的手段。出于某种疯狂的原因,他们选择使用static来隐藏链接器的函数,以减少其负载(* 对我来说,这是一个更大的谜团,而不是为什么链接是任意公开的 *)。
实际上,在声明函数static时,除了拒绝其他模块访问“内部”之外,在非常大的程序中隐藏链接器的符号是值得的,以加快构建时间并减少内存消耗。这可能会很快变得笨拙。与其在代码库中散布static来隐藏方法,实际上更有意义的是使用编译器选项将隐藏的可见性设置为默认值,然后装饰您希望对其他模块可见的函数。
在Linux中,你可以命令编译器将隐藏可见性设为默认值(-fvisibility=hidden):参见https://stackoverflow.com/a/52742992/1607937
事实上,它比这要复杂一点;存在提供可视性的更精细调整的其它选项。关于https://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Function-Attributes.html
visibility(“visibility_type”)ELF目标上的visibility属性导致声明以默认、隐藏、受保护或内部可见性发出。

void __attribute__ ((visibility ("protected")))
          f () { /* Do something. */; }
          int i __attribute__ ((visibility ("hidden")));

请参阅ELF gABI了解完整的细节,但简短的故事是:

默认

默认可见性是ELF的正常情况。此值可用于可见性属性,以覆盖可能更改符号的假定可见性的其他选项。
隐藏
隐藏可见性表示符号不会被放置到动态符号表中,因此其他模块(可执行文件或共享库)无法直接引用它。

内部

内部可见性类似于隐藏可见性,但具有额外的特定于处理器的语义。除非psABI另有规定,否则GCC定义内部可见性意味着函数永远不会从另一个模块调用。请注意,隐藏符号虽然不能被其他模块直接引用,但可以通过函数指针间接引用。
通过指示不能从模块外部调用符号,GCC可以例如省略PIC寄存器的加载,因为已知调用函数加载了正确的值。

保护

受保护的可见性指示符号将被放置在动态符号表中,但定义模块内的引用将绑定到本地符号。也就是说,该符号不能被另一个模块覆盖。
并非所有ELF目标都支持此属性。
(* 也可以看到彼得·科德斯在线程中的评论 *)
还要注意的是,函数可以被可以链接进来的“螺栓连接”实现覆盖。这对于单元测试中的模拟方法很有用。如果您打算使用这个属性,那么值得使用“weaklinkage”属性。
值得一提的是,在C++中,最好使用匿名命名空间而不是static来声明符号为“私有”:

namespace {
    <module-private code>
} // anonymous namespace

参见核心准则SF.22 -https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rs-unnamed2

  • 根据我的经验,许多公司在他们的编码标准中接受这一点。

注意,它并不完全等同于“静态”:
静态命名空间和匿名命名空间不是一回事。在匿名命名空间中定义的函数将具有外部链接。但它保证存在于唯一命名的作用域中。事实上,我们不能在定义它的翻译单元之外引用它,因为它是未命名的。
.所以对于非常大的C++程序,仍然值得使用-fvisibility=hidden,并装饰你 * 确实 * 希望对链接器可见的方法,即使使用匿名命名空间。

e0bqpujr

e0bqpujr4#

在全局命名空间范围内声明了static关键字的函数将具有本地链接。这意味着,
a)如果它们在.cpp文件中声明,则无法从任何其他编译单元(其他.cpp文件)访问它们。
B)如果它们在头文件中声明,则在包含该头文件的每个编译单元中都会有每个函数的副本。
c)如果它们是在模块中声明的,则不能从其他任何地方访问它们。
为什么语言是这样设计的?这是最初的决定,无论是在C和C++。在C语言中头文件是次要的,是可选的.你可以链接一个没有头文件的程序。在C中,你需要在使用前在源代码中声明函数的原型。在C中,你甚至不需要这个。
C
使用相同的策略。你可以说它遵循了最小惊喜原则。这些函数默认具有本地链接,并且需要“extern”关键字(或“export”,或其他一些令人讨厌的扩展),这是意料之外的。在C++中,匿名命名空间的作用与“默认本地链接”最接近。

pinkon5k

pinkon5k5#

编译器没有看到“源文件中有定义,头文件中没有声明”。它所看到的只是“翻译单位中的定义”。在你的方案下,你需要给你打算在多个翻译单元中使用的每个函数给予外部链接。
默认值对于C来说很有意义,因为C中只有自由函数,并且在C++中保留了向后兼容性。

vuv7lop3

vuv7lop36#

如果希望让程序员声明两种稍微不同的构造形式,使用语法标记来区分“替代”形式和主要形式,那么至少有两种明智的方法可以决定哪种形式应该是主要形式:
1.如果一种形式比另一种形式使用得更多,请将其作为主要形式。
1.如果一种语言没有一种形式就没有用,但没有另一种形式至少在某种程度上是可用的,那么就把第一种形式作为主要形式。
如果一个人试图最大限度地减少将编译器“引导”到新平台上所需的工作量,那么他应该设法忽略那些对于启动和运行最小编译器来说不是绝对必要的东西。如果编译器生成的代码需要与任何其他已经启动并运行的代码进行交互,那么绝对需要对外部链接的支持。对内部链接的支持可能很好,但远没有那么必要。

相关问题