如何识别对象是否在堆栈内存或堆内存中

How to recognize if object is on the stack or heap memory

提问人:Default 提问时间:9/23/2023 最后编辑:trincotDefault 更新时间:11/11/2023 访问量:126

问:

我最近收到了数据结构课程的大学作业,这要求我在 C++ 中创建一个双向链表

在处理我的双向链表时,我需要实现各种功能,但有一种方法特别引起我的注意,叫做“clear()”。此方法负责清除双向链表中的所有元素

void clear(Node* head_ptr)
{
    Node* previous_ptr = nullptr;

    while(head_ptr != nullptr)
    {
        previous_ptr = head_ptr; // Store previous node.

        head_ptr = head_ptr->next;

        delete previous_ptr;
    }
};

该方法非常简单;它只是遍历所有元素并为每个元素释放内存。然后,我在析构函数中调用此方法,如下所示:Node

~List()
{
    clear_list(m_head_ptr);
};

然后我开始思考。如果我的节点元素在堆上,这种释放内存的方法很好,如下所示:

int main()
{
    List list;

    Node* node_1 = new Node(3, nullptr);    // The tail node.
    Node* node_2 = new Node(1, node_1);
    Node* node_3 = new Node(5, node_2);
    Node* node_4 = new Node(7, node_3);     // The head node. 
    
    list.add(node_1);
    list.add(node_2);
    list.add(node_3);
    list.add(node_4);

    // Then do some stuff with the list...

}   // The list goes out of scope and the destructor is called...

但是,一旦我在堆栈上创建节点传递指向堆栈对象的指针,这种情况就会中断,如下所示:

int main()
{
    List list;

    Node* node_1(3, nullptr);    // The tail node.
    Node* node_2(1, node_1);
    Node* node_3(5, node_2);
    Node* node_4(7, node_3);     // The head node. 
    
    list.add(&node_1);
    list.add(&node_2);
    list.add(&node_3);
    list.add(&node_4);

    // Then do some stuff with the list...

}   // The list goes out of scope and the destructor is called and the program crashes because it attempts to free stack objects...

原因是因为我正在尝试释放堆栈对象,这不是一个好主意。当然,我们通常不会使用基于堆栈的 s,因为我们通常希望我们的数据在创建它们的范围之外持久存在。然而,这让我想到了我的问题:NodeNode

我该如何应对?有没有办法检查内存中的某些节点是否在我的函数的堆或堆栈上,然后相应地释放它?或者,有没有更好的方法来解决这个问题?

C++ 列出 堆内存 堆栈内存

评论

8赞 Paul Sanders 9/23/2023
一旦我在堆栈上创建节点,这种情况就会中断,所以不要这样做!
2赞 Louis Go 9/23/2023
编译吗?它应该是相反的。另外,从书单中获取一本关于如何在 C++ 中完成动态内存分配的书会更好。当你这样做时,你要对它负责。如果您不新建它,为什么需要删除它?Node* node_1(3, nullptr);Nodenewdelete
6赞 Raymond Chen 9/23/2023
你的代码不符合 C++ 核心指南,这就是你陷入这种混乱的原因。R.3:原始指针(T*)是非拥有的。但是您的代码使用原始指针来表示有时拥有 () 和有时不拥有(堆栈上的 Node)。它还违反了规则 R.11:避免显式调用 newdelete。您的方法应接受 .new Node()addstd::unique_ptr<Node>
6赞 Raymond Chen 9/23/2023
此外,检测地址是在堆栈上(可能有多个)还是在堆上是错误的问题。 这个“节点”在堆上,但你仍然不应该这样做。实际上,问题是“我如何跟踪谁负责释放此内存?C++ 为此提供了许多技术,例如 和 .struct Bonus { int bonus; Node node; }; Bonus* bonus = new Bonus(); list.add(&bonus->node);deletestd::unique_ptrstd::shared_ptr
2赞 user4581301 9/23/2023
如果专业完成,API 文档将明确说明如果要手动销毁原始指针传递的对象,该对象将如何销毁。如果您正在编写 API,并且您正在传递原始指针,并且用户必须手动管理其生存期,则由您来记录如何操作。

