一个C++类能否确定它是在堆上还是栈上?

47

我有

class Foo {
....
}
有没有一种方法让Foo能够分离出:
function blah() {
  Foo foo; // on the stack
}

并且

function blah() {
  Foo foo* = new Foo(); // on the heap
}

我希望根据Foo对象是在堆上还是栈上分别实现不同的功能。

编辑:

很多人问我“为什么这样做?”

答案是:

我现在正在使用引用计数的GC。但是,我也想能够运行标记和清除算法。为此,我需要标记一组“根”指针--这些指针位于堆栈上。因此,对于每个类,我想知道它们是在堆上还是在栈上。


2
分离出来,例如?在我看来,静态分配是在堆栈上完成的,而像“new”这样的分配将在堆上完成。 - user59634
3
为什么你需要将它们区分开来,有什么用途? - Georg Fritzsche
11
这个问题能被回答吗?无论那个人是否知道自己在做什么,对于那些确实需要它的人来说可能很有用。 - Matt Joiner
1
目瞪口呆地转动眼睛:-( - Martin York
3
无法进行可移植性的操作,即使能够进行也不会提供有用的信息。如果您认为自己需要这样做,几乎可以肯定是错误的。 - JoeG
显示剩余6条评论
15个回答

22
一种折中的方法:
struct Detect {
   Detect() {
      int i;
      check(&i);
   }

private:
   void check(int *i) {
      int j;
      if ((i < &j) == ((void*)this < (void*)&j))
         std::cout << "Stack" << std::endl;
      else
         std::cout << "Heap" << std::endl;
   }
};

如果对象是在栈上创建的,那么它必须位于外部函数栈变量方向的某个地方。堆通常从另一侧增长,因此栈和堆会在中间某处相遇。

(当然,有些系统可能不适用这种情况)


1
不过我并不建议为任何真正的任务这么做,这只是一个想法。 - sth
3
我没有测试过,但这可能在多线程应用程序中无法正常工作。 - Nick Dandoulakis
10
我也相信他知道你知道他知道,并且只是在说话。 - Martin York
2
我实际上在2003年尝试过这个。不幸的是,其中一个它无法运行的系统几乎是任何开启优化的C++编译器。 - Daniel Earwicker
1
这在任何现代系统上都不起作用,即任何支持线程的系统。 - Paul Groke
显示剩余9条评论

9
您需要实际询问我们真正的问题(a) :- )。您认为这是必要的,但它几乎肯定不是。实际上,这几乎总是个坏主意。换句话说,您认为为什么需要这样做?
我通常发现,这是因为开发人员想要根据对象分配的位置删除或不删除对象,但这通常应留给代码的客户端而不是代码本身来处理。

更新:

既然您在问题中澄清了原因,那我道歉,您可能已经找到了很少几个需要这样做的领域(运行自己的垃圾回收进程)。理想情况下,您需要覆盖所有内存分配和释放运算符以跟踪从堆中创建和删除的内容。

然而,我不确定是否简单地拦截类的new/delete操作,因为有些情况下可能不会调用delete,而且由于标记/扫描依赖于引用计数,因此您需要能够拦截指针赋值才能使其正常工作。

您考虑过如何处理这个问题吗?

经典的例子:

myobject *x = new xclass();
x = 0;

不会导致删除调用。

另外,你如何检测到指向你的实例之一的 指针 是否在堆栈上?拦截 new 和 delete 可以让你存储对象本身是基于堆栈还是基于堆的,但我不知道如何告诉你指针将被分配到哪里,特别是对于像这样的代码:

myobject *x1 = new xclass();  // yes, calls new.
myobject *x2 = x;             // no, it doesn't.

也许您需要了解C++的智能指针,它们可以大大减少手动内存管理的工作量。但是单独使用共享指针仍可能遇到循环依赖等问题,不过巧妙地使用弱指针就可以轻松解决这个问题。

也许在您的情况下不再需要手动进行垃圾回收。


(a) 这被称为“X/Y问题”。很多时候,人们会提出一个预设解决方案的问题,而更好的方法是只描述问题本身,不要预先设想最佳解决方案。


