【问题标题】:Recursive memory "tree": properly freeing memory递归内存“树”:正确释放内存
【发布时间】:2020-11-28 03:56:50
【问题描述】:

刚上完第一堂 CS 课,我想更多地练习递归内存分配,所以我决定制作一个名为“Recursive Dungeon”的小游戏。它只是允许用户通过在每次玩家进入空(NULL)房间时递归生成一个新“房间”来在无限地牢中漫游。

然后新房间被保存,并且可以通过...访问。

  1. 沿着进入房间时使用的完全相同的路径前进
  2. 如果您离开房间,请原路返回同一房间

*该程序不使用循环(至少,我认为不会)。我不熟悉循环的概念以及如何使用它。

我遇到的问题是,当我尝试清理所有递归分配的内存(“房间”)时,我收到经典错误“分段错误:核心转储”。

下面是我的结构“房间”:


    struct room
    {//begin struct

        room* backward;
        room* left;
        room* forward;
        room* right;

        string desc;
  
        room() { //begin
    
            backward = NULL;
            left = NULL;
            forward = NULL;
            right = NULL;
    
        /*End*/}
 
    /*End struct*/};

每个“房间”都与其他房间相连(左/右/前/后)。用户从空房间指针“起点”开始,并且可以朝上述任何方向前进。在尝试进入空(NULL)房间时,会随机生成一个新房间供用户进入。

一旦玩家对探索感到满意,我会尝试在结束程序之前使用一个存储所有房间的数组来清理分配的内存。相反,它会导致分段错误。代码如下:

    void ClearAllocatedMemory(room* aRoom, room** roomArray, int& raIndex) {

        for(short i=0; i<raIndex; i++) {//begin for
    
            delete roomArray[i];
    
        /*End for*/}
    
        delete[] roomArray;

    /*End func*/}

这是创建我的数组并定义其第一个(第 0 个)索引的代码:


    room** roomArray;
    int raIndex = 0;

    room* startingpoint = new room();
    roomArray[0] = startingpoint;

这是在新房间中添加到roomArray 索引的代码:


    room* GenRoom(room** roomArray, int& raIndex) {

        room* newroom = new room();
    
        newroom->desc = GenRoomDesc( rand()%12 + 1 );
    
        raIndex++;
    
        roomArray[raIndex] = newroom;

        return newroom;


    }

【问题讨论】:

  • "cmets 关于如何...提高清晰度/格式" 第一步:从代码中删除空行,它们占用大量空间。
  • 你能到达你已经住过的房间吗?您可以在不回溯步骤的情况下这样做吗(即图中是否有任何循环)?
  • 我想room-&gt;left-&gt;right 就是这样一个循环,这就是所提供的代码有问题。
  • @john 我的意思是像 left->left->left->left 这样的非平凡循环
  • 我认为递归和手动内存管理并不是最好的方法。您应该将房间存储在一个数据结构中,这样您就可以有效地执行所需的操作。我不明白你为什么要使用递归,因为这个问题本质上不是递归的。

标签: c++ recursion memory segmentation-fault c++17


【解决方案1】:

做更多的研究,回顾 stackheap 内存,并阅读这个article- 我想我可能已经在上面的代码中找到了问题。

在更改了清理内存的方式(从使用递归到使用 @Quimby 建议的数组)之后,我实现了一个 room** 数组来保存所有房间。我遇到的问题是我试图删除不应该删除的房间**:房间**是从 stack 而不是 heap 分配的 因为我没有使用关键字new 来创建它。

因此,要修复我的代码,我只需从我的函数中取出 delete[] roomArray;

