C++ 类可以确定它是在堆栈上还是在堆上?

Posted

技术标签:

【中文标题】C++ 类可以确定它是在堆栈上还是在堆上?【英文标题】:Can a C++ class determine whether it's on the stack or heap? 【发布时间】:2011-01-04 12:17:09 【问题描述】:

我有

class Foo 
....

有没有办法让Foo能够分离出来:

function blah() 
  Foo foo; // on the stack

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

我希望 Foo 能够根据它是在堆栈上还是在堆上分配来做不同的事情。

编辑:

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

答案:

我现在正在使用 ref-counted GC。但是,我也希望有能力进行标记和扫描。为此,我需要标记一组“根”指针——这些是堆栈上的指针。因此,对于每个类,我想知道它们是在堆栈中还是在堆中。

【问题讨论】:

分离出来,如in?恕我直言,静态分配是在堆栈上完成的,而像“new”这样的分配将在堆上完成。 为什么需要区分它们,用例是什么? 这个问题能直接回答吗?不管这个人是否知道他在做什么,这对于我们这些真正需要它的人来说可能很有用。 在茫然困惑中翻白眼:-( @Anacrolix;它不能便携,它不会告诉你任何有用的东西,如果你认为你需要它,那你几乎肯定是错的。 【参考方案1】:

为你的班级重载 new()。这样您就可以区分堆和堆栈分配,但不能区分堆栈和静态/全局。

【讨论】:

当你的类的实例是另一个类的非静态成员时,这也会给你带来麻烦。【参考方案2】:

我不肯定你在问什么,但覆盖 new 运算符可能是你想要做的。由于在 C++ 中在堆上创建对象的唯一安全方法是使用 new 运算符,因此您可以区分存在于堆上的对象与其他形式的内存。谷歌“在 c++ 中重载新的”以获取更多信息。

但是,您应该考虑在类内部区分这两种类型的内存是否真的有必要。如果您不小心,根据对象的存储位置,对象的行为会有所不同,这听起来像是灾难的秘诀!

【讨论】:

不一定正确。考虑这些对象的向量。 vector 的数据可能是从堆中分配的,但该对象从未调用过 new。 在vector中构造对象调用placement new来构造对象。现在我不确定这是否意味着您还需要提供一个新的展示位置......以前不必深入挖掘。 放置-new 无法替换。也就是说,vector使用placement-new。 (或容器,就此而言。)他们调用分配器的construct 方法。 (通常称为placement-new。:P) 关于向量的好点,虽然我认为你的意思是数组?可以通过将默认构造函数设为私有来禁止数组中的分配,但这很难看——尤其是如果对象在其构造函数中不需要参数。【参考方案3】:

您需要真正向我们提出真正的问题(a) :-)可能很清楚为什么您认为这是有必要,但几乎可以肯定不是。事实上,这几乎总是一个坏主意。换句话说,为什么你认为你需要这样做?

我通常发现这是因为开发人员希望根据分配的位置删除或不删除对象,但这通常应该留给代码的客户端而不是代码本身。


更新:

既然您已经在问题中阐明了您的原因,我很抱歉,您可能已经找到了您所问的少数几个领域之一(运行您自己的垃圾收集进程)。理想情况下,您应该覆盖所有内存分配和取消分配运算符,以跟踪堆中创建和删除的内容。

但是,我不确定拦截类的 new/delete 是否简单,因为可能存在不调用 delete 的情况,并且由于 mark/sweep 依赖于引用计数,因此您需要能够拦截指针分配以使其正常工作。

你有没有想过你将如何处理它?

经典例子:

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 problem。很多时候,人们会提出一个预先假定有一类解决方案的问题,而更好的方法只是描述问题,没有先入为主地知道最佳解决方案是什么。

【讨论】:

在用户土地标记/清除垃圾收集器中,我希望提供某种智能指针来包含指向可收集对象的指针(实际上,这提供了准确的标记)。因此,您的代码 sn-ps 不合法,因为它们仅使用非 gc 原始指针引用了 gc 对象。 “编译器”实现可能会使用保守标记并直接分析堆栈。 重载 new 并不完全可靠。您可以 malloc() 一个缓冲区并将其放置新(或只是简单地转换)到一个类。那看起来仍然像一个基于堆栈的类,但它在堆上。 IMO 你不能垃圾收集用 new 创建的东西:你需要自己的分配和指针包装器。 我计划将它与引用计数的智能指针一起使用。重载了创建、操作符=和析构函数。上面的例子最终会变成这样: MyObject::Ptr x = new MyObject(); x = 0; // operator = 的重载导致 x 进行 ref 递减,这会触发析构函数。 您应该尝试boost::shared_ptr,以获得更规范且经过测试的引用计数实现。 @GManNickG 或者,在 C++11 中,std::shared_ptr 修复了 boost::shared_ptr 的一些问题。【参考方案4】:

