如何知道指针指向堆还是栈?

31

例子:

bool isHeapPtr(void* ptr)
{
     //...
}

int iStack = 35;
int *ptrStack = &iStack;
bool isHeapPointer1 = isHeapPtr(ptrStack); // Should be false
bool isHeapPointer2 = isHeapPtr(new int(5)); // Should be true
/* I know... it is a memory leak */

为什么需要这样做:

如果我在一个类中有一个成员指针,而我不知道指向的对象是否是新分配的。那么我应该使用这样一个工具来确定是否需要delete指针。

但是:
我的设计还没有完成。因此,我将编写代码以始终执行delete操作。我将避免写垃圾代码。


4
出于好奇,你为什么想要这个? - JSBձոգչ
9
你忘记了第三种可能性:全局变量和静态变量。static int iStatic = 42; int *ptrStatic = &iStatic; - Mark Ransom
1
仅仅因为某个对象是用new分配的内存并不意味着你应该调用delete来释放它。这个对象可能被代码中的其他部分所拥有,这些部分可能会自行释放它,这是不好的。它也可能被其他东西使用,并且调用delete将导致其他东西使用已删除的内存。它可能是指向已分配块的指针,而不是指向已分配块的指针,在这种情况下,delete很可能会破坏你的堆栈,并在以后引起几乎不可能找到的问题。不要这样做。 - David Thornley
2
@Heath:有些问题自然而然地来自于对事物的某些错误思考方式。问Martijn的问题的原因通常是Martijn给出的那个。此外,Neil的答案无论出于何种原因都是正确的:一般情况下无法区分(在许多或大多数实现中可能存在),并且任何需要不可能操作的设计都存在问题。 - David Thornley
5
@Martijn: 你使用了一个被称为智能指针的东西。这些对象可以自动跟踪它们所指向的对象的生命周期。如果你有一个持有指向堆或栈内存的指针的类,那么该类不应该释放该堆/栈内存的内存。你应该在其他地方进行释放操作。 - Puppy
显示剩余4条评论
14个回答

27

这是不可能实现的 - 如果你需要这样做,那么你的设计存在问题。关于为什么不能这样做的讨论,请参考More Effective C++


23
针对过于说教的绝对主义态度给予-1。请参考Carl Norum的回答,了解如何部分满足提问者的需求。 - Heath Hunnicutt
18
StackOverflow已经在回答者获得85.5k声望的情况下告诉提问者他们“做错了”。太棒了。我很好奇为什么Martijn也想这样做,但是为什么要责备他呢?让他自己尝试吧。 - Heath Hunnicutt
19
@Heath:没有什么可以试验的,这是不可能的。这就像有人说“我想画一张没有边缘的正方形”,我们回答“你做不到”,你觉得我们应该让他试验一下。(顺便说一句,他还是可以试验的。)那么他应该做些什么,你希望得到什么样的回答呢?这是一个很好的回答,因为它甚至指向其他资源来解释为什么他无法做到,而这些资源还来自领先的C++程序员。 - GManNickG
10
@Heath 如果你认为我拥有的85K超能力可以阻止Martijn进行实验,那么你高估了我的能力。 - anon
8
@Heath: 但他在第一个短语中是绝对正确的:"没有办法做到这一点"。毫无疑问,没有办法做到OP所要求的事情,结束了。它完全取决于系统。不仅仅是操作系统相关的问题,而是涉及系统相关的问题;即使是运行相同操作系统的不同CPU架构也需要不同的方法来跟踪信息。如此多的组合使得构建这样一个函数几乎不可能—甚至可能无法在运行时收集所需的信息。正确的答案是重新开始。 - Randolpho
显示剩余28条评论

15

总的来说,恐怕你没什么好办法——因为指针可以具有任何值,所以无法区分它们。如果你能从嵌入式操作系统中的任务控制块(TCB)中获得栈的起始地址和大小信息,那么你可能能够做到这一点。大致代码如下:

stackBase = myTCB->stackBase;
stackSize = myTCB->stackSize;

if ((ptrStack < stackBase) && (ptrStack > (stackBase - stackSize)))
    isStackPointer1 = TRUE;

