内联函数中的静态变量

99
我有一个在头文件中声明和定义的函数。这本身就是一个问题。当该函数未被内联时,每个使用该头文件的翻译单元都会得到该函数的副本,并且当它们被链接在一起时会出现重复。我通过使该函数成为内联函数来“修复”此问题,但我担心这是一个脆弱的解决方案,因为据我所知,即使指定了"inline"关键字,编译器也不能保证进行内联。如果这不是真的,请纠正我。
无论如何,真正的问题是,这个函数内的静态变量会发生什么?我最终会得到多少个副本?
9个回答

126
我猜你漏掉了一些东西。
静态函数?
声明一个函数为静态将使其在编译单元中“隐藏”。
引用范围内的名称如果是以下内容之一,则具有内部链接:
- 显式声明为静态的变量、函数或函数模板; - C++14(n3797)3.5/3
当名称具有内部链接时,它所表示的实体可以被同一翻译单元中其他作用域的名称引用。
C++14(n3797)3.5/2
如果在头文件中声明此静态函数,则包括此头文件的所有编译单元都将拥有其自己的函数副本。
问题在于,如果该函数内有静态变量,则包括此头文件的每个编译单元也将拥有其自己的版本。
内联函数?
将其声明为内联函数将使其成为内联的候选项(这在现代C++中意义不大,因为编译器会内联或不内联,有时会忽略是否存在关键字inline):
带有内联说明符的函数声明(8.3.5、9.3、11.3)声明了一个内联函数。内联说明符表明实现应优先考虑在调用点内联替换函数体,而不是使用常规函数调用机制。实现不必在调用点执行此内联替换;但是,即使省略此内联替换,仍应遵守7.1.2定义的内联函数的其他规则。
C++14(n3797)7.1.2/2
在头文件中,其具有一个有趣的副作用:可以在同一模块中多次定义内联函数,并且链接器将简单地将它们合并为一个(如果它们由于编译器原因未被内联)。
对于内部声明的静态变量,标准明确说明只有一个:
extern inline函数中的静态局部变量始终引用同一对象。
C++98/C++14(n3797)7.1.2/4
(默认情况下,函数是外部的,因此,除非您明确将函数标记为静态,否则适用于该函数)
这具有“静态”的优点(即它可以在头文件中定义),但没有其缺陷(如果未内联,则最多存在一次)。
静态局部变量?
静态局部变量没有链接(它们不能在其作用域之外通过名称引用),但具有静态存储期(即它是全局的,但其构造和析构服从特定规则)。
静态+内联?
混合使用内联和静态将产生你所描述的后果(即使函数是内联的,其中的静态变量也不会被内联,并且你会得到与包含静态函数定义的编译单元一样多的静态变量)。

回答作者的附加问题

自从我写了这个问题之后,我尝试在Visual Studio 2008中打开所有符合标准的选项,但我可能错过了一些。以下是结果:

当函数仅为“inline”时,只有一个静态变量的副本。

当函数为“static inline”时,有与翻译单元数量相同的副本。

真正的问题现在是事情是否应该是这样的,还是这是 Microsoft C++ 编译器的特异性。

所以我想你可能有类似于这样的东西:

void doSomething()
{
   static int value ;
}

您必须意识到函数内的静态变量,简单来说,就是一个全局变量,但只对函数本身可见。

内联函数不会改变任何内容:

inline void doSomething()
{
   static int value ;
}

只会有一个隐藏的全局变量。编译器尝试内联代码并不会改变只有一个全局隐藏变量的事实。

现在,如果您的函数被声明为静态:

static void doSomething()
{
   static int value ;
}

这里的“private”是指对于每个编译单元都是私有的,这意味着包括声明静态函数的头文件在内的每个CPP文件都将拥有其自己的私有函数副本,包括其自己的全局隐藏变量的私有副本,因此变量数量与包含头文件的编译单元一样多。

在一个带有“static”变量的“static”函数中添加“inline”:

inline static void doSomething()
{
   static int value ;
}

就静态变量而言,不添加“inline”关键字和添加“inline”关键字的结果是相同的。

因此,VC++的行为是正确的,您误解了“inline”和“static”的真正含义。


1
我认为你漏掉了一个重要的点需要提到,那就是在链接阶段,所有在内联函数中声明的静态变量都会被解析为一个,我错了吗? - user14416
1
不会,因为每个静态变量都在其自己独立的函数内部:尽管这些函数具有相同的名称,但它们具有内部链接,因此不能在翻译单元之间共享。 - paercebal
1
inline void doSomething() { static int value ; } 中,该函数具有外部链接;如果它出现在从两个不同单元包含的头文件中,则会违反 ODR。 - M.M
1
@zzz777,我不熟悉C的标准,但在C201x标准草案(http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1548.pdf)中,在6.7.4§3(函数说明符)中提到:“具有外部链接的内联函数定义不得包含具有静态或线程存储期的可修改对象的定义,并且不得包含对具有内部链接的标识符的引用。”这似乎排除了具有静态局部变量的extern inline函数。我以为extern是函数的默认值,但显然在这种情况下不是...我不知道...另请参见§7... - paercebal
实际上,我认为这可能发生的方式是如果它被内联到调用者中,每个内联版本可能会引用其自己版本的静态变量。我不知道为什么没有决定要求所有内联副本像C++那样引用相同的内容,但似乎作为一个原则,C处理“inline”留给链接器的事情更少(例如,与C ++不同,其中链接器的工作是解决在TUs跨越标记为inline的函数的重复符号,在C中,如果无法将事物内联到调用者中,则必须在TU中存在恰好一个“extern inline”版本)。 - 1110101001
显示剩余5条评论