答:

8赞 Jerry Coffin 9/23/2023 #1

最简单、最有效的解决方案通常是让列表本身管理节点的分配。用户甚至不需要知道类型存在,更不用说分配它们了。node

因此,对于用户来说,您在问题中显示的内容的等价物是这样的:

List list;
list.add_head(1);
list.add_head(3);
list.add_head(5);
list.add_head(7);

您通常还可以将项目添加到列表的末尾。每个(head 或 tail)通常都应该返回一个抽象类型(可能是指向节点的指针的包装器),因此您可以执行以下操作:add_tailadditerator

auto pos = list.add_head(7);
list.add_tail(5);
list.add_after(pos, 3);

...这将在开头添加 7,在结尾添加 5,并在 .37

这样,列表本身就会分配所有节点,并知道如何处置它们。您可以更进一步,让它将分配和处置委托给类。这当然是有用的,但可能有点超出了基本练习的意义(在实际使用中,您可能希望使用标准库中的容器 - 虽然标准库确实提供了单链表和双链表,但它们很少有用)。Allocator

评论

0赞 Default 9/23/2023
因此,从本质上讲,解决方案是将 Node 类声明为我的 List 类的私有成员,并禁用从类外部创建单个节点的功能?但是,每个程序员都不断向我建议,将类放在他们的专用文件中是一种首选做法。有没有办法在单独的文件中定义一个类,同时确保它对特定类保持可见和访问?
2赞 user4581301 9/23/2023
@Default,每个文件类是一种很好的、有序的做事方式,但有时它是错误的方法。将标头视为接口的描述,在接口中公开的后端细节越少越好,因为它会降低您将来更改接口的能力。节点类,与使用它的链表类紧密耦合的东西,也可能位于同一个标头中。由于对用户隐藏节点类有很大的好处,因此它应该位于链表类中,并且不会被看到。
1赞 user4581301 9/23/2023
通常还有一种情况是,只在标头中声明节点,并通过将定义放在编译的实现文件中来完全隐藏它,这样除了链表实现之外,没有人可以弄乱它或对它做出假设。但这可以等到您的课程材料达到“不透明指针”和“PIMPL 习语”。
0赞 Jerry Coffin 9/23/2023
@Default:对于这样的情况,标头中唯一可能需要的是节点类的声明,例如:.这足以让你做标头需要做的其他事情 - 主要包含:(可能)。除此之外,还有一个实现细节,所以它可以完全存在于内部(或者,如果你真的愿意,只为它创建一个单独的——但我不认为这是一个胜利。class node;Listnode *head;node *tail;nodeList.cppnode.hpp
1赞 franji1 9/23/2023 #2

由于您现在刚刚学习动态内存,因此您必须从头开始学习(必须有人编写库 - 有一天可能是你!

因此,使用链表的一个原因是,您不知道将提前添加多少个节点。同样,在链表上放置“MAX_SIZE”和基于堆栈的节点数据的实现也不好(MAX_SIZE是否足够大?对于链表的某些客户端来说,它是否太大了?

因此,链表几乎总是使用动态内存实现的。在 C 语言中,这将是 malloc/free。在 C++ 中,这将是 new/delete。最终,客户端代码将利用标准库模板化实现(就像许多人指向你的那样)。

因此,为了回答您的问题,在您的教育阶段,请专门为您的链表节点使用动态内存,使用 new/delete。

你很快就会知道为什么你(最终)会使用标准库实现,因为你必须了解使用/操作手工制作的链表时的所有边缘情况(取消引用已删除的指针、内存泄漏等)。

很多时候,你会从失败中学到更多(例如,如何使用调试器!

评论

0赞 Default 9/23/2023
谢谢你对我的鼓励!
1赞 franji1 9/23/2023
“我最近收到了数据结构课程的大学作业”为我的答案提供了正确的背景。