C++中的静态成员函数是否会在多个翻译单元中复制?

11

我在我的程序中有一个帮助类,它具有许多静态函数,这些函数在程序的不同类中使用。例如:

helper.h


Class helper {
 public: 
   static void fn1 () 
   { /* defined in header itself */ }

   /* fn2 defined in src file helper.cpp */
   static void fn2(); 
}

Helper类仅有静态成员函数。因此,其他模块不会创建Helper类的对象。帮助函数在其他模块中使用,如:

A.cpp

#include "helper.h"
A::foo() {
  helper::fn1(); 
  helper::fn2();
}

B.cpp

#include "helper.h"
B::foo() {
  helper::fn1();
  helper::fn2(); 
}

编译器会在A.cppB.cpp中创建 helper 函数的独立副本吗?我看了一些早期的帖子,并从回复中得出是会这样做的结论。但当我从A.cppB.cpp分别打印fn1fn2的地址,例如:printf("Address of fn1 is %p\n", &helper::fn1);printf("Address of fn1 is %p\n", &helper::fn1);,我得到的地址是相同的。现在我感到困惑了。是否有人可以澄清一下,我是否漏掉了什么。

我担心 helper 函数会存在多个副本(如果发生的话),因为我们正在尝试减少可执行文件的大小并对其进行优化。

3个回答

13

在类体内定义的函数会被隐式标记为inline。如果您获取该函数的地址,编译器也会创建一个常规副本(每个编译单元),但链接器将只选择其中一个副本包含在可执行文件中,因此只有一个地址。

然而,内联过程可能会使该函数产生许多副本,甚至比编译单元的数量还要多。通常,通过消除参数传递和函数调用开销,以及消除公共子表达式等优化机会,可以抵消代码复制的增加大小。虽然内联通常被认为是大小和速度之间的一种权衡,但大小的增加通常是可忽略的甚至是负面的。

在类中仅声明并在单个编译单元中实现的函数,在可执行文件中肯定只有一个副本。


+1. 我应该更仔细地阅读问题 ;) 删除了我的帖子。 - Mahesh
@snkrish:这是一个模板静态成员函数还是一个模板静态全局函数?如果它是一个静态成员函数,你可能会为每个模板参数组合得到一个新的副本,但编译单元的数量仍然不会影响它。如果是一个静态全局(或命名空间)函数,在每个编译单元中都会得到一个新的副本。 - Ben Voigt
感谢详细的回答,现在我明白为什么地址是相同的了。我有一个跟进问题,不确定是否应该开启单独的帖子。如果上面的 fn2 使用模板进行编写如:template <typename T> static void fn2(); - fn2 应该在头文件中定义还是应该使用 extern template <typename T> helper::fn2() { /* 在 helper.cpp 中定义 */ } 来优化可执行文件大小。当 fn2 被不同的 T 调用时,无论它如何被定义,编译器是否会创建一个函数的副本? - cppcoder
模板真的需要放在头文件中。如果你将它们放入单个编译单元中,编译器就不知道使用哪些T值来特化模板,然后你会得到链接器错误。编译器将为每个不同的T生成一个fn2<T>,而导致编译器执行此操作的编译单元数量并不重要,因为链接器只会保留一个(针对每个T)。 - Ben Voigt
这不是一个糟糕的设计吗?我认为最好使用命名空间来分组这些函数。 - LearningMath
没有足够的信息来确定。无论如何,这个问题是关于成员函数的。 - Ben Voigt

2
如果可见(例如在类声明中定义),则许多编译器会隐式声明其为内联函数。
如果被内联,那么它可能会在某些情况下被复制,在某些情况下被内联,在其他情况下被部分内联。
它遵循“一个定义规则”(ODR),在多个翻译中找到的副本将在链接时被删除(除非您启用了私有外部内联,否则您可能真的会得到冗余的导出实现)。
如果您来自C语言:在这种情况下,static并不会创建函数的唯一副本,它只是意味着您可以调用声明它的类的函数而不需要该类的实例。

谢谢。我使用静态方法来调用fn1和fn2,而不需要创建一个辅助对象。 - cppcoder
@srikrish 我明白你的意思。我之所以提到这一点,是因为C程序员习惯于将声明为静态的函数影响链接(当未内联到每个已知用途时会产生私有副本--与问题相关)。这是他们经常感到困惑的一个普遍原因。正因为如此,而且由于我不知道你的背景...我加了最后一个细节。 - justin

0

内联静态类方法与内联自由函数并没有太大的区别。理论上,ODR规则意味着只有一个函数实例,但在实践中,编译器可能总是将其内联,因此实际上不存在函数实例。

然而,获取函数地址的行为将强制编译器创建函数实例,编译系统的问题是执行ODR并确保始终获得相同的地址。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接