45

我相信编译器会创建许多变量的副本,但链接器会选择其中一份并使其他所有副本引用它。当我尝试创建不同版本的内联函数时,也得到了类似的结果; 如果该函数没有实际被内联(调试模式),则无论从哪个源文件调用,所有调用都转到同一个函数。

暂且假设自己是编译器 - 否则还能怎么做呢?每个编译单元(源文件)都独立于其他单元,可以单独编译;因此,每个编译单元都必须创建变量的副本,并认为该副本是唯一的。链接器有能力跨越这些边界,并调整变量和函数的引用。


3
据我所知,你在这里所说的完全正确。我不明白为什么人们会对这个答案进行负面评价。我唯一的猜测是他们读到“变量的许多副本”,然后就停了! :( 无论如何,我会给你一个赞 (+1)。 - Richard Corden
3
当人们询问编译器的作用时,他们指的是编译器和链接器,因为无法运行目标文件。因此,这个回答是正确的,但完全没有意义。 - Evan Dark
1
因为人们缺乏知识,这是一个更高级的问题,所有人都应该在讨论中做出区分。 - Sogartar
1
实际上,这个答案非常有意义。它回答了“意思”问题,但仍然使用了正确的术语。 - Alex Che
我刚用gcc v9.3.0测试了一下,即使在链接和剥离之后,它仍然会为每个文件中内联函数的静态变量创建一个副本。因此,在我的情况下,我有3个源文件,每个文件都使用相同的内联函数。内联函数的静态数据在编译后的二进制文件中出现了3次(使用-s -O2编译)。clang v10.0.0也是如此。 - CR.
显示剩余5条评论

12
我发现Mark Ransom的回答很有帮助——编译器会创建许多静态变量的副本,但链接器会选择一个,并在所有翻译单元中强制执行它。
我在其他地方找到了这个:
请参见[dcl.fct.spec] / 4
[...] 具有外部链接的内联函数在所有翻译单元中必须具有相同的地址。 extern inline函数中的静态局部变量始终指向同一对象。 在extern inline函数中的字符串字面值是不同翻译单元中的同一对象。
我没有标准副本可以检查,但这符合我的经验,在VS Express 2008中检查汇编代码。

6

应该是这样的。

"static"告诉编译器您希望函数局限于编译单元,因此您希望每个编译单元有一个副本和每个函数实例的静态变量的一个副本。

"inline"用于告诉编译器您希望将函数内联;现在,它只是将其视为“如果代码有几个副本,则可以”,只需确保是相同的函数即可。因此,每个人都共享静态变量。

注意:此答案是针对原始发布者自己发布的答案编写的。


2
他正在询问“内联函数”中的“静态变量”,而不是静态函数中的变量。 - Richard Corden
我们同意这一点,但你是对的:需要进行编辑以将答案放回上下文中。 - Raphaël Saint-Pierre
我也遇到了这个。所以这两个选项中哪一个是正确的?inline是使函数内联,还是允许有多个副本? - Vassilis
@Vassilis 两种说法都是正确的,尽管 inline 并不会 导致 内联,它只是建议内联,并允许有多个定义(但不能在同一编译单元中)。 - Raphaël Saint-Pierre

3
自从我写了这个问题以来,我用Visual Studio 2008试了一下。 我试图打开所有使VS遵守标准的选项,但我可能错过了一些。 结果如下:
当函数仅为“inline”时,静态变量只有一个副本。
当函数为“static inline”时,有与翻译单元数相同数量的副本。
真正的问题是现在是否应该这样做,或者这是微软C ++编译器的特异性。

1
当函数是 "static inline" 时,你最初的帖子没有提到这一点。你应该期望不同的结果,因为在函数上的 static 和在变量上的 static 意义不同。函数上的 static 意味着其他翻译单元看不到这个定义。 - Windows programmer
不确定你的设置,但在这种情况下编译器是正常工作的。但是,为了防止将来遇到某些不符合规范的编译器,您可能需要包括单元测试。 - Robert Gould

-2
内联意味着可执行代码(指令)被嵌入到调用函数的代码中,编译器可以选择在您要求之前进行操作。这对函数中声明的变量(数据)没有影响。

-3

静态意味着一份副本分布在整个程序中,但内联意味着在同一程序中需要多次使用相同的代码,因此不可能在内联函数内部使变量静态。


-4

我相信你最终会得到每个翻译单元一个版本。实际上,你有许多版本的该函数(及其声明的静态变量),每个包含该头文件的翻译单元都有一个。


-4
除了任何设计问题之外,既然您已经卡在这里了,您应该在这种情况下使用静态而不是内联。这样每个人都共享相同的变量。(静态函数)

1
这个答案是错误的。静态具有与您描述的相反的效果。 - Johan Boulé

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