一个引用单例是放在栈上还是堆上?

4

我已经阅读了很多关于单例的文章,但没有一个能够解决我的问题。我知道只有在需要时才应该使用单例,在我的游戏中,我正在为引擎的特定部分使用它们。

那么,我最初将我的单例设置为指针,如下所示:

static MapReader* Instance()
{
    if (instance == 0)
    {
        instance = new MapReader();
        return instance;
    }
    return instance;
}

然而,我一直认为使用过多指针容易出现泄漏问题,如果我能不用它们的话(或者必须使用智能指针),我更倾向于不使用。因此,我将所有单例都改成了如下的引用形式:

static MapReader& Instance()
{
    static MapReader instance;
    return instance;
}

然而,现在我发现我的游戏在奇怪的时间会出现卡顿,然后加速,就像 FPS 有点不稳定。

我的问题是:引用单例是否全部堆积在堆栈上?还是它们仍然分配在堆上?我应该把它们改回指针并使用智能指针吗?

2个回答

3
几乎可以肯定地说,由于没有使用“new”创建,因此它几乎不可能在堆上。
然而,标准对静态变量的存储位置没有明确规定,只提到了它们的行为。例如,参见C++11 3.7.1:
1. 所有没有动态存储期、线程存储期且不是局部变量的变量都具有静态存储期。这些实体的存储空间应持续到程序的生命周期(3.6.2,3.6.3)。 2. 如果具有静态存储期的变量具有初始化或具有副作用的析构函数,则即使它看起来未被使用,也不会被消除,但类对象或其副本/移动可以根据12.8中指定的方式被消除。 3. 关键字static可用于声明具有静态存储期的局部变量。 4. 在类定义中将关键字static应用于类数据成员会给出数据成员的静态存储期。
这基本上是标准对它们的要求。
大多数实现可能会有一个与堆和栈都不同的区域,用于存储具有静态存储期的变量。
几乎可以肯定的是,静态变量和引用并不会拖慢您的代码速度。深入研究编译器及其工作原理后,静态变量往往至少与其他变量一样快,因为它们在内存中定位非常快。
另外,您的单例指针变体存在两个问题。第一个是潜在的竞争条件,如果您在多线程环境中工作,则可能会创建多个对象,如果不同的线程调用Instance()。具体来说,如果线程A进入if语句,然后线程B开始运行,B可以通过创建对象并返回来继续执行。如果A然后继续,它将创建一个新对象。如果您是单线程的,或者只从一个线程创建实例,或者在其他线程运行之前创建实例,则应该没问题。第二个问题只是一些花哨的东西。没有必要在if块内返回对象,因为当您到达函数底部时,它将被返回。
static MapReader* Instance() {
    if (instance == 0)
        instance = new MapReader();
    return instance;
}

我担心由于堆栈上有太多内容,我的代码可能会变慢。这就是为什么我关注单例放置的位置的原因。 - Oria
过多的堆栈不会减慢您的代码。如果您超出了大小,它可能会导致崩溃,但引用并不比指针更具破坏性。 - paxdiablo
那很好,知道它不会减慢速度并只是崩溃。到目前为止,我没有遇到任何崩溃,只有偶尔的FPS跳动。 - Oria
非常感谢!这让我对事物的状态有了一个大致的了解,我会继续将它们作为参考。如果我被允许的话(需要15个声望),我会点赞的。 - Oria

1
静态变量不属于函数内部,它们位于数据段。同样,静态变量在函数外和全局范围内也是如此。

我挑战你找到ISO标准中“数据段”的任何一个提及。 :-) - paxdiablo
那与此有何关联? - Seva Alekseyev
这很重要,因为C++没有数据段的概念。一个_实现_可能以某种方式执行,但这并不是强制性的。从技术上讲,静态存储周期变量__可以__在堆上,因为符合规范的实现可能会在调用main()之前对它们进行全部new操作。 - paxdiablo
1
取决于原帖想要了解的是标准还是现实情况。 :) - Seva Alekseyev
@paxdiablo 是的,我认为我的实现有一个堆栈,不是所有没有使用new创建在堆上的对象都会进入堆栈吗?至于平台,目前正在开发适用于Windows和Linux,并使用SDL2。 - Oria
显示剩余7条评论

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