10
非栈即堆并不意味着非堆即栈。 - Heath Hunnicutt
3
@Heath,绝对正确。但是,如果有适当的访问OS结构或链接器定义变量的权限,您可以消除其他非堆区域。这就是我所说的“类似于”的原因。isHeapPointer只是因为OP的术语。正在进行编辑。 - Carl Norum
2
就像编辑一样。一个人肯定可以确定一个地址是来自“堆栈”还是“一个堆栈”。如果一个进程中有多个线程,那么该进程应该检查每个线程的堆栈。 - Heath Hunnicutt
2
在现代操作系统中,“堆栈”不一定要实现为“堆栈数据结构”。我记得读过一篇文章,他们试图通过将堆栈段随机放置在内存中(即作为堆的一部分)来防止堆栈溢出攻击。如果您的操作系统使用了这种技术,那么您就没有运气了。 - Martin York

12
我能想到的唯一“好”的解决方案是为该类重载 operator new 并对其进行跟踪。类似这样(脑部编译代码):
class T {
public:    
    void *operator new(size_t n) {
        void *p = ::operator new(n);
        heap_track().insert(p);
        return p;
    }

    void operator delete(void* p) {
        heap_track().erase(p);
        ::operator delete(p);
    }

private:

    // a function to avoid static initialization order fiasco
    static std::set<void*>& heap_track() {
        static std::set<void*> s_;
        return s_;
    }

public:
    static bool is_heap(void *p) {
        return heap_track().find(p) != heap_track().end();
    }
};

然后你可以做这样的事情:

然后你可以做这样的事情:

T *x = new X;
if(T::is_heap(x)) {
    delete x;
}

然而,我建议不要设计需要您能够询问某些内容是否在堆上分配的程序。


你可能只需要使用 std::set,不需要映射到任何东西。此外,在删除时应该将其移除吗? - GManNickG
好的调用,已更新 :-)。是的,我认为应该在删除时将其删除,因为该地址可能被几乎任何其他类型的对象重复使用。我不认为这会使其功能变差。 - Evan Teran
虽然这回答了“我能删除这个指针吗”的问题,而不是那些没什么用的“它是否指向堆”,但这种方法仍然存在潜在的问题。通常情况下,如果您执行(例如)new T [4],然后执行new X,即使它们是不同类型的,T数组末尾合法(但无法引用)的指针可能具有与动态分配的X指针相同的数值。 - CB Bailey
@Charles Bailey:当然,我想is_heap可以采用T*来稍微增加安全性,但说实话,我认为我们都同意OP正在尝试做一些我们都知道不是一个好主意的事情。毫无疑问,任何解决方案都会有一些缺陷。 - Evan Teran

9
好的,拿出你的汇编书,将指针地址与堆栈指针进行比较:
int64_t x = 0;
asm("movq %%rsp, %0;" : "=r" (x) );
if ( myPtr < x ) {
   ...in heap...
}

现在,x将包含您需要将指针与其进行比较的地址。请注意,它不适用于在另一个线程中分配的内存,因为它将具有自己的堆栈。


3
我认为最好的解决方案是沿着这些线路,但你必须了解堆栈的方向。 - Alexandre C.
2
@Alexandre 是的,这确实是一个试错的过程。它永远不会给你一个令人满意的答案,但会满足你的好奇心并教你一些关于内存布局的知识。 - Gianni
不,这样行不通。栈向较小的地址增长,因此对于任何本地变量地址都将大于ESP。但是对于所有头地址,这个语句也是正确的。 - Andrey
1
@Andrey,就像我在上面的评论中所说的那样,它在许多情况下都不起作用,但是我认为除了全面比较所有堆栈指针和堆栈基址并且对程序在RAM中的布局有着深入的了解之外,没有什么能够解决这个问题。 - Gianni
不,有一种方法可以找到堆栈的顶部和底部,我搜索了一下并找到了它:https://dev59.com/AHA75IYBdhLWcg3wkJ31#3230873 - Andrey
@Andrey 我上面粘贴的代码是我曾经做过的一个测试用例,而且它运行得很好!它能够以100%的准确率检测到我的简单情况。再说一遍,我知道它永远不会百分之百地工作,但还是有趣尝试找出*为什么8它不起作用。 - Gianni

5
即使您可以确定指针是否在一个特定的堆栈或者特定的堆上,对于一个应用程序而言,可能有多个堆和多个栈。
根据提问的原因,每个容器是否“拥有”它持有的指针都有一个严格的策略是非常重要的。毕竟,即使这些指针指向堆分配的内存,其他代码可能还有相同指针的副本。每个指针应该在某一时间只有一个“owner”,虽然所有权可以转移。拥有者要负责销毁。
在极少数情况下,容器跟踪拥有和非拥有的指针很有用 - 可以使用标志或将它们分开存储。但大多数情况下,为任何可以保留指针的对象设置明确的策略更简单。例如,大多数智能指针总是拥有其容器实际指针。
当然,智能指针在这里非常重要 - 如果您想要一个拥有跟踪的指针,我相信你可以找到或编写一个智能指针类型来抽象掉这个麻烦。