【讨论】:

    【解决方案2】:

    即使双重删除没问题,您的算法也不起作用。 如果您有两个相互连接的房间,它们将递归地尝试相互删除,从而导致堆栈溢出。 IE。你不能用简单的递归删除循环。

    您有一个设计问题 - 谁拥有每个房间?如果这是用垃圾收集的语言写的,你不会在意,房间会因为它们自己的存在而相互拥有。在 C++ 中,您必须关心并且设计应该反映这一点。

    shared_ptrstd::weak_ptr 在这种情况下会很混乱。即使您可以建立树状层次结构并因此使用unique_ptr,也可能由于深层树的嵌套析构函数而导致堆栈溢出。

    最好和最简单的解决方案是创建一个std::vector&lt;Room&gt;,它是所有房间的明确所有者。然后邻居可以在这个向量中使用索引。一个警告是,在向量中心取消分配房间会使较高的索引无效。这可以通过与最后一个元素交换并仅修复它们的连接来解决。它也会从中间 O(1) 中删除。

    如果地图真的是动态的,就像你的情况一样,我可以主张 std::list&lt;Room&gt; - 使用迭代器或邻居指针 - 或 std::vector&lt;std::unique_ptr&lt;Room&gt;&gt; - 使用原始 non-owning 指针.

    这些仅限邻居的解决方案只是一个警告 - 当玩家循环探索房间时,您没有工具来确定该房间是否应该存在。例如。 go 2 up, 2 right, 2down, 2 left 应该让玩家回到初始房间。您可能需要考虑实际使用 2D 网格(稍后再考虑内存/性能)。

    【讨论】:

    • 100% 同意这里的核心问题是不清楚谁或什么拥有这些房间。
    • 我想我明白你所说的双重删除是什么意思,以及为什么需要修复它——我的 ClearAllocatedMemory 函数有时会尝试删除一个房间两次,对吧?另外,感谢您关于将所有房间放置在单个 1D 矢量数组中的建议。这听起来像是一个好主意,它将使事情变得更易于管理。我会尝试实施它并相应地更新我的帖子。
    • @是的,它几乎肯定会删除它两次,因为指向房间的指针总是在至少两个地方,除了地图边缘。试试看:)如果遇到任何问题,您也可以发布新问题。
    【解决方案3】:

    如果你的房间图是一棵普通的树(没有回头路),你的算法就可以工作。由于您可以返回,因此您需要注意不要再次处理您已经在处理的房间。

    我将展示两种方法,一种适用于任意图,另一种仅适用于带有反向链接的树。


    方法1.房间类应该有一个布尔visited标志(不是由玩家访问,而是由耗尽序列访问)。初始值为false。那么你的删除功能应该这样修改:

    ClearAllocatedMemory(room* aRoom) {
       if (aRoom == nullptr || aRoom->visited) return;
       aRoom->visited = true;
       ClearAllocatedMemory(aRoom->backward);
       ClearAllocatedMemory(aRoom->left);
       ClearAllocatedMemory(aRoom->forward);
       ClearAllocatedMemory(aRoom->right);
       delete room;
    }
    

    这实际上将删除过程变成了Depth-first search


    方法 2。房间删除例程应该知道它是从哪个房间到达 ,而不是触及那个房间。

    ClearAllocatedMemory(room* aRoom, room* parent) {
       if (aRoom == nullptr) return;
    
       if (aRoom->backward != parent)   ClearAllocatedMemory(aRoom->backward, aRoom);
       if (aRoom->left     != parent)   ClearAllocatedMemory(aRoom->left,     aRoom);
       if (aRoom->forward  != parent)   ClearAllocatedMemory(aRoom->forward,  aRoom);
       if (aRoom->right    != parent)   ClearAllocatedMemory(aRoom->right,    aRoom);
    
       delete room;
     }
    

    这只是正常的从上到下的树遍历,添加了防止向上的检查。


    第一种方法可能更可取,因为它是通用的,您可以将删除例程设置为 析构函数,就像在 C++ 中一样。但这是另一个场合。

    还要注意这个检查

    if ( (aRoom->backward == NULL) && (aRoom->left == NULL)\
            && (aRoom->forward == NULL) && (aRoom->right == NULL) ) {
    

    没有多大意义。每个房间都需要被删除,而不仅仅是那些没有传出路径的房间。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-06-23
      • 2023-03-11
      • 1970-01-01
      • 2021-04-21
      • 2014-06-20
      • 2011-05-16
      • 2011-04-30
      • 1970-01-01
      相关资源
      最近更新 更多