3
在用户空间标记/清除垃圾收集器中,我期望提供某种智能指针来包含可回收对象的指针(从效果上看,这提供了准确的标记)。因此,您的代码片段不合法,因为它们仅使用非垃圾收集的原始指针引用 gc 对象。一个“编译器空间”实现可能使用保守标记并直接分析堆栈。 - Steve Jessop
1
重载 new 不是完全可靠的。你可以使用 malloc() 分配一个缓冲区,然后使用 placement new(或者只是简单地强制转换)将其转换为类。这仍然看起来像是基于堆栈的类,但它在堆上。在我看来,你不能垃圾回收使用 new 创建的东西:你需要自己分配和指针包装器。 - AshleysBrain
我计划与引用计数智能指针一起使用它。这些指针已经重载了创建、operator=和析构函数。上面的例子将变成:MyObject::Ptr x = new MyObject(); x = 0; // operator=的重载导致x的引用计数减少,从而触发析构函数。 - anon
你应该尝试使用 boost::shared_ptr,这是一个更加规范和经过测试的实现引用计数的方式。 - GManNickG
1
在C++11中,可以使用std::shared_ptr来解决boost::shared_ptr存在的一些问题,或者使用@GManNickG。 - user1203803

8

如果你将“this”的值与当前堆栈指针的值进行比较,那么有可能判断出你是否在堆栈中分配了空间。如果“this”< sp,则表示你已经被分配在堆栈中。

可以尝试以下操作(使用x86-64中的gcc):

#include <iostream>

class A
{
public:
    A()
    {
        int x;

        asm("movq %1, %%rax;"
            "cmpq %%rsp, %%rax;"
            "jbe Heap;"
            "movl $1,%0;"
            "jmp Done;"
            "Heap:"
            "movl $0,%0;"
            "Done:"
            : "=r" (x)
            : "r" (this)
            );

        std::cout << ( x ? " Stack " : " Heap " )  << std::endl; 
    }
};

class B
{
private:
    A a;
};

int main()
{
    A a;
    A *b = new A;
    A c;
    B x;
    B *y = new B;
    return 0;
}

应该输出:
Stack 
Heap 
Stack 
Stack 
Heap

你能否重新为VC++打出这个asm()部分吗?我在VS2008下使用它遇到了困难。谢谢。 - Aoi Karasu

8
答案是否定的,没有标准/可移植的方法来实现这一点。涉及重载new运算符的黑客技巧往往存在漏洞。依赖于检查指针地址的黑客技巧是特定于操作系统和堆实现的,并且可能会随着操作系统的未来版本而发生变化。您可能对此感到满意,但我不会建立任何围绕这种行为的系统。
我会开始寻找不同的方法来实现您的目标-也许您可以有一个完全不同的类型来作为您计划中的“根”,或者要求用户使用特殊构造函数正确注释堆栈分配的类型。

new的hack是否不可靠:你如何知道调用placement new时会将对象放在堆栈上还是堆中? - Matthieu M.
1
问题是“如何”做到,而不是“如何标准/可移植地做到”。 - Justicle

5
一种更直接、不那么侵入式的方法是在内存区域映射中查找指针(例如/proc/<pid>/maps)。每个线程都有一个分配给其堆栈的区域。静态和全局变量将驻留在.bss section,常量在rodata或const段中等等。

4
我不确定你的问题是什么,但是覆盖new操作符可能是你想要做的。在C++中,唯一安全的在堆上创建对象的方法是使用new操作符,因此你可以区分存在于堆上的对象和其他形式的内存。请搜索“在C++中重载new”以获取更多信息。
然而,你应该考虑是否真的需要从类内部区分两种类型的内存。如果你不小心的话,让一个对象根据存储位置表现出不同的行为听起来像是灾难的预谋!

2
不一定是真的。考虑这些对象的向量。向量的数据可能已经从堆中分配,但对象从未调用new。 - GManNickG
在向量中构造对象时,调用放置 new 来构造对象。现在我不确定这是否意味着您也需要提供放置 new...以前没有深入研究过。 - Michael Anderson
1
Placement-new 无法被替换。话虽如此,vector并没有使用placement-new。(或者其他容器也是如此。)它们调用其分配器的construct方法。(通常会调用placement-new。:P) - GManNickG
你关于向量的观点很好,不过我想你是指数组吧?通过将默认构造函数设为私有,可以禁止在数组中进行分配,但这样做很丑陋——特别是如果对象在其构造函数中不需要参数的情况下。 - Dan Breslau

