【问题标题】:stack overflow during recursion in C++C ++中递归期间的堆栈溢出
【发布时间】:2020-09-23 19:28:44
【问题描述】:

我为单链表编写了一个模板类。为了以相反的顺序打印值,我使用递归实现了 traverse_reverse() 函数。当 list 中的元素数量接近 4000 时,调用该函数会引发堆栈溢出错误。 在这样的数字范围内,我不确定是否应该发生堆栈溢出。

环境为 Visual Studio 2019 社区版,Windows 10 64 位操作系统。

我的问题是:

  1. 如何避免堆栈溢出

  2. 如何在运行时增加堆栈大小。

下面是sn-p的代码:


#pragma once
#include <mutex>
#include <iostream>

namespace MyDS
{
    template <typename T>
    struct Node
    {
        T* m_pData = nullptr;
        Node* m_pNext = nullptr;
    };

    template <class T>
    class sList
    {
        Node<T>* m_pHead = nullptr;
        Node<T>* m_pCurrentNode = nullptr;
        int m_Size = 0;
        std::mutex m_ListMutex;

    public:
        bool insert_front(T val);
        bool insert_last(T val);
        bool insert_anywhere(T val, int loc);

        bool remove(T val);
        //bool remove(int loc);
        bool remove_front();
        bool remove_last();

        void traverse();
        void traverse_reverse();

        bool emptyList();

        int getSize();

    private:
        void traverse_reverse(Node<T>* pNode);
    };

    template<typename T>
    void sList<T>::traverse_reverse(Node<T>* pNode)
    {
        if (pNode->m_pNext != nullptr)
            traverse_reverse(pNode->m_pNext);
        std::cout << *pNode->m_pData << " ";
    }

    template<typename T>
    bool sList<T>::emptyList()
    {
        bool ret = false;
        if (getSize() > 0)
        {
            std::lock_guard<std::mutex> lg(m_ListMutex);

            Node<T>* pTempNode = m_pHead, pTempNode1 = nullptr;
            while (pTempNode->m_pNext!= nullptr)
            {
                pTempNode1 = pTempNode->m_pNext;

                delete pTempNode->m_pData;
                delete pTempNode;
                pTempNode = pTempNode1;
            }

            delete pTempNode->m_pData;
            delete pTempNode;

            pTempNode->m_pData = pTempNode1->m_pData = m_pHead->m_pData = m_pCurrentNode->m_pData = nullptr;
            pTempNode = pTempNode1 = m_pHead = m_pCurrentNode = nullptr;
            m_Size = 0;
        }

        ret = true;
        return ret;
    }

    template<typename T>
    int sList<T>::getSize()
    {
        return m_Size;
    }

    template<typename T>
    bool sList<T>::insert_front(T val)
    {
        Node<T>* pNode = new Node<T>;
        pNode->m_pData = new T(val);

        if (getSize() > 0)
        {
            pNode->m_pNext = m_pHead;
        }

        m_pHead = pNode;
        m_Size++;

        return true;
    }


    template<typename T>
    bool sList<T>::insert_last(T val)
    {
        Node<T>* plastNode = m_pHead;
        while (plastNode->m_pNext!= nullptr)
            plastNode = plastNode->m_pNext;

        plastNode->m_pNext = new Node<T>;
        plastNode->m_pNext->m_pData = new T(val);

        return true;
    }


    template<typename T>
    bool sList<T>::insert_anywhere(T val, int loc)
    {
        return true;
    }


    //template<typename T>
    //bool sList<T>::remove(int loc)
    //{
    //  return true;
    //}


    template<typename T>
    bool sList<T>::remove_front()
    {
        std::lock_guard<std::mutex> lg(m_ListMutex);
        Node<T>* pNode = m_pHead;
        m_pHead = m_pHead->m_pNext;

        delete pNode->m_pData;
        delete pNode;

        m_Size--;
        return true;
    }

