【问题标题】:How to make a stack grow dynamically?如何使堆栈动态增长?
【发布时间】:2015-03-15 17:38:55
【问题描述】:

这是我的 Stack 类的一个小程序:

 #include <iostream>

using namespace std;

#include <iomanip>

template <typename T>
class Stack
{
private:
    T *stackPtr; 
    int size; 
    T top; 
public:
    Stack(int = 10);
    ~Stack(); 
    bool push(const T  );
    bool pop(); 
    void printStack();
};

int main()
{
    Stack <int> myStack(5);


    cout << "Push 5 elements to stack: ";
    int ct = 0;
    while (ct++ != 5)
    {
        int temp;
        cin >> temp;
        myStack.push(temp);
    }

    myStack.printStack(); 

    cout << "\nErasing two elements:\n";

    myStack.pop(); 
    myStack.pop(); 
    myStack.printStack(); 

    return 0;
}


template <typename T>
Stack<T>::Stack(int s)
{
    size = s > 0 ? s: 10;   
    stackPtr = new T[size]; 
    top = -1; 
}


template <typename T>
Stack<T>::~Stack()
{
    delete [] stackPtr; 
}


template <typename T>
bool Stack<T>::push(const T value)
{
    if (top == size - 1)
        return false; 

    top++;
    stackPtr[top] = value; 

    return true; 
}


template <typename T>
bool Stack<T>::pop()
{
    if (top == - 1)
        return false; 

    stackPtr[top] = 0; 
    top--;

    return true; 
}


template <typename T>
void Stack<T>::printStack()
{
    for (int ix = size -1; ix >= 0; ix--)
        cout << "|" << setw(4) << stackPtr[ix] << endl;
}

所以,为了使用这样的堆栈,我应该在使用前在构造函数中声明它的大小,就像Stack&lt;int&gt; newstack(10),如果我需要 10 个元素。那么,如果我不知道堆栈的最终大小,该怎么办?如何让它动态增长,只是通过向它推送元素?我一直在寻找解决方案,但我的所有想法仍然是计算元素,然后声明一个堆栈以适应元素数量。

【问题讨论】:

  • 用链表代替数组。

标签: c++ class memory heap-memory stack-memory


【解决方案1】:

首先,您知道 C++ 内置了std::stack,不是吗?对于其余的答案,我假设您有理由不使用它(也许这是一个学习练习)。


实现你想要的一个非常天真的方法是:

  1. 在每次添加元素时,使用new[] 分配一个 size + 1 的新数组。
  2. 将旧数组的所有元素和新元素复制到新数组中。
  3. delete[] 旧数组。
  4. 使stackPtr 指向新数组。

除了这个解决方案的所有性能和异常安全缺陷之外,如果您的元素类型T 没有默认构造函数,它怎么可能工作?它甚至不会编译。实际上,您的课程失败了,因为以下T

struct CannotUseInThisStack
{
    CannotUseInThisStack(int) {} // no default constructor
};

Stack<CannotUseInThisStack> s; // error

真正的解决方案是:不要使用new[]delete[]。根据std::vector(或std::deque,这正是std::stack 默认所做的!)实现您的堆栈。 std::vector 以更好的方式支持开箱即用的动态增长,无需在每次添加元素时连续重新分配,也无需能够默认构造 T

当然,这理所当然地引出了std::vector 如何做到这一切的问题。

答案是std::vector,或者更确切地说是它的标准分配器std::allocator不是根据new[]delete[] 本身实现的,而是在放置新的方面。内存分配和元素构造是分开的。见std::allocator:allocate。这解决了缺少默认构造函数的问题。首先分配原始内存,然后使用复制构造函数在该原始内存位置构造新元素(在 C++11 中,您还可以完美转发以在适当位置构造 T,但这有点偏离 -主题)。

使用placement new 还可以让std::vector 的容量成倍增长。容器不需要每次添加元素都重新分配内存;它会提前为更多元素分配原始内存(类似于 Christophe 在他的回答中所做的)。仅当超出当前容量时才会重新分配。

有了new[]delete[],这样一个复杂的机制是不可能的。


一般来说,如果您想了解严肃的容器设计,请查看您的编译器如何实现所有 C++ 标准容器

【讨论】:

    【解决方案2】:

    您始终可以使用链接列表来完成此操作。如果要插入堆栈,则只需将一个节点放在链表的开头即可。要弹出,您只需将其删除。现在你可能会想如何实现类似 Stack S=new Stack(5); 这样的语句。

    您只需保留一个计数器来保持此值。您可以对这个变量进行任何检查(如堆栈溢出等)。你只需让它像 C++ STL 支持的那样动态化,它就会很容易使用。

    【讨论】:

    • 如果你想要较差的性能(缓存未命中),链表是可行的方法。在实现堆栈时,您可能不希望这样。
    • @JorenHeit 确实性能不会好。但我只是给了他动态增长或收缩的想法。您始终可以使用数组并在填充了 80% 或 70% 时增加它,这显然是一种方法,并且在这种情况下性能会更好。我只是给出了一个实现动态增长的简单方法。
    • 同意。但是,我认为向新手提供非良好实践的解决方案是不明智的。如果您不知道:经验法则是“不使用链表”,几乎从来没有!已经表明,即使您的问题表现为链表问题,向量(或任何其他连续内存解决方案)几乎总是优于链表(例如std::list)。
    • @JorenHeit.:我同意。从下一篇文章开始,我将尝试更加精确和准确。谢谢
    【解决方案3】:

    我想建议使用 std::stack() 不会是您期望的答案 ;-)

    当你达到限制时,你只需要分配更多的空间:

    template <typename T>
    bool Stack<T>::push(const T value)
    {
        if (top == size - 1) {
            //return false; 
            size_t s = size + 10;       // for example increase by 10
            T *ps = new (nothrow) T[s]; // allocate new region
            if (ps==nullptr) 
                return false;           // out of memory
            for (size_t i=0; i<size; i++) 
                ps[i] = stackPtr[i];  
            delete[] stackPtr;  
            stackPtr = ps; 
            size = s;                  // stack is now bigger,  
        }                              // so continue as if there was no problem
    
        top++;
        stackPtr[top] = value; 
    
        return true; 
    }
    

    问题是,要增加多少筹码。您可以像这里一样使用常量方法,但最好考虑一个成员变量并在您的构造函数中对其进行初始化。

    编辑:我已经使用nothrow 的新功能来避免在内存不足的情况下抛出异常,因为您的签名预见到成功的返回码的操作。

    【讨论】:

    • new 将抛出std::bad_alloc,而不是返回nullptr。你应该提到它不是异常安全的。 T 的赋值运算符可以抛出。
    • @ChristianHackl 抱歉;忘记了扔;我会更正
    • 我仍然认为异常安全问题也应该得到解决,至少通过提及它。如果ps[i] = stackPtr[i]; 抛出,就会发生内存泄漏。
    猜你喜欢
    • 2021-12-08
    • 2020-06-15
    • 2020-07-20
    • 2011-03-23
    • 2011-04-04
    • 2012-10-08
    • 1970-01-01
    • 2017-08-12
    • 2011-12-30
    相关资源
    最近更新 更多