4

如上所述,您需要通过重载 new 运算符来控制对象的分配。但要注意两点:首先是'定位 new'运算符,它在用户预分配的内存缓冲区内初始化您的对象;其次,没有任何防止用户将任意内存缓冲区强制转换为您的对象类型。

char buf[0xff]; (Foo*)buf;

另一种方式是,大多数运行时在进行堆分配时会使用比请求的内存多一点。它们通常会放置一些服务结构来标识指针的适当释放。你可以检查你的运行时实现这些模式,尽管这将使你的代码非常不可移植、危险和过度冗余。
同样,如上所述,你真正需要问的是初始问题("为什么"),而不是解决方案的细节("怎么做")。

3

不行,这不能可靠或合理地完成。

您可能能够通过重载new来检测何时使用new分配对象。

但是如果对象作为类成员构造,拥有类在堆上分配怎么办?

这是第三个代码示例,添加到您已经拥有的两个示例中:

class blah {
  Foo foo; // on the stack? Heap? Depends on where the 'blah' is allocated.
};

关于静态/全局对象呢?你怎么区分它们和栈/堆中的对象?
你可以查看对象的地址,并使用该地址来确定它是否在定义栈的范围内。但是栈可能会在运行时调整大小。
因此,最好的答案实际上是“有一个原因,为什么不使用标记和清除GC与C++一起使用”。 如果您需要一个适当的垃圾收集器,请使用支持它的其他语言。
另一方面,大多数经验丰富的C++程序员发现,当您学习必要的资源管理技术(RAII)时,对垃圾收集器的需求几乎消失了。

2
一种 MFC 类的方法: .H 文件
class CTestNEW : public CObject
{
public:
    bool m_bHasToBeDeleted;
    __declspec(thread) static void* m_lastAllocated;
public:
#ifdef _DEBUG
    static void* operator new(size_t size, LPCSTR file, int line) { return internalNew(size, file, line); }
    static void operator delete(void* pData, LPCSTR file, int line) { internalDelete(pData, file, line); }
#else
    static void* operator new(size_t size) { return internalNew(size); }
    static void operator delete(void* pData) { internalDelete(pData); }
#endif
public:
    CTestNEW();
public:
#ifdef _DEBUG
    static void* internalNew(size_t size, LPCSTR file, int line)
    {
        CTestNEW* ret = (CTestNEW*)::operator new(size, file, line);
        m_lastAllocated = ret;
        return ret;
    }

    static void internalDelete(void* pData, LPCSTR file, int line)
    {
        ::operator delete(pData, file, line);
    }
#else
    static void* internalNew(size_t size)
    {
        CTestNEW* ret = (CTestNEW*)::operator new(size);
        return ret;
    }

    static void internalDelete(void* pData)
    {
        ::operator delete(pData);
    }
#endif
};

.CPP

#include "stdafx.h"
.
.
.
#ifdef _DEBUG
#define new DEBUG_NEW
#endif

void* CTestNEW::m_lastAllocated = NULL;
CTestNEW::CTestNEW()
{
    m_bHasToBeDeleted = (this == m_lastAllocated);
    m_lastAllocated = NULL;
}

2

如果pax提出的元问题是“你为什么要这样做”,你可能会得到更详细的答案。

现在假设你是出于“好奇心”而这样做的,可以通过重载new和delete运算符来实现此行为,但不要忘记重载所有 12个变体,包括:

new,delete,new no throw,delete no throw,new array,delete array,new array no throw,delete array no throw,placement new,placement delete,placement new array,placement delete array。

你可以将这个放在一个基类中并从中派生。

这有点麻烦,所以你想要什么不同的行为呢?


1
有一个问题 - 放置新的可以用于来自堆栈和堆的内存。如何区分这两者? - Tadeusz Kopec for Ukraine

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