    template<typename T>
    bool sList<T>::remove_last()
    {
        Node<T>* plastNode = m_pHead;
        std::lock_guard<std::mutex> lg(m_ListMutex);
        if (getSize() > 1)
        {
            while (plastNode->m_pNext->m_pNext != nullptr)
                plastNode = plastNode->m_pNext;

            Node<T>* pNode = plastNode->m_pNext;
            plastNode->m_pNext = nullptr;

            delete pNode->m_pData;
            delete pNode;
            pNode->m_pData = pNode = nullptr;
            m_Size--;
        }
        else if(getSize() == 1) // Only 1 node 
        {
            delete m_pHead->m_pData;
            delete m_pHead;

            m_pHead->m_pData = m_pHead = nullptr;
            m_Size--;
        }
        else   // No node available
        {
            //Nothing to do 
        }

        return true;

    }

    template<typename T>
    bool sList<T>::remove(T val)
    {
        bool ret = false;
        Node<T>* pNode = m_pHead;
        Node<T>* pNodeNext = pNode->m_pNext;

        if (pNode->m_pData == val)
        {
            ret = remove_front();
        }
        else if (pNodeNext->m_pData == val)
        {
            pNode->m_pNext = pNodeNext->m_pNext;
            pNodeNext->m_pNext = nullptr;

            delete pNodeNext->m_pData;
            delete pNodeNext;
            pNodeNext->m_pData = pNodeNext = nullptr;
            ret = true;
            m_Size--;
        }
        else
        {
            while (pNodeNext->m_pData != val)
            {
                pNode = pNodeNext;
                pNodeNext = pNodeNext->m_pNext;
            }

            if (pNodeNext == nullptr)
                ret = false;
            else
            {
                pNode->m_pNext = pNodeNext->m_pNext;
                pNodeNext->m_pNext = nullptr;

                delete pNodeNext->m_pData;
                delete pNodeNext;
                pNodeNext->m_pData = pNodeNext = nullptr;
                m_Size--;
                ret = true;
            }
        }

        return ret;
    }

    template<typename T>
    void sList<T>::traverse()
    {
        m_pCurrentNode = m_pHead;
        while (m_pCurrentNode->m_pNext != nullptr)
        {
            std::cout << *m_pCurrentNode->m_pData<<" ";
            m_pCurrentNode = m_pCurrentNode->m_pNext;
        }

        std::cout << *m_pCurrentNode->m_pData;
        std::cout << std::endl;
    }

    template<typename T>
    void sList<T>::traverse_reverse()
    {
        m_pCurrentNode = m_pHead;
        traverse_reverse(m_pCurrentNode);
        std::cout << std::endl;
    }

}

#include "MyDS.h"

int main()
{
    MyDS::sList<int> myList;
    for(int i = 0; i <= 3987; ++i)
        myList.insert_front(i);

    myList.traverse_reverse(); //Recursion

//  myList.traverse();
    return 0;
}

【问题讨论】:

  • 当链表无法正常工作,或者至少在单线程环境中存在基本问题时,为什么要使用mutexes。此外,您应该发布代表minimal reproducible example 的代码。我们不知道链表是​​否正确组合或存在问题,您也没有意识到。
  • 我没有在块的底部看到它。我看到了traverse_reverse(Node&lt;T&gt;* pNode) 的实现。
  • 此外,在 Windows 上,根据您的编译器选项,您的堆栈空间可以小至 512k(默认为 1M -- 我认为),而使用 gcc/clang 您默认有 4M 的堆栈。创建 4000 个重载模板函数的副本可能会耗尽堆栈空间。
  • @PaulMcKenzie:抱歉,如果它看起来不完整。我也会更新并添加剩余的代码。
  • @PaulMcKenzie:该列表运行良好。这个问题只发生在元素的数量上。我已经编译了没有互斥锁的代码,并且发生了同样的问题。

标签: c++


【解决方案1】:

正如其他答案所指出的,您没有提供完整的代码。也就是说,从您给出的代码中猜测,我认为问题在于当元素列表足够长时,由于堆栈上的函数调用过多而发生堆栈溢出是正确的。

一般来说,最好避免在堆栈上进行大量的函数调用。增加堆栈大小通常不是一个好的解决方案。例如,请参阅 why is stack memory size so limited? 以了解有关此主题的一些讨论。

单链表可能很难注册。这。一种选择可能是反转单链表(可能创建一个新的单链表),然后只遍历那个单链表(之后可能删除创建的列表)。双链表可以非常轻松有效地做到这一点,因为您可以找到最后一个元素,然后从那里向后退。

