C++提醒:函数局部静态变量仅初始化一次,即使看起来像是多次初始化

The Old New Thing (Raymond Chen) 新闻

摘要

本文解释了C++中的函数局部静态变量仅初始化一次,并通过一个示例展示如何通过将变量设为静态来避免释放后使用漏洞,同时讨论了共享状态的影响。

<p>当你在函数内编写一个静态变量时,它只被初始化一次,具体来说是<a title="Magic statics vs. std::call_once" href="https://devblogs.microsoft.com/oldnewthing/20260916-00/?p=112703">在执行首次到达该变量的声明时</a>。如果以后再次执行到该变量,不会发生初始化,它只是保留其旧值。</p> <p>前一段时间,我注意到一个通过将变量设为静态来修复生命周期问题的尝试。</p> <p>原始代码使用了来自外部小部件库的这个头文件:</p> <pre>// widget.h struct WidgetController { virtual WidgetKind GetKind() = 0; virtual WidgetFlags GetFlags() = 0; virtual void OnOpening() = 0; ⟦ 以及类似内容 ⟧ }; std::shared_ptr&lt;Widget&gt; MakeWidget(std::shared_ptr&lt;WidgetController&gt; const&amp; controller); </pre> <p>其想法是,你提供一个<code>WidgetController</code>对象,小部件会在各种时候咨询它,从而允许你自定义小部件行为。</p> <p>应用程序代码这样调用它:</p> <pre>// 基本的Widget控制器提供信息但 // 不覆盖任何默认行为。 struct BasicWidgetInfo { WidgetKind kind; WidgetFlags flags; ⟦ 以及类似内容 ⟧ }; struct BasicWidgetController : WidgetController { BasicWidgetController(BasicWidgetInfo const&amp; info) : m_info(info) {} WidgetKind GetKind() override { return m_info.kind; } WidgetFlags GetFlags() override { return m_info.flags; } // 不自定义任何动态操作。 void OnOpening() override { } ⟦ 以及类似内容 ⟧ private: BasicWidgetInfo const&amp; m_info; } struct Gadget { std::shared_ptr&lt;Widget&gt; m_widget; void CreateWidget(GadgetFlags flags) { BasicWidgetInfo info = { WidgetKind::Vanilla, WidgetFlags::Openable | (flags &amp; GadgetFlags::ClosableWidget ? WidgetFlags::Closable : WidgetFlags::None) }; auto controller = std::make_shared&lt;BasicWidgetController&gt;(info); m_widget = MakeWidget(controller); } ⟦ 其他小部件内容 ⟧ }; </pre> <p>问题在于<code>WidgetOptions</code>构造函数接受一个<code>GadgetOptions</code>的引用并保存了该引用。后来,当小部件向控制器请求标志时,控制器会在<code>BasicWidgetInfo</code>结构中查找答案,但那个<code>BasicWidgetInfo</code>在<code>CreateCustomWidget</code>返回时已经析构,因此它返回垃圾值(或者甚至可能崩溃)。</p> <p>这是一个释放后使用漏洞。</p> <p>为了解决这个问题,他们将<code>info</code>设为静态。静态对象即使在函数返回后也会继续存在。</p> <pre> void CreateWidget(GadgetFlags flags) { <span style="border: solid 1px currentcolor;">static</span> BasicWidgetInfo info = { WidgetKind::Vanilla, WidgetFlags::Openable | (flags &amp; GadgetFlags::ClosableWidget ? WidgetFlags::Closable : WidgetFlags::None) }; auto controller = std::make_shared&lt;BasicWidgetController&gt;(info); m_widget = MakeWidget(controller); } </pre> <p>现在程序不再崩溃。太好了!</p> <p>然而,有一个问题:如果两个Gadgets都尝试创建一个小部件,它们都将具有与第一个相同的选项,因为函数局部静态变量在所有类实例中共享,且仅在执行首次到达该变量时初始化。第一次调用时传递的任何标志都被锁定在<code>info</code>中,后续传递的标志无关紧要,因为<code>info</code>已经初始化;它不会再次初始化。</p> <p>如果你想每次都初始化它,那么你必须每次都修改它。</p> <pre> void CreateWidget(GadgetFlags flags) { <span style="border: solid 1px currentcolor; border-bottom: none;">static BasicWidgetInfo info;</span> <span style="border: solid 1px currentcolor; border-top: none;">info = { </span> WidgetKind::Vanilla, WidgetFlags::Openable | (flags &amp; GadgetFlags::ClosableWidget ? WidgetFlags::Closable : WidgetFlags::None) }; auto controller = std::make_shared&lt;BasicWidgetController&gt;(info); m_widget = MakeWidget(controller); } </pre> <p>这次,我们将赋值操作与构造分开执行,这意味着它每次都会执行,<code>info</code>会用最新的标志更新。</p> <p>当然,如果两个Gadgets创建具有重叠生命周期的Widgets,这仍然是一个问题,因为两个<code>BasicWidgetController</code>共享同一个<code>info</code>。在第二次调用<code>CreateWidget</code>时,它对<code>info</code>的更新会悄悄改变第一个使用的值。</p> <p>此外,如果<code>CreateWidget</code>被两个线程同时调用,你就会在写入<code>info</code>变量时发生数据竞争,然后结果将不可预测。</p> <p>根本问题是<code>BasicWidgetController</code>想要延长其<code>info</code>的生命周期,但引用无法做到这一点,因此它不得不依赖陌生人的善意。</p> <p>一个想法是将<code>info</code>放在其他地方,以便以其他方式延长其生命周期。也许你把它放在<code>Gadget</code>中:</p> <pre>struct Gadget { std::shared_ptr&lt;Widget&gt; m_widget; <span style="border: solid 1px currentcolor;">BasicWidgetInfo m_info;</span> void CreateWidget(GadgetFlags flags) { <span style="border: solid 1px currentcolor;">m_info = {</span> WidgetKind::Vanilla, WidgetFlags::Openable | (flags &amp; GadgetFlags::ClosableWidget ? WidgetFlags::Closable : WidgetFlags::None) }; auto controller = std::make_shared&lt;BasicWidgetController&gt;(<span style="border: solid 1px currentcolor;">m_info</span>); m_widget = MakeWidget(controller); } ⟦ 其他小部件内容 ⟧ }; </pre> <p>现在你的工作是确保<code>m_info</code>在最后一个指向<code>BasicWidgetController</code>的shared_ptr之前不被析构。这很棘手,因为你并不真正知道最后一个指向<code>BasicWidgetController</code>的shared_ptr何时会被析构,尽管你可能有一些启发式方法,因为它的生命周期可能与<code>Widget</code>绑定。</p> <p>是否有办法挂钩到最终<code>shared_ptr</code>的析构?</p> <p>是的,事实上我们已经在不知不觉中使用了这个特性。</p> <p>你可以使用一个别名shared_ptr,它指向一个<code>BasicWidgetController</code>,但其生命周期控制<code>BasicWidgetController</code>及其关联的<code>BasicWidgetInfo</code>。</p> <pre>struct BasicWidgetControllerWithInfo { BasicWidgetControllerWithInfo(BasicWidgetInfo const&amp; info) : m_info(info), m_controller(m_info) {} // m_info必须在m_controller之前,因为 // m_controller初始化器依赖于m_info。 BasicWidgetInfo m_info; BasicWidgetController m_controller; }; void CreateWidget(GadgetFlags flags) { BasicWidgetInfo info = { WidgetKind::Vanilla, WidgetFlags::Openable | (flags &amp; GadgetFlags::ClosableWidget ? WidgetFlags::Closable : WidgetFlags::None) }; auto controllerAndInfo = std::make_shared&lt;BasicWidgetCont
查看原文
查看缓存全文

