【问题标题】:memory management for linked list and tree programs in c++c++中链表和树程序的内存管理
【发布时间】:2015-02-27 14:50:12
【问题描述】:

我解决来自 leetcode、黑客排名或破解编码面试等网站的算法问题。如果用 c++ 的问题,我做的最多。所以对于他们中的大多数人来说,我有一个如下的节点结构

struct Node {
   Node* next;
   //for tree
   Node* left;
   Node* right;
   int data; 
   //ctor
   Node(int val) : left(nullptr);.....
}; 

然后我有一个实现算法的函数

bool someAlgorithm(Node* root) {
  //do stuff
}

最后我在主节点中创建节点

int main() {
  auto root = new  Node(4);
  root->left = new .. 
  root->left->left = new ..
}

我想在这种解决方案中加入内存管理。如果我使用 c++11 shared_ptr 是否需要提供析构函数?如果是,我应该在析构函数中写什么?但我发现 shared_ptr 使得代码过于复杂,对于这样的小程序难以理解。

一般来说,解决此类问题记忆安全的最佳方法是什么?

【问题讨论】:

  • std::unique_ptr 可能是比std::shared_ptr 更好的选择
  • 如果shared_ptr 使程序更加变得复杂,那你就做错了。智能指针为您必须自己编写的代码提供了通用实现。所以如果有的话,这个类应该变得更简单。
  • 除非节点实际上可以在树之间共享,否则shared_ptr 是错误的方法。 unique_ptr 可能更有用。

标签: c++ c++11 memory-management


【解决方案1】:

解决这样一个简单的内存安全问题的最好方法是在节点中添加一个析构函数,并在程序结束时删除根节点。因为 root 是在堆栈上分配的,所以当前最后会有内存泄漏。

这就是定义的样子。

~Node() {
    //call delete on every pointer in the struct
    delete next;
    delete left;
    delete right;
}

然后在您的程序结束时,您可以调用delete root,然后将调用 dtor,递归地删除它下面的每个节点。即使您使用shared_ptrunique_ptr 而不是调用delete,您仍然需要dtor,否则在删除root 时您的所有子节点将保持分配状态。

【讨论】:

  • 我找到了一种方法,将左、右、下一个指针本身设为 unique_ptr 。因此,如果根超出范围并被删除,它将自动删除其他节点,因此我不必编写 dtor。这看起来对吗?
  • 只要 root 是 unique_ptr 或者你在最后删除它,它就可以工作。
  • 如果这样做,您还应该将Node的复制语义(复制构造函数和复制赋值运算符)标记为deleted。如果没有,当你复制Nodes 然后删除它们时会发生一些不好的事情。如果您在 Node 对象中使用 unique_ptrs,复制语义将被自动删除,因为 unique_ptr 没有复制语义。
【解决方案2】:

如果您需要将节点删除操作作为算法的一部分,则必须自己实现。没有任何技巧可以帮助您避免它。

删除所有节点是一项非常简单的任务,因为它只需要另一个遍历树结构,就像您在 someAlgorithm() 执行期间可能所做的那样。

所以,我相信即使选择手动删除,整体难度也不会有太大变化。

【讨论】:

    【解决方案3】:

    如果你想要内存安全,首先要意识到new创建的对象是一种资源,即系统中有限数量的东西,不属于你的程序,你的程序必须借用(通过new 在您的情况下),并回馈(通过delete 在您的情况下)。

    C++11 中的资源管理将此借用/还给对映射到构造函数/析构函数对。通过其构造函数借用资源的对象被称为OWN此资源,并负责将其归还给系统。

    除了构造函数和析构函数之外,还有其他 4 个特殊函数参与所有权:

    • 复制构造函数:当一个对象被复制时,你必须决定资源是否可以被两个结果对象所拥有,或者资源应该被复制,或者如果复制资源是如此无意义/不可行,那么复制对象应该被禁止。
    • 移动构造器:掠夺资源!一个对象会死。它可能是一个临时的未命名对象,一个即将从函数返回的本地对象,或者程序员认为它很快就会死掉的对象(通过使用std::move),无论如何,该对象将不需要其宝贵的、昂贵的借来资源。因此,新对象可以成为一个成熟的对象,方法是取得即将死亡对象的资源的所有权,使其成为自己的,从而将资源的所有权转移给新对象。
    • 复制分配:分配对象丢弃(归还)其当前拥有的资源,并用右侧对象的资源副本替换它们。做什么决定与复制构造函数完全一致。
    • 移动分配。与复制分配相同,但窃取而不是复制。与移动构造函数密切相关。

    因此,如果你读得太快,你可能会读到你必须重新实现所有 6 个。但是这种资源管理在 C++11 中无处不在,特殊功能的自动生成以一种简单明了的方式进行,因此:

    • 复制/移动构造函数分别自动生成。复制/移动对象的所有成员。因此,如果任何成员无法复制/移动,则不会生成相应的特殊功能。
    • 析构函数会破坏所有成员对象。所以当一个对象死亡时,所有成员的资源释放逻辑都会被执行。

    C++11 提供了一些特殊的类来管理单个资源:

    • std::unique_ptr 拥有一个资源。它拥有资源,所以如果它在销毁时仍然拥有它,它将释放它。您不能复制资源,因此不允许复制它。但资源可以通过移动转手。 (战利品!战利品!战利品!)它保证资源只有一个所有者。
    • std::shared_ptr 让许多对象共享单个资源的所有权。您可以复制它,在这种情况下您复制的是资源的所有权。简而言之,它在资源上维护一个适当的计数器来跟踪有多少指针拥有它。

    如果您使用这些类的实例来管理分配的内存(作为类的成员或作为本地变量),那么确定性资源释放将自动发生。

    【讨论】:

      猜你喜欢
      • 2020-08-07
      • 1970-01-01
      • 2011-09-16
      • 1970-01-01
      • 1970-01-01
      • 2020-10-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多