C ++单例类实例的堆/动态与静态内存分配

时间:2013-02-25 08:21:57

标签: c++ memory static singleton dynamic-memory-allocation

我的具体问题是,在C ++中实现singleton class时,以下两个代码之间在性能,问题或其他方面存在实质性差异:

class singleton
{
    // ...
    static singleton& getInstance()
    {
        // allocating on heap
        static singleton* pInstance = new singleton();
        return *pInstance;
    }
    // ...
};

和此:

class singleton
{
    // ...
    static singleton& getInstance()
    {
        // using static variable
        static singleton instance;
        return instance;
    }
    // ...
};


(请注意,基于堆的实现中的解除引用不应该影响性能,因为AFAIK没有为解除引用生成额外的机器代码。似乎只需要语法来区分指针。)

更新

我有一些有趣的答案和评论,我试着在这里总结一下。 (建议有兴趣的人阅读详细的答案。):

  • 在使用 static 局部变量的单例中,类析构函数在进程终止时自动调用,而在动态分配情况下,您必须在某处管理对象销毁有时候,例如通过使用智能指针:
    static singleton& getInstance() {
        static std::auto_ptr<singleton> instance (new singleton());
        return *instance.get(); 
    }
  • 使用动态分配的单例是“lazier”而不是静态单例变量,在后一种情况下,单例对象所需的内存是(始终?)在进程启动时保留-up(作为加载程序所需的整个内存的一部分)并且仅调用singleton构造函数被推迟到getInstance()调用时。当sizeof(singleton)很大时,这可能很重要。

  • 两者都是C ++ 11中的线程安全的。但是对于早期版本的C ++,它是特定于实现的。

  • 动态分配案例使用一个间接级别来访问单例对象,而在静态单例对象案例中,确定对象的直接地址并在编译时进行硬编码。

    < / LI>


P.S。:我已根据@TonyD的回答更正了我在原始帖子中使用的术语。

2 个答案:

答案 0 :(得分:7)

  • new版本显然需要在运行时分配内存,而非指针版本在编译时分配内存(但两者都需要做相同的构造)

  • new版本不会在程序终止时调用对象的析构函数,但非new版本将:您可以使用智能指针来纠正此

    • 你需要注意一些静态/命名空间范围对象的析构函数在其静态本地实例的析构函数运行后不会调用你的单例...如果你关心这个,你应该多读一些单身生命周期和管理它们的方法。 Andrei Alexandrescu的Modern C ++ Design具有非常易读的处理方式。
  • 在C ++ 03下,它的实现定义是否是线程安全的。 (我相信GCC往往是,而Visual Studio往往没有 - 评论确认/纠正赞赏。)

  • 在C ++ 11下,它是安全的:6.7.4“如果控件在初始化变量时同时进入声明,则并发执行应等待初始化完成。” (没有递归)。

讨论重新编译时与运行时分配&amp;初始化

从您对自己的摘要和一些评论的措辞,我怀疑你并没有完全理解静态变量的分配和初始化的微妙方面....

假设您的程序有3个本地静态32位int s - abc - 在不同的函数中:编译器可能编译一个二进制文件告诉OS加载程序为这些静态留下3x32位= 12字节的内存。编译器决定每个变量的偏移量:它可以将a放在数据段中的偏移量1000十六进制位置,b位于1004位置,c位于1008位置。当程序执行时,OS加载器不需要为每个单独分配内存 - 它只知道总共12个字节,它可能已经或可能没有被特别要求0初始化,但它可能想要做任何事情以确保该过程无法看到来自其他用户程序的内存内容。程序中的机器代码指令通常会对偏移1000,1004,1008进行硬编码,以便访问abc - 因此在运行时不需要分配这些地址-time。

动态内存分配的不同之处在于指针(比如p_ap_bp_c)将在编译时给出地址,如上所述,但另外:

  • 指向内存(abc中的每一个)必须在运行时找到(通常在静态函数首次执行但编译器允许的情况下)根据我对另一个答案的评论,及早做到这一点)
    • 如果操作系统当前为进程提供的内存太少,动态分配成功,那么程序库将要求操作系统获取更多内存(例如使用sbreak()) - 操作系统通常会出于安全原因消灭
    • 为每个abc分配的动态地址必须复制回指针p_ap_b和{{1} }}。

这种动态方法显然更复杂。

答案 1 :(得分:2)

主要区别在于使用本地static对象将在关闭程序时被销毁,而堆分配的对象将被抛弃而不会被销毁。

请注意,在C ++中如果在函数内部声明一个静态变量,它将在您第一次进入作用域时初始化,而不是在程序启动时初始化(就像它发生在全局静态持续时间变量中一样)。

多年来,我一直从使用延迟初始化切换到显式控制初始化,因为程序启动和关闭是微妙的阶段,很难调试。如果你的类没有做任何复杂的事情而且不能失败(例如它只是一个注册表),那么即使是懒惰的初始化也没问题......否则控制将会为你节省很多问题。

在输入main的第一条指令之前或执行main的最后一条指令之后崩溃的程序更难调试。

使用懒惰构造单例的另一个问题是,如果你的代码是多线程的,你必须注意让并发线程同时初始化单例的风险。在单个线程上下文中进行初始化和关闭更简单。