【讨论】:

  • 我已经更新了完整实现的代码 sn-p。我已经经历了多次实现以相反的顺序遍历单链表。这包括使用一些额外的容器,然后显示内容。您说得对,对于逆序遍历,最好使用双向链表。
【解决方案2】:

如果您想避免堆栈溢出,请不要使用递归。一个简单的while 可以完成相同的工作,而无需从堆栈中获取更多资源:

template<typename T>
void sList<T>::traverse_reverse(Node<T>* pNode)
{
    while (pNode != nullptr){
        std::cout << *pNode->m_pData << " ";
        pNode=pNode->m_pNext;
    }
}  

增加堆栈大小:Increase stack size in c++

但是,如果上述代码不起作用,我怀疑您的问题出在其他地方。我最初的猜测是你有一个无限循环(递归)。假设由于某种原因,您的列表中有循环依赖项:每个节点都有他的m_pNext 填充除nullptr 之外的其他内容。递归永远不会结束,因此 stackoverflow 错误。上面的代码不起作用。

循环依赖通常是由插入或删除方法的错误实现引起的。如果由于某种原因您将删除中的指针错误地更新到另一个节点,则可能会导致循环依赖。

您可以使用以下代码检查循环依赖:

template<typename T>
void sList<T>::traverse_reverse(Node<T>* pNode)
{
    Node<T>* org_p=pNode;
    while (pNode->m_pNext != nullptr){
        pNode=pNode->m_pNext;
        if(org_p==pNode){
            std::cout << "Circular Dependency";
            break;            
        }
    }
    std::cout << "No Circular Dependency";
}  

【讨论】:

  • 我不认为这是一个无限循环。这听起来像是真正的堆栈溢出,因为堆栈上有太多的函数调用,相对于输入大小增加堆栈会起作用(尽管像这样增加堆栈通常是错误的方法)。据我了解他的问题,他描述它适用于元素数量比 4000 少得多的单链表的输入。
  • @MelvinWM 这是应该检查的东西。我遇到了太多边缘情况的错误(例如,当接下来的几个节点具有相同的数据值时删除节点)。适用于小输入,但立即改变比例通常会在插入/移除中出现一些他以前没有测试过的边缘情况。我们都同意这个代码我们只能猜测。我只是尝试建议实际测试以排除循环依赖。
  • @MelvinWM 有两个函数名traverse_reverse,他只给出了一个实现。这就是为什么我保留我的建议,即问题可能出在其他地方
  • 我已经更新了完整实现的代码。正如@MelvinWM 所说,我主要关心的是让它处理大量数据。超过 4000 个元素引起了我的关注。虽然函数 traverse() 能够成功运行大量数据,而 traverse_reverse() 由于多次递归而失败。
【解决方案3】:

为了以相反的顺序打印值,我使用递归实现了 traverse_reverse() 函数。

递归(除非您的编译器将optimized 称为tail-recursive calls)总是消耗call stack 空间。另请参阅this draft 报告以获取有趣的GCC 优化示例。可能您的 C++ 编译器能够进行类似的优化。

您可能更喜欢消耗堆空间,例如使用中间标准C++ containers 保存临时数据。

您可能对continuation-passing style 感兴趣。有时它可以让你避免递归(并使用更多的heap memory)。

您可以找到 C++ 实现(想到最近的 GCC 或 Clang...),其源代码 std::vector 或 std::list 是 open source 并且可读。您可能会对它们的复杂性感到惊讶,与rule of five 相关。

如果您使用最近的 GCC 编译您的 C++ 代码,您可以将 used g++ -Wall -Wextra -g -fstack-protector -Wstack-usage=2048 与 -O2 结合使用,以警告大型调用帧。

您可能对 Frama-C++、Coverity、Clang static analyzer 或 address sanitizer 等静态源程序分析工具感兴趣,或者对编写自己的 GCC plugin 来构建调用图和 感兴趣有时检测潜在的堆栈溢出(但请注意Rice's theorem)。另见valgrind。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-01-20
    • 2015-02-08
    • 2020-12-17
    • 2016-11-11
    • 2013-06-16
    • 1970-01-01
    • 1970-01-01
    • 2015-04-04
    相关资源
    最近更新 更多