缓存时间: 2026/09/29 14:46

# C++ 提醒:函数内的静态变量只初始化一次,即使看起来会被多次初始化 来源:https://devblogs.microsoft.com/oldnewthing/20260928-00/?p=112738/ 当你在函数内部编写一个静态变量时,它只会被初始化一次——具体来说,是在执行流首次到达该变量的声明处时(https://devblogs.microsoft.com/oldnewthing/20260916-00/?p=112703)。如果之后执行流再次到达该变量,不会再进行初始化,它只是保留旧值。 前段时间,我注意到有人尝试通过将变量设为静态来修复一个生命周期问题。原始代码使用了来自外部控件库的这个头文件: ```cpp // widget.h struct WidgetController { virtual WidgetKind GetKind() = 0; virtual WidgetFlags GetFlags() = 0; virtual void OnOpening() = 0; // 其他内容... }; std::shared_ptr<Widget> MakeWidget(std::shared_ptr<WidgetController> const& controller); ``` 其思路是,你提供一个 `WidgetController` 对象,控件会在各个时机咨询它,从而允许你定制控件行为。应用代码的调用方式如下: ```cpp // 基础控件控制器提供信息但不覆盖任何默认行为。 struct BasicWidgetInfo { WidgetKind kind; WidgetFlags flags; // 其他内容... }; struct BasicWidgetController : WidgetController { BasicWidgetController(BasicWidgetInfo const& info) : m_info(info) {} WidgetKind GetKind() override { return m_info.kind; } WidgetFlags GetFlags() override { return m_info.flags; } // 不定制任何动态操作。 void OnOpening() override { } // 其他内容... private: BasicWidgetInfo const& m_info; }; struct Gadget { std::shared_ptr<Widget> m_widget; void CreateWidget(GadgetFlags flags) { BasicWidgetInfo info = { WidgetKind::Vanilla, WidgetFlags::Openable | (flags & GadgetFlags::ClosableWidget ? WidgetFlags::Closable : WidgetFlags::None) }; auto controller = std::make_shared<BasicWidgetController>(info); m_widget = MakeWidget(controller); } // 其他小工具内容... }; ``` 问题在于,`BasicWidgetController` 构造函数接受一个对 `BasicWidgetInfo` 的引用并保存它。后来,当控件向控制器请求标志时,控制器会在 `BasicWidgetInfo` 结构体中查找答案,但那个 `BasicWidgetInfo` 在 `CreateWidget` 返回时就已析构,因此它返回的是垃圾数据(甚至可能崩溃)。这是一个**释放后使用**的 bug。 为了解决这个问题,他们将 `info` 设为了静态。静态对象即使在函数返回后也会持续存在。 ```cpp void CreateWidget(GadgetFlags flags) { static BasicWidgetInfo info = { WidgetKind::Vanilla, WidgetFlags::Openable | (flags & GadgetFlags::ClosableWidget ? WidgetFlags::Closable : WidgetFlags::None) }; auto controller = std::make_shared<BasicWidgetController>(info); m_widget = MakeWidget(controller); } ``` 现在程序不再崩溃了。太棒了! 然而,这里有个陷阱:如果两个 `Gadget` 都尝试创建控件,它们都会拥有与第一个相同的选项,因为**函数内的静态变量在类的所有实例间共享,且只在执行流首次到达该变量时初始化**。第一次调用时传入的标志被锁定到了 `info` 中,之后无论你传入什么标志都无关紧要,因为 `info` 已经初始化过了,不会再次初始化。 如果你想每次都重新初始化,那么你必须每次都修改它。 ```cpp void CreateWidget(GadgetFlags flags) { static BasicWidgetInfo info; info = { WidgetKind::Vanilla, WidgetFlags::Openable | (flags & GadgetFlags::ClosableWidget ? WidgetFlags::Closable : WidgetFlags::None) }; auto controller = std::make_shared<BasicWidgetController>(info); m_widget = MakeWidget(controller); } ``` 这一次,我们将赋值作为一个独立于构造的步骤,这意味着它每次都会执行,`info` 会用最新的标志进行更新。当然,如果两个 `Gadget` 创建的控件生命周期有重叠,这仍然是一个问题,因为两个 `BasicWidgetController` 共享同一个 `info`。在第二次调用 `CreateWidget` 时,它对 `info` 的更新会暗中改变第一个控制器正在使用的值。另外,当然,如果 `CreateWidget` 被两个线程同时调用,那么对 `info` 变量的写操作会发生数据竞争,结果将不可预测。 根本问题在于,`BasicWidgetController` 希望延长其 `info` 的生命周期,但引用无法做到这一点,因此它只能依赖陌生人的善意。 一个想法是把 `info` 放在其他地方,以便能用其他方式延长其生命周期。或许可以把它放在 `Gadget` 中: ```cpp struct Gadget { std::shared_ptr<Widget> m_widget; BasicWidgetInfo m_info; void CreateWidget(GadgetFlags flags) { m_info = { WidgetKind::Vanilla, WidgetFlags::Openable | (flags & GadgetFlags::ClosableWidget ? WidgetFlags::Closable : WidgetFlags::None) }; auto controller = std::make_shared<BasicWidgetController>(m_info); m_widget = MakeWidget(controller); } // 其他小工具内容... }; ``` 现在你的任务是确保 `m_info` 不会在指向 `BasicWidgetController` 的最后一个共享指针之前析构。这很棘手,因为你并不真正知道 `BasicWidgetController` 的最后一个共享指针何时会析构,尽管你可能有一些启发式方法,因为它的生命周期很可能与 `Widget` 绑定。 有没有办法钩入最后一个 `shared_ptr` 的析构?实际上有,而且我们已经在无意中使用过这个特性。你可以使用一个**别名共享指针**(aliasing shared pointer),它指向一个 `BasicWidgetController`,但其生命周期同时控制一个 `BasicWidgetController` 和其关联的 `BasicWidgetInfo`。 ```cpp struct BasicWidgetControllerWithInfo { BasicWidgetControllerWithInfo(BasicWidgetInfo const& info) : m_info(info), m_controller(m_info) {} // m_info 必须在 m_controller 之前,因为 // m_controller 的初始化器依赖于 m_info。 BasicWidgetInfo m_info; BasicWidgetController m_controller; }; void CreateWidget(GadgetFlags flags) { BasicWidgetInfo info = { WidgetKind::Vanilla, WidgetFlags::Openable | (flags & GadgetFlags::ClosableWidget ? WidgetFlags::Closable : WidgetFlags::None) }; auto controllerAndInfo = std::make_shared<BasicWidgetControllerWithInfo>(info); auto controller = std::shared_ptr<WidgetController>( controllerAndInfo, &controllerAndInfo->m_controller); m_widget = MakeWidget(controller); } ``` 我们使用了指向控制器的别名构造函数,但告诉它控制 `BasicWidgetControllerWithInfo` 的生命周期。 当然,所有这一切都是应用程序自己造成的问题。他们应该直接修复 `BasicWidgetController`,使其**拷贝** `BasicWidgetInfo` 而不是接受引用。 ```cpp struct BasicWidgetController : WidgetController { BasicWidgetController(BasicWidgetInfo const& info) : m_info(info) {} WidgetKind GetKind() override { return m_info.kind; } WidgetFlags GetFlags() override { return m_info.flags; } // 不定制任何动态操作。 void OnOpening() override { } // 其他内容... private: BasicWidgetInfo /* const& */ m_info; }; ``` 现在原始代码又能正常工作了。 ```cpp void CreateWidget(GadgetFlags flags) { BasicWidgetInfo info = { WidgetKind::Vanilla, WidgetFlags::Openable | (flags & GadgetFlags::ClosableWidget ? WidgetFlags::Closable : WidgetFlags::None) }; auto controller = std::make_shared<BasicWidgetController>(info); m_widget = MakeWidget(controller); } ``` ### 分类 ### 主题 ## 作者 Raymond Chen Raymond 参与 Windows 的演进已超过 30 年。2003 年,他建立了一个名为 The Old New Thing 的网站,其受欢迎程度远超他最疯狂的想象,这一发展至今仍让他感到鸡皮疙瘩。该网站催生了一本书,书名也恰巧是 The Old New Thing(Addison Wesley 2007)。他偶尔会出现在 Windows Dev Docs 的 Twitter 账号上,讲些毫无实用信息的故事。

相似文章

静态分配,恒定工作

Lobsters Hottest

本文探讨了静态分配策略,以防止释放后使用、类型混淆等内存安全问题,讨论了对象池和代际索引,并介绍了来自TigerStyle的技巧,以在初始化后避免动态内存分配。

魔法静态变量 vs. std::call_once

The Old New Thing (Raymond Chen)

本文比较了两种 C++ 技术:魔法静态变量和 std::call_once,用于缓存计算,并解释了根据作用域和初始化等因素何时使用哪种技术。

C++ 中的惰性初始化

Lobsters Hottest

本文解释 C++ 中的惰性初始化,通过将计算推迟到必要时来优化性能,使用辅助结构体来利用 std::optional 中的转换运算符。

就地初始化的四个层次

Lobsters Hottest

文章讨论了 Rust 中就地初始化的四个层次,从原始指针到语言提案如 &uninit/&own,以提高编程中的内存效率和安全性。