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

Mas*_*ari 16 c++ memory singleton static dynamic-memory-allocation

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

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

还有这个:

class singleton
{
    // ...
    static singleton& getInstance()
    {
        // using static variable
        static singleton instance;
        return instance;
    }
    // ...
};
Run Code Online (Sandbox Code Playgroud)


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

更新:

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

  • 在使用静态局部变量的单例中,类析构函数在进程终止时自动调用,而在动态分配情况下,您必须在某个时候管理对象破坏,例如通过使用智能指针:
    static singleton& getInstance() {
        static std::auto_ptr<singleton> instance (new singleton());
        return *instance.get(); 
    }
Run Code Online (Sandbox Code Playgroud)
  • 使用动态分配的单例比静态单例变量"更懒惰",如后一种情况,单例对象所需的内存(始终?)在进程启动时保留(作为加载程序所需的整个内存的一部分)并且只调用单例构造函数被推迟到getInstance()调用时.这可能sizeof(singleton)很重要.

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

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


PS:根据@TonyD的答案,我已经更正了我在原帖中使用的术语.

Ton*_*roy 7

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

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

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

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

讨论重新编译时与运行时分配和初始化

从你的汇总和一些评论的方式来看,我怀疑你并没有完全理解静态变量的分配和初始化的微妙方面....

假设你的程序有3个本地静态32位int的S - a,b以及c-在不同的功能:编译器可能编译二进制文件告诉操作系统加载器离开3x32位= 12个字节的内存对于那些静.编译器决定每个变量的偏移量:它可以放在a数据段的偏移量1000十六进制,b1004和c1008.当程序执行时,OS加载器不需要为每个分别分配内存 -所有它知道的是总共12个字节,它可能会或可能不会被特别要求0初始化,但它可能想要做任何事情以确保进程无法看到来自其他用户程序的内存内容.程序中的机器代码指令通常将对偏移1000,1004,1008进行硬编码以便访问a,b并且c因此在运行时不需要分配那些地址.

动态存储器分配是,所述不同的指针(比如p_a,p_b,p_c)将给出如刚刚描述在编译时地址,但额外的:

  • 必须在运行时找到指向的内存(每个a,bc)(通常在静态函数首次执行时,但编译器允许按照我对其他答案的评论更早地执行),以及
    • 如果操作系统当前为进程提供的内存太少而无法成功进行动态分配,那么程序库将要求操作系统提供更多内存(例如使用sbreak()) - 出于安全原因操作系统通常会将其清除
    • 为每个分配的动态地址a,b并且c必须被复制回指针p_a,p_bp_c.

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