一个 hacky 方法:

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;
   
;

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

(肯定有一些系统无法使用)

【讨论】:

我不建议为任何实际任务这样做,只是想到一个有趣的想法。 我没有测试它,但这可能不适用于多线程应用程序。 我也确信他知道你知道他知道并且只是在说。 我实际上是在 2003 年左右尝试过的。不幸的是,它无法在其中一个系统上运行,几乎是任何开启了优化的 C++ 编译器。 这不适用于任何现代系统,即任何支持线程的系统。【参考方案5】:

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

现在假设您这样做是出于“一个很好的理由”(也许只是出于好奇)可以通过覆盖操作符 new 和 delete 来获得这种行为,但不要忘记覆盖 所有 12 个变体,包括:

新建,删除,新建不抛出,删除不抛出,新数组,删除数组,新数组不抛出,删除数组不抛出,放置新,放置删除,放置新数组,放置删除数组。

您可以做的一件事是把它放在一个基类中并从它派生出来。

这有点痛苦,所以你想要什么不同的行为?

【讨论】:

有一个问题 - 放置 new 可以从堆栈和堆中用于内存。这个怎么区分?【参考方案6】:

如上所述,您需要通过重载的 new 运算符控制对象的分配方式。但是要注意两件事,首先是“placement new”运算符,它在用户预分配的内存缓冲区中初始化您的对象;其次,没有什么能阻止用户简单地将任意内存缓冲区转换为您的对象类型:

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

另一种方式是,大多数运行时使用的内存比进行堆分配时要求的多一点。他们通常在那里放置一些服务结构,以通过指针识别正确的释放。您可以检查这些模式的运行时实现,尽管这会使您的代码真的不可移植、危险且不可支持的矫枉过正。

同样,如上所述,当您应该询问您设计此解决方案的初始问题(“为什么”)时,您实际上是在询问解决方案的详细信息(“如何”)。

【讨论】:

【参考方案7】:

一种更直接、侵入性更小的方法是在内存区域映射中查找指针(例如/proc/&lt;pid&gt;/maps)。每个线程都有一个分配给它的堆栈的区域。静态和全局变量将存在于 .bss section 中,常量将存在于 rodata 或 const 段中,等等。

【讨论】:

【参考方案8】:

答案是否定的,没有标准/便携的方法可以做到这一点。涉及重载新运算符的黑客往往有漏洞。依赖于检查指针地址的黑客是特定于操作系统和堆实现的,并且可能会随着操作系统的未来版本而改变。您可能对此感到满意,但我不会围绕这种行为构建任何类型的系统。

我会开始寻找不同的方法来实现你的目标——也许你可以有一个完全不同的类型作为你的方案中的“根”,或者要求用户(正确地)注释堆栈分配的类型一个特殊的构造函数。

【讨论】:

new hack 不可靠性:你怎么知道新调用的放置会将对象放在堆栈还是堆上? 问题是“如何”做到这一点,而不是“如何标准/便携地做到这一点”。【参考方案9】:

不,它不能可靠或明智地完成。

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

但是如果对象被构造为类成员,并且拥有的类是在堆上分配的呢?

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

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

静态/全局对象呢?除了堆栈/堆之外,您如何区分它们?

您可以查看对象的地址,并使用它来确定它是否在定义堆栈的范围内。但堆栈可能会在运行时调整大小。

所以说真的,最好的答案是“有一个原因为什么标记和清除 GC 不与 C++ 一起使用”。 如果您想要一个合适的垃圾收集器,请使用另一种支持它的语言。

另一方面,大多数有经验的 C++ 程序员发现,当您学习必要的资源管理技术 (RAII) 后,对垃圾收集器的需求几乎消失了。

【讨论】:

【参考方案10】:

如果您将“this”的值与堆栈指针的当前值进行比较,这是可能的。如果这个