4
在主流操作系统中,栈从顶部向下增长,而堆从底部向上增长。因此,你可以基于一些“大”的定义(视情况而定),启发式地检查地址是否超出了该值。例如,在我的64位Linux系统上,以下方法可行:
#include <iostream>

bool isHeapPtr(const void* ptr) {
  return reinterpret_cast<unsigned long long int>(ptr) < 0xffffffffull;
}

int main() {
  int iStack = 35;
  int *ptrStack = &iStack;
  std::cout << isHeapPtr(ptrStack) << std::endl;
  std::cout << isHeapPtr(new int(5)) << std::endl;
}

请注意,这只是一个粗略的启发式算法,可能有一定的趣味性,但不适用于生产代码。

4
关于堆栈的说法可能是正确的,但除此之外还可以有多个堆、多个栈,静态变量又该怎么处理呢? - anon
17
那个常数实在是有点难以念清楚。 - sbi
2
请将您的答案从“现代操作系统”更改为“主流操作系统”。我在多个现代操作系统上工作,您的答案不适用。 - Brian Neal
在主流操作系统中...这与操作系统无关,而是取决于硬件架构:Intel和Sparc向下增长堆栈,但HP的PA向上增长。 - James Kanze
1
当然,即使堆栈向下增长,也不能保证其起始地址位于地址空间的顶部。(例如在Windows下不是。)而且,不同的线程将有不同的堆栈。 - James Kanze

4

这里是针对MSVC的解决方案:

#define isheap(x, res) {   \
void* vesp, *vebp;     \
_asm {mov vesp, esp};   \
_asm {mov vebp, ebp};    \
res = !(x < vebp && x >= vesp); }

int si;

void func()
{
    int i;
    bool b1;
    bool b2;
    isheap(&i, b1); 
    isheap(&si, b2);
    return;
}

虽然不太美观,但是它能够工作。仅适用于局部变量。如果您从调用函数传递堆栈指针,则此宏将返回 true(表示其在堆上)。


也适用于static变量的情况下也返回true。 - undefined

3

尽管有人大声宣称相反,但很明显可以以平台相关的方式实现你想要的功能。然而,仅仅因为某些事情是可能的,并不意味着它自动成为一个好主意。一个简单的规则是堆栈==不删除,否则==删除,这种做法不太可行。

更常见的做法是说,如果我分配了一个缓冲区,那么我必须删除它,如果程序传递给我一个缓冲区,那么删除它不是我的责任。

例如:

class CSomething
{
public:
    CSomething()
    : m_pBuffer(new char[128])
    , m_bDeleteBuffer(true)
    {
    }

    CSomething(const char *pBuffer)
    : m_pBuffer(pBuffer)
    , m_bDeleteBuffer(false)
    {
    }

    ~CSomething()
    {
        if (m_bDeleteBuffer)
            delete [] m_pBuffer;
    }

private:
    const char *m_pBuffer;
    bool        m_bDeleteBuffer;
};

1
可能吗?真的吗?您的库在编译和链接之前就能知道它将链接到的应用程序是否是多线程(多个堆栈)或使用 DLL(多个堆)吗? - user180247
1
你似乎没有理解我的观点。是的,我相信这是可能的,但那已经不重要了。我的主要观点是“不要这样做”。 - Michael J
我理解并同意你的主要观点,但这并不意味着我不能反对你在途中提出的一些小观点。你确实说过“平台相关”,因此额外加分,但即使如此...例如,堆只是一个数据结构——认为它必须由“平台”实现是错误的。即使忽略自定义分配器的问题,还存在多个编译器针对多个DLL的问题,每个DLL都有自己的运行时,因此有自己的堆实现。 - user180247

3

首先,为什么需要了解这个?你试图解决什么实际问题?

我所知道的唯一确定方法是重载全局operator newoperator delete。然后,你可以询问内存管理器一个指针是否属于它(堆)或不属于它(栈或全局数据)。


这是一种确定堆中来自于你自己源代码分配的方法。但它无法帮助你处理来自其他 API 的指针。 - Heath Hunnicutt

2

你正在用一种复杂的方式来尝试解决问题。明确你的设计,让人们清楚谁"拥有"数据,并让代码处理它的生命周期。


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