C++ 中递归期间的堆栈溢出

stack overflow during recursion in C++

本文关键字:堆栈 栈溢出 递归 C++      更新时间:2023-10-16

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

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

我的问题是:

  1. 如何避免堆栈溢出

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

下面是代码片段:


#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;
}

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

通常,最好避免在堆栈上出现大量函数调用。增加堆栈大小通常不是一个好的解决方案。例如,请参阅为什么堆栈内存大小如此有限?有关此主题的一些讨论。

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

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

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

增加堆栈大小:在 C++ 中增加堆栈大小

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

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

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

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";
}  

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

递归(除非编译器作为尾递归调用进行了优化(始终占用调用堆栈空间。另请参阅此报告草案,了解有趣的 GCC 优化示例。您的C++编译器可能能够执行类似的优化。

相反,您可能更喜欢消耗堆空间,例如使用中间标准C++容器来保留临时数据。

您可能会对延续传递样式感兴趣。有时,它使您能够避免递归(并改用更多的堆内存(。

你可以找到C++实现(最近的GCC或Clang浮现在脑海中......(,其源代码std::vector或std::list是开源的和可读的。您可能会对它们的复杂性感到惊讶,这与五法则有关。

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

您可能对静态源程序分析工具感兴趣,例如 Frama-C++、Coverity、Clang 静态分析器或地址清理器,或者编写自己的 GCC 插件来构建调用图,有时还会检测潜在的堆栈溢出(但请注意 Rice 定理(。另见瓦尔格林德。