试试这个(在 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 下使用它时遇到问题。谢谢。【参考方案11】:

我建议改用智能指针。按照设计,类应该有关于类的数据和信息。记账任务应该委派给课外。

重载 new 和 delete 会导致比你想象的更多的漏洞。

【讨论】:

【参考方案12】:

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;

【讨论】:

【参考方案13】:

要回答您的问题,一种可靠的方法(假设您的应用程序没有使用多个线程),假设您的智能指针不包含的所有内容不在堆上:

-> 重载new,这样你就可以存储所有分配的块的列表,以及每个块的大小。 -> 当你的智能指针的构造函数时,搜索你的 this 指针所属的块。如果它不在任何块中,您可以说它“在堆栈上”(实际上,这意味着它不是由您管理的)。否则,您知道分配指针的位置和时间(如果您不想寻找孤立指针和缓慢释放内存,或类似的东西..) 它不依赖于架构。

【讨论】:

这是正确的想法,但您可能还需要担心标准分配器以及新的分配器。如果你的类包含一个向量,你需要知道它的存储也被跟踪。标准分配器使用 ::operator new 所以你可以重新定义它并完成。【参考方案14】:

在此处查看程序:http://alumni.cs.ucr.edu/~saha/stuff/memaddr.html。通过几次演员,它输出:

        Address of main: 0x401090
        Address of afunc: 0x401204
Stack Locations:
        Stack level 1: address of stack_var: 0x28ac34
        Stack level 2: address of stack_var: 0x28ac14
        Start of alloca()'ed array: 0x28ac20
        End of alloca()'ed array: 0x28ac3f
Data Locations:
        Address of data_var: 0x402000
BSS Locations:
        Address of bss_var: 0x403000
Heap Locations:
        Initial end of heap: 0x20050000
        New end of heap: 0x20050020
        Final end of heap: 0x20050010

【讨论】:

【参考方案15】:

有一个解决方案,但它强制继承。参见 Meyers,“更有效的 C++”,第 27 条。

编辑: Meyers 的建议是由 Ron van der Wal 撰写的 summarized in an article,Meyers 本人在他的博客 (in this post) 中对此进行了链接:

跟踪基于堆的对象

作为全局变量的替代品 方法,Meyers 提出了一个 HeapTracked 类,它使用一个列表来保持 跟踪堆外分配的类实例的地址,然后 使用此信息来确定特定对象是否驻留在 堆。实现是这样的:

class HeapTracked 
  // Class-global list of allocated addresses
  typedef const void *RawAddress;
  static list<RawAddress> addresses;
public:
  // Nested exception class
  class MissingAddress ;

  // Virtual destructor to allow dynamic_cast<>; pure to make
  // class HeapTracked abstract.
  virtual ~HeapTracked()=0;

  // Overloaded operator new and delete
  static void *operator new(size_t sz)
  
    void *ptr=::operator new(sz);
    addresses.push_front(ptr);
    return ptr;
  

  static void operator delete(void *ptr)
  
    // Remove ‘ptr’ from ‘addresses’
    list<RawAddress>::iterator it=find(addresses.begin(),

    addresses.end(), ptr);
    if (it !=addresses.end()) 
      addresses.erase(it);
      ::operator delete(ptr);
     else
      throw MissingAddress();
  

  // Heap check for specific object
  bool isOnHeap() const
  
    // Use dynamic cast to get start of object block
    RawAddress ptr=dynamic_cast<RawAddress>(this);
    // See if it’s in ‘addresses’
    return find(addresses.begin(), addresses.end(), ptr) !=
      addresses.end();
  
;

// Meyers omitted first HeapTracked:: qualifier...
list<HeapTracked::RawAddress> HeapTracked::addresses; 

在原始文章中有更多内容需要阅读:Ron van der Wal 对此建议进行了讨论,然后演示了其他替代堆跟踪方法。

【讨论】:

以上是关于C++ 类可以确定它是在堆栈上还是在堆上?的主要内容,如果未能解决你的问题,请参考以下文章

是应该在堆栈还是堆上分配pthread函数参数?

我应该啥时候在堆上分配? (C++)

对象的私有成员是在堆上还是在栈上?

字符串类型是存储在堆上还是栈上?

c#结构/类堆栈/堆控制?

C++:在堆上创建对象,还是在栈上?