【问题标题】:Node-based stack class (need peer review)基于节点的堆栈类(需要同行评审)
【发布时间】:2010-11-08 06:52:02
【问题描述】:

我最近根据指令编写了一个基于节点的堆栈类(代码之前的 cmets 中的规范,取自论坛帖子)。我被告知将它发布在这里以供 SO 社区的一位更友好的成员审查,所以就在这里。为简单起见:我将定义与实现放在一起。我了解何时使用头文件 =)

主要是,我想知道我对删除的使用是否合理。在使用析构函数时,我仍然不确定。规范听起来像是我应该删除节点的唯一时间应该是在弹出期间,其他任何事情都是不安全的。我也不明白这里复制构造函数/赋值构造函数的使用。

无论如何,任何关于代码的错误或 cmets 都会很棒。

/*stack class

Background: the specs for this are, verbatim: 

"Write a node-based stack class smile.gif

The stack is one of the most fundamental data structures used in computer science.

A stack has three basic operations:

push(value) - puts a value on the top of the stack
pop() - removes and returns the value that's on the top of the stack
peek() - return (but does not remove) the value off the top of the stack

Before creating the stack, you first have to create a Node class, which is a 
very basic class with just two member variables: the value of the node, and a 
pointer to the previous node in the stack.

Your stack should have only one member variable: the top node of the stack. 
When you push, you add a node with the new value, with it's previous pointer 
pointing towards the current stack top item. When you pop, you delete the top 
node and then set the top of the stack to whatever that node's previous node 
pointer was.

push, pop, and peek must all run in constant time.

You should write it so that it can only push (and pop/peek) ints."
*/

#include <string>
#include <iostream>

class Node
{
    private:
        int value;
        Node* prev;

    public:
        int returnValue() { return value; }
        Node* returnPtr() { return prev; }

        /* constructors and destructors */

        Node(int val, Node* ptrToLast) 
        {
            value = val;            
            prev = ptrToLast; 
        }
};

class Stack
{
    private:
        Node* top;
        int size;

    public:
        Stack() { size = 0; top = NULL; }
        //added this after told the need for a destructor; not sure if it works
        ~Stack() {   
                    while (top != NULL) 
                    {  
                        Node* tempPtr = top.returnPtr();
                        delete top;
                        top = tempPtr;
                    }
                 }     

        Node* returnTopPtr() { return top; }

        void push(int);
        int pop();
        int peek();

        //bonus; figured it might be worth knowing how many
        //nodes are in a given stack 
        int returnSize();
};

int Stack::returnSize()
{
    return size; 
}

void Stack::push(int value)
{ 
    ++size;
    Node* tempPtr = top;
    top = new Node(value, tempPtr); 
}

int Stack::peek()
{
    return top->returnValue();
}


int Stack::pop()
{    
    const std::string throwStr = "You are trying to access/delete a node that doesn't exist. Seriously. ";

    if (size == 0)
    {
        throw(throwStr);
    }

    --size; 

    Node* tempPtr = top->returnPtr();
    int tempVal = top->returnValue();
    delete top;
    top = tempPtr;

    return tempVal;    
}

【问题讨论】:

  • 在我进一步讨论之前快速评论 - 通常因为 Node 是一个您仅在内部使用的“私有”类 - 将数据成员公开并摆脱 getter 函数更容易。
  • 第一个问题:您的规范说唯一的成员变量可以是堆栈中的顶部节点;你也在存储大小。如果您真的想做 size 函数,请遍历堆栈以找到大小,否则您可能会失去该要求。
  • 我们在论坛帖子中讨论过这个问题。我想我应该更新规范。共识是,没有必要:使用 LIFO 设计,堆栈的其余部分无关紧要。主要属性是堆栈是否为空,这就是我在类中使用它的方式。也许使用布尔值会是更好的设计,但这似乎具有相同的成本以获得更多的多功能性。
  • 如果你想要技术,那么不,不是真的:bool 是单字节,int 是 4+ 字节。
  • 为了有效地反驳您的说法,我需要知道:我可以合理地期望堆栈得到多大?如果它变得巨大,那么是的,我认为你是对的。如果没有很多值,我认为成本实际上是相同的。

标签: c++ stack


【解决方案1】:

在 Stack::push() 中,new 可能会失败(内存不足),但您已经增加了大小。这不太可能发生,但会导致状态不一致。

您将 top 初始化为 NULL,因此如果您在推送任何内容之前调用 peek(),您将崩溃。你应该处理那个。如果在调用 push() 之前调用 pop(),也会发生类似的坏事。

考虑使用构造函数初始化列表,例如:

   Node(int val, Node* ptrToLast) : value(val), prev(ptrToLast) {}

【讨论】:

  • 请定义构造函数初始化列表。至于大小,不错的提示。至于将 ptr 分配给 NULL,我认为崩溃(或抛出异常)将是尝试访问其他程序正在使用的内存的更好选择。另外,一旦我纠正了推送功能中的大小问题,我不能只使用大小来知道那里是否至少有一个节点吗?我实际上在流行音乐中这样做,但不是偷看。这不是一个好方法吗?
【解决方案2】:

有许多值得讨论的样式问题 - 但最大的问题是,每当您在类中显式管理动态内存时,您至少需要一个用户定义的析构函数、一个复制构造函数和一个赋值运算符来处理所有动态适当的内存问题 - 否则您将有内存泄漏和未定义的删除。您需要显式定义这些函数的原因是因为您需要一个副本或赋值来复制头指针和后续节点指向的结构,而不仅仅是复制它们指向的地址(这是默认行为编译器提供的实现)-默认析构函数永远不会删除动态分配的内存-这就是为什么您需要定义一个析构函数。

这是一个您可以使用的合理实现 - 但更重要的是,将其与使用向量且根本不需要处理显式内存管理的方法(包含在末尾)进行对比:

类堆栈 { // 客户端不需要知道的嵌套实现类 结构节点 { 节点* 上一个; 整数值; 节点(节点* prev,int value):prev(prev),value(value){} 节点():上一个(0),值(0){} ~Node() { 删除上一个; } // 自己清理 // 递归复制,直到遇到空指针 Node(const Node& o) : value(o.value), prev( prev ? new Node(*prev) : 0 ) { } }; 节点* head_; 整数大小_; 民众: 堆栈():头_(0),大小_(0){} ~Stack() { 删除 head_; } //递归复制直到null Stack(const Stack& o) : head_(o.head_ ? new Node(*o.head_) : 0) { } // 使用复制构造函数进行赋值 栈&运算符=(const Stack& o) { 堆栈副本(o); 节点* cur = head_; 头_ =复制.头_; size_ = 复制.size_; 复制头_ = cur; // 拷贝的析构函数将被删除 返回*这个; } 无效推送(整数值) { head_ = 新节点(head_,值); ++尺寸_; } int peek() 常量 { if (!head_) throw "试图偷看空栈!"; 返回头_->值; } 诠释弹出() { if (!head_) throw "试图弹出一个空栈!"; int ret = head_->值; 节点* cur = head_; // 保留它,这样我们就可以删除它 头_ =头_->上一个; //调整我的指针 当前->上一个= 0; // 如果不设置为 0,则删除所有节点 删除当前; - 尺寸_; 返回 ret; } int size() const { return size_; } }; // -- 一种更简单的方法来编写 ints 堆栈 ;) 结构体 VecStack { std::vector vec; 无效推(int x) { vec.push_back(x); } int peek() 常量 { if(vec.empty()) throw "是空的"; 返回 *--vec.end(); // 你可能更喜欢 vec[vec.size() - 1]; } 诠释弹出() { if (vec.empty()) throw "是空的"; int ret = *--vec.end(); vec.pop_back(); 返回 ret; } int size() const { return vec.size(); } };

【讨论】:

  • 在第二个示例中,peek 不应该完全返回结尾,并且与 pop 相同吗?
  • no - 在 C++ 容器/算法库 (STL) 中,容器总是返回一个超过最后一个元素的迭代器(原因我们稍后会介绍) - 这就是为什么你必须从 (end()) 中减一以访问最后一个元素。
【解决方案3】:

首先,一些在这种情况下不会导致问题的通用 cmets,但在其他情况下可能会出现问题:

您应该只抛出从类std::exception 派生的异常。 C++ 确实允许你抛出任何类型(比如字符串,在你的情况下),但这是一个非常糟糕的主意。

类成员应该使用初始化列表进行初始化,如下所示:(这在其他情况下可能会导致错误。如果不使用初始化列表,则首先默认构造成员,然后使用赋值运算符在构造函数的主体中覆盖它们。并非所有类型都有赋值运算符,或者赋值运算符可能有不良副作用,因此不使用初始化列表可能会有问题)

Node(int val, Node* ptrToLast) : value(val), prev(ptrToLast) {}
Stack() : size(0), top(NULL) {}

给你的函数命名return*我们毫无意义。就叫他们size()topPtr(),或者getSize()getTopPtr()

第二,你没有遵守规则。 ;) 您的堆栈类有两个成员变量,它只允许有一个。 :)

最后,破坏堆栈的事情:

当您尝试取消引用空指针时,这将崩溃:

void test() {
  Stack s;
  s.peek(); // crashes
}

这会泄漏内存,因为分配的节点永远不会被删除(堆栈析构函数应该这样做):

void test() {
  Stack s;
  s.push(1);
}

析构函数应该如下所示:

~Stack() {
  while (top != NULL){
    Node* next = top.returnPtr();
    delete top;
    top = next;
  }
}

这个也应该很有趣:

void test() {
  Stack s;
  s.push(1);
  Stack t(s);
  s.pop();
}

t.returnSize() 现在将返回 1,但 t.top 指向刚刚删除的 s 中的节点。这应该通过为堆栈定义一个复制构造函数和一个赋值运算符来解决(也许也为节点类定义) 复制构造函数如下所示:

Stack(const Stack& s);

如果你初始化一个堆栈从另一个堆栈调用,就像上面一样。 赋值运算符如下所示:

Stack& operator= (const Stack& s);

如果我将一个堆栈分配给另一个堆栈,则在两者都初始化后调用:

Stack s;
Stack t;
t = s; // now both are already initialized, so the assigment operator is used, not the copy constructor

这些函数的作用是确保 t 成为s副本。所以s中的每个节点都应该被复制,并分配给t,以避免它们指向相同的节点。 (顺便说一句,这是您之前关于所有权问题的一个很好的例子。节点应该完全属于一个 Stack 对象。如果它在多个对象之间共享,那么您就有问题了,它变成崩溃只是时间问题)

最后,如果我们想变得更糟:

void test() {
  Stack s;
  s.push(1);
  s.push(2);
}

如果第二个节点的内存分配失败会发生什么(也许我们的内存用完了。不太可能,但它可能会发生)。 您增加大小后会引发异常。现在 s 的大小为 2,即使 top 仍然指向第一个节点 如果你认为这个问题不太可能被认真对待,想象一下你的班级的一个小扩展。假设它是一个模板,因此它可以存储除 int 之外的其他类型。

这意味着每次创建节点时,我们都必须调用值类型的复制构造函数。这也可能引发异常。我们不知道,因为我们不知道用户可能会尝试将哪种类型存储在堆栈中。

“异常安全”的概念很重要,而且真的很难做到正确。基本上,如果抛出异常,您的班级处于哪个状态?它是否仍处于有效状态? (应该总是这样)。它是否丢失了任何数据(对于某些可能无法避免的情况,对于其他情况可以小心避免),如果它丢失了数据,是否已正确删除?调用析构函数,释放内存? (再次重申,应该始终如此)

最后一点是为什么我如此确定你至少会有一个错误。每个人都弄错了异常安全,包括我在内。在 C++ 中编写一个正确 实现像堆栈这样简单的东西是非常困难的。 :)

奖励:

为了回应 cmets 关于复制构造函数、析构函数和 RAII 的问题,让我们完成整个事情: 首先,让我说可能还有一两个我没有发现的错误。 其次,这是我测试过的代码,以下所有代码都通过了。也可以随意通过它运行您自己的代码。 (它应该按原样工作,除非您必须重命名 getSize 函数):(live 变量是我为调试添加的变量。我已经修改了我的 Stack 实现,以便构造函数递增它,而析构函数递减它,只是为了验证构造和破坏的数量是否相等。一旦你确定它有效,显然应该从 Stack 类中删除它

测试代码

static int live; // debugging - keeps track of how many nodes have been allocated. Constructors add 1, destructors subtract. It should end in 0

#include "stack.h"
#include <iostream>
#include <cassert>

int main(){
    {
        // test stack creation + push
        Stack s;
        s.push(1);
        s.push(2);
        s.push(3);
        assert(s.getSize() == 3);

        Stack t;
        t.push(4);
        t.push(5);
        t.push(6);
        assert(t.getSize() == 3);

        // test assigment operator when both stacks contain data
        s = t;
        assert(s.getSize() == 3);
        assert(s.peek() == 6);
        assert(t.peek() == 6);

        Stack u(s);
        // test self assigment when stack contains data
        u = u;
        assert(u.getSize() == 3);
        assert(u.peek() == 6);


        Stack v;
        // test copy construction from stack with data
        Stack w(t);
        assert(w.getSize() == 3);
        assert(w.peek() == 6);
        assert(t.getSize() == 3);
        assert(t.peek() == 6);

        // test assignment operator when source is empty, destination contains data
        w = v;
        assert(w.getSize() == 0);
        assert(v.getSize() == 0);

        // test copy construction from empty stack
        Stack x(v);
        assert(x.getSize() == 0);
        assert(v.getSize() == 0);

        // test pop
        assert(t.pop() == 6);
        assert(t.pop() == 5);
        assert(t.pop() == 4);

        assert(s.pop() == 6);
        assert(s.pop() == 5);
        assert(s.pop() == 4);
    } // at this point, all allocated stacks go out of scope, so their destructors are called, so now is a good time to check for memory leaks:
    assert(live == 0);
}

固定实现

现在,首先是简单的修复。 Stack 类中添加了复制构造函数、赋值运算符和析构函数。如果单独使用 Node 类仍然存在问题,但只要它仅通过 Stack 使用,我们就可以确保节点被正确复制和删除。不幸的是,Stack 现在需要访问Node.tail_ 才能进行复制,所以我将其交为朋友。所以它可以工作,但它并不优雅。

#include <stdexcept> // for std::exception

class Stack;

class Node
{
    private: // changed naming to head/tail, which are commonly used in implementations of linked lists like this. The head is the current element, tail is a pointer to the remainder
        int head_;
        Node* tail_;

    public:
        friend class Stack; // this is necessary for the Stack copy constructor in order to modify the tail pointer after the node is created.
        // the elegant solution had been to define a copy constructor on the Node class as well, but we'll get to that

        int head() const { return head_; }
        Node* tail() const { return tail_; }

        Node(int val, Node* prev) : head_(val), tail_(prev) { ++live; } // use initializer list
        ~Node() { --live; }

        Node(const Node& other) : head_(other.head_), tail_(other.tail_){ ++live; }; // this could be omitted, but I use it to update 'live' for debugging purposes
};

class Stack
{
    private:
        Node* top;
//        int size; // we don't actually need the size at all, according to spec, so I removed it to keep things simple

        bool empty() const { return top == NULL;}

        void freeNodes() { // helper function to avoid duplicate code
            while (!empty()){
                pop();
            }
        }
    public:
        Stack() : top() {} // use initializer list
        ~Stack() { // destructor - the stack is being deleted, make sure to clean up all nodes
            freeNodes();
        }
        Stack(const Stack& other) : top() { // copy constuctor - we're being initialized as a copy of another stack, so make a copy of its contents
            if (other.empty()){
                return;
            }

            top = new Node(*other.top); // copy the first node, to get us started

            Node* otherNext = other.top->tail();            
            Node* current = top;

            while (otherNext != NULL){
                current->tail_ = new Node(*otherNext); // copy the current node
                current = current->tail(); // move to the next node
                otherNext = otherNext->tail();
            }
        }
        Stack& operator= (const Stack& other) {
            if (this == &other){ // If we assign this stack to itself (s = s), bail out early before we screw anything up
                return *this;
            }

            //now create the copy
            try {
                if (other.empty()){
                    freeNodes();
                    top = NULL;
                    return *this;
                }
                // naively, we'd first free our own stack's data before constructing the copy
                // but what happens then if an exception is thrown while creating the copy? We've lost all the current data, so we can't even roll back to a previous state
                // so instead, let's simply construct the copy elsewhere
                // this is almost straight copy/paste from the copy constructor. Should be factored out into a helper function to avoid duplicate code
                Node* newTop = new Node(*other.top); // copy the first node, to get us started

                Node* otherNext = other.top->tail();
                Node* current = newTop;

                while (otherNext != NULL){
                    current->tail_ = new Node(*otherNext); // copy the current node
                    current = current->tail(); // move to the next node
                    otherNext = otherNext->tail();
                }
                // once we're sure that we're able to create the copy of the other stack, we're ready to free the current one
                // this is a bit of duplicate code
                freeNodes();
                top = newTop;
                return *this;
            }
            catch (...){      // if an exception was thrown
                throw;        // and rethrow the exception so the application can deal with it
            }
        }

        // Node* returnTopPtr() { return top; } // not necessary. It's not a required part of the public interface, and class members can just access the top variable directly

        void push(int);
        int pop();
        int peek() const;

        int getSize() const{
            if (empty()){ return 0; }
            int i = 0;
            for (Node* cur = top; cur != NULL; cur = cur->tail_, ++i){}
            return i;
        }
};

void Stack::push(int value)
{ 
    Node* currentTop = top;
    top = new Node(value, currentTop); // this could throw an exception, but if it does, our stack will simply be left unchanged, so that's ok
}

int Stack::peek() const
{
    if (empty()){
        throw std::exception("Stack is empty");
    }
    return top->head();
}

int Stack::pop()
{    
    if (empty()){
        throw std::exception("Stack is empty");
    }

    Node* tail = top->tail();
    int result = top->head();
    delete top;
    top = tail;

    return result;
}

RAII v. 1

RAII 是一项重要技术的糟糕名称。基本思想是每个资源分配(包括但不限于new. 的内存分配)都应该包装在一个类中,该类负责根据需要复制或删除资源。 在我们的例子中,不是让Stack 跟踪所有节点,我们可以通过让Node 类本身完成大部分工作来简化一些事情。现在Node 也被赋予了复制构造函数、赋值运算符和析构函数。堆栈现在只需要跟踪top 节点......几乎。它仍然有点不确定,因为Stack.push 分配了新节点,但Node 现在负责大部分删除。 .但是,它确实让我们摆脱了删除或复制节点列表之前需要的循环。

Stack still needs to access thetail_member ofNode`,但是这一次,我做了一个访问函数而不是让类成为成员。总的来说,更好,但我仍然不满意。

#include <stdexcept>

class Node
{
private:
    int head_;
    Node* tail_;

public:
    int head() const { return head_; }
    Node* tail() const { return tail_; }
    Node*& tail() { return tail_; } // Another way to allow Stack to modify the tail. Needed for pop()


    Node(int val, Node* prev = NULL) : head_(val), tail_(prev) { ++live; }

    ~Node(){ --live; delete tail_; } // it is safe to call delete on a NULL pointer

    Node(const Node& other) : head_(other.head()), tail_(NULL) {
        ++live;
        if (other.tail() == NULL){
            return;
        }
        tail_ = new Node(*other.tail());
    }

    Node& operator= (const Node& other){
        if (this == &other){
            return *this;
        }
        head_ = other.head();
        if (other.tail() != NULL){
            return *this;
        }

        Node* oldTail = tail_;

        try {
            tail_ = new Node(*other.tail());
        }
        catch(...){
            tail_ = oldTail;
            throw;
        }
    }
};

class Stack
{
private:
    Node* top;

    bool empty() const { return top == NULL;}

public:
    Stack() : top() {} 
    ~Stack() {
        delete top;
    }

    Stack(const Stack& other) : top(){
        if (other.empty()){
            return;
        }

        top = new Node(*other.top);
    }

    Stack& operator= (const Stack& other) {
        if (this == &other){
            return *this;
        }

        Node* oldTop = top;

        try {
            top = NULL;
            if (other.top != NULL){
                top = new Node(*other.top);
            }
            delete oldTop;
            return *this;
        }
        catch (...){ 
            delete top;
            top = oldTop;
            throw;  
        }
    }

    void push(int);
    int pop();
    int peek() const;

    int getSize() const{
        if (empty()){ return 0; }
        int i = 0;
        for (Node* cur = top; cur != NULL; cur = cur->tail(), ++i){}
        return i;
    }
};

void Stack::push(int value)
{ 
    Node* currentTop = top;
    top = new Node(value, currentTop);
}

int Stack::peek() const
{
    if (empty()){
        throw std::exception("Stack is empty");
    }
    return top->head();
}

int Stack::pop()
{    
    if (empty()){
        throw std::exception("Stack is empty");
    }

    Node* tail = top->tail();
    int result = top->head();
    if (top != NULL){
        top->tail() = NULL; // detach top from the rest of the list
        delete top;
    }

    top = tail;

    return result;
}

RAII v.2

为了解决上面提到的问题,我决定稍微改变一下我的策略。 Node 现在完成所有繁重的工作,包括推送/弹出/peek 操作。 Stack 只是这些的薄包装。事实证明,这解决了大部分问题。 Stack 不再需要与 Node 的私​​人成员混在一起,我们对所有权有了一些更明确的规则。堆栈拥有顶部节点,每个非顶部节点都归其父节点所有——这一次,所有者 both 创建、复制和销毁该节点。更加一致。

为了实现这一点,我必须在 Node 类上添加一个 isLast 函数,否则,Stack.pop 无法知道是否该删除 top。我对这个解决方案也不是 100% 满意(如果我没有从堆栈中删除 size 成员,我可以用它来解决问题)

但总的来说,这个比上述尝试更干净、更简单。 (这是我唯一花了不到一个小时调试的,一方面。;))

#include <stdexcept>

class Node {
public:
    Node(int value, Node* prev = 0) : head(value), tail(prev) { ++live;}
    ~Node() { 
        --live;
        delete tail;
    }

    Node(const Node& other) : head(other.head), tail(0) {
        ++live;
        if (other.tail != 0){
            tail = new Node(*other.tail);
        }
    }

    Node& operator= (const Node& other){
        if (this == &other){
            return *this;
        }

        Node* oldTail = tail;
        tail = new Node(*other.tail);
        delete oldTail;

        head = other.head;

        return *this;
    }

    void push(int val){
        tail = new Node(head, tail);
        head = val;
    }

    int peek(){
        return head;
    }
    void pop(){
        Node* oldTail = tail;
        head = tail->head;
        tail = tail->tail; // lol
        oldTail->tail = 0;
        delete oldTail;
    }

    bool isLast() { return tail == NULL; }

    int getSize() const{
        int i = 0;
        for (const Node* cur = this; cur != NULL; cur = cur->tail, ++i){}
        return i;
    }


private:
    Node* tail;
    int head;
};

class Stack {
public:
    Stack() : top(){}
    ~Stack() { delete top; }
    Stack(const Stack& other) : top() {
        if (other.empty()){
            return;
        }

        top = new Node(*other.top);
    }

    Stack& operator= (const Stack& other){
        if (this == &other){
            return *this;
        }

        Node* newTop = NULL;
        if (!other.empty()){
            newTop = new Node(*other.top);
        }
        delete top;
        top = newTop;

        return *this;
    }

    void push(int val){
        if (empty()) {
            top = new Node(val);
        }
        else {
            top->push(val); 
        }
    }

    int peek(){
        if (empty()){
            throw std::exception("Empty stack");
        }
        return top->peek();
    }
    int pop(){
        int result = peek();

        if (top->isLast()){
            delete top;
            top = NULL;
        }
        else {
            top->pop();
        }


        return result;
    }

    int getSize() const{
        if (empty()){ return 0; }
        return top->getSize(); 
    }

private:
    bool empty() const { return top == NULL; }
    Node* top;
};

既然所有这一切都是为了向您展示为什么 C++ 不是一个很好的初学者语言,我想我可以肯定地说任务完成了!

:)

【讨论】:

  • 你介意告诉我在这种情况下如何使用析构函数吗?另外,我如何编写代码来防止人们在未来的堆栈中泄漏内存,就像在你的第三个示例中一样?
  • 同意。实际上,我首选的解决方案是让 Node 成为一个 RAII 类,此时 Stack 析构函数将是微不足道的,可以完全省略。而是一步一个脚印。 :)
  • 我个人不喜欢递归构造函数和析构函数。有时您的集合大于可用的堆栈空间。 RAII 通常是处理资源的正确方式,但对于链表我认为你必须确保迭代复制和析构函数。
  • 没错,使用 STL 可以非常简单地完成。它不是一个很好的初学者语言的原因是您可以轻松地创建一些 似乎 工作但仍然包含许多细微错误的东西,就像我说明的那样。 :)
  • 真正的问题是,要求有多现实?如果您是一个完全的编程初学者,那么在您有足够的经验之前,可能无需尝试编写这样的类就可以学习 C++。可能是。问题是,如果您从任何其他编程语言使用 C++,您要么尝试资源管理并弄错(来自 C),要么期望资源神奇地被清理(来自 Java)。无论哪种方式,您最终都可能尝试编写这样的类。
【解决方案4】:

这里有一个小建议 - 这段代码:

Node* tempPtr = top;
top = new Node(value, tempPtr);

可以替换为

top = new Node(value, top); 

除非您希望额外的赋值语句使代码更清晰。如果是这种情况,您可以这样做:

Node* oldTopPtr = top;
top = new Node(value, oldTopPtr);  

【讨论】:

  • 是的,为了清楚起见,我想要它,但我想我选择了一个意义不大的名称来实现这一点。
【解决方案5】:

好的,这里有一个快速回顾。请记住,某些内容将是我的个人意见(就像我写的评论一样。)

1 - 每当访问一个有指针的值或方法时,首先检查指针是否有效!否则,这将导致您出现段错误。例如,如果您在推入节点之前先查看,则调用 NULL->returnValue()。这不好。

2 - 您不需要在推送中使用的临时指针,您应该检查是否能够成功分配内存。

3 - 您需要一个复制构造函数/析构函数,因为您的对象管理动态分配的数据。因此,默认情况下,c++ 仅在复制对象时复制您的静态值,而在破坏对象时仅为静态变量释放内存。复制构造函数和析构函数确保您通过动态内存并处理它。 (即:对于要删除每个节点的析构函数。)

4 - returnTopPointer 是一个可怕的想法 - 它使人们可以访问您的内部数据并让他们为所欲为。

如果您在复制构造函数和析构函数方面需要帮助,请告诉我们。

【讨论】:

  • 我在第一篇文章中添加了析构函数,欢迎提出想法。我的主要问题是我不知道什么时候会调用析构函数,所以我怎么知道要写什么?我不明白复制构造函数到底是什么,或者赋值构造函数是什么。这些定义应该让我开始。如果我需要更多帮助,我会问。顺便说一句:我不知道 returnTopPtr 是如何进行剪辑的——我的“等级代码”(最终代码)已将其删除。他也向我解释了为什么这是一个坏主意。
  • 哎呀,这些话题可能太大了,无法发表评论。我在我的答案中添加了关于如何实施它们的简要描述,但没有详细说明。 :)
【解决方案6】:

如果您想要一种稍微不同的方法,我会(可能)这样做。主要区别在于 operator= 的复制和交换习惯用法,我认为其他人没有提到,所以你可能有兴趣看看。如果允许 jalf 要求复制构造函数和 operator=,即使它们不在原始规范中,那么我也可以要求 std::swap ;-)

这通过了 jalf 的测试代码。对于任何喜欢动态类型而不是静态类型的人 - 第一个编译、通过的版本;-)。

我只使用了有限的 RAII,因为正如我在对 jalf 的回答的评论中提到的,我不想要递归的 con/destructors。有一些地方是“不安全的”,因为某些代码行必须不被抛出,并且是。但是 SafeNode 上的复制构造函数是异常安全的,不需要 try-catch,所以实际上可能抛出的部分被覆盖了。

#include <stdexcept>
#include <algorithm>

class Stack {
    private:
    struct Node {
        Node *prev;
        int value;
        Node(int v, Node *p = 0): value(v), prev(p) { ++live; }
        ~Node() { --live; }
    };

    public:
    Stack() : top(0), size(0) { }
    Stack &operator=(const Stack &rhs) {
        if (this != &rhs) {
            Stack s(rhs);
            swap(s);
        }
        return *this;
    }

    public:
    void push(int value) {
        top.node = new Node(value, top.node);
        ++size;
    }

    int pop() {
        // get node and value at the top of the stack
        Node *thisnode = top.get();
        int retval = thisnode->value;

        // remove top node from the stack and delete it
        top.node = thisnode->prev;
        --size;
        delete thisnode;

        return retval;
    }

    int peek() const {
        return top.get()->value;
    }

    size_t getSize() {
        return size;
    }

    void swap(Stack &rhs) {
        top.swap(rhs.top);
        std::swap(size, rhs.size);
    }

    private:
    struct SafeNode {
        Node *node;
        SafeNode(Node *n) : node(n) {}
        SafeNode(const SafeNode &rhs_) : node(0) {
            const Node *rhs = rhs_.node;
            if (rhs == 0) return;
            SafeNode top(new Node(rhs->value));
            Node *thisnode = top.node;
            while(rhs = rhs->prev) {
                thisnode->prev = new Node(rhs->value);
                thisnode = thisnode->prev;
            }
            swap(top);
        }
        ~SafeNode() {
            while (node != 0) {
                Node *nextnode = node->prev;
                delete node;
                node = nextnode;
            }
        }
        void swap(SafeNode &rhs) { std::swap(node, rhs.node); }
        Node *get() const {
            if (node == 0) throw std::logic_error("Empty stack");
            return node;
        }
        private: SafeNode &operator=(const SafeNode &);
    };

    private:
    SafeNode top;
    size_t size;

};

namespace std {
    template <>
    void swap<Stack>(Stack &lhs, Stack &rhs) {
        lhs.swap(rhs);
    }
}

【讨论】:

  • 好的,这有点难以解读。例如,在 pop 中,您删除了 thisnode - 但它似乎不是动态的。你如何删除它?
  • “变量活在哪里?”。它在 jalf 的测试代码中,我没有在这里重新发布。很好发现:-)
  • 在pop中,这个节点是动态的。它是在推送中(或在复制期间)分配的,并隐藏在 SafeNode 中以确保它会在析构函数中被删除。然后 top.get() 返回它。 "top.node = thisnode->prev" 行从列表中删除了这个节点,所以我们必须删除它。
【解决方案7】:

在从答案中吸取了一些教训,开发了一种 getter 函数风格,制作了适当的复制 ctor 和 dtor 之后,我认为这个最新的代码比我的第一次尝试要好得多。

这里有一些不那么糟糕、更好的内存管理代码:

/*stack class

Background: the specs for this are, verbatim: 

"Write a node-based stack class smile.gif

The stack is one of the most fundamental data structures used in computer science.

A stack has three basic operations:

push(value) - puts a value on the top of the stack
pop() - removes and returns the value that's on the top of the stack
peek() - return (but does not remove) the value off the top of the stack

Before creating the stack, you first have to create a Node class, which is a 
very basic class with just two member variables: the value of the node, and a 
pointer to the previous node in the stack.

Your stack should have only one member variable: the top node of the stack. 

ADDENDUM: also a size variable is allowed.

When you push, you add a node with the new value, with it's previous pointer 
pointing towards the current stack top item. When you pop, you delete the top 
node and then set the top of the stack to whatever that node's previous node 
pointer was.

push, pop, and peek must all run in constant time.

You should write it so that it can only push (and pop/peek) ints."
*/

#include <string>
#include <iostream>


class Stack
{
    private:
        struct Node
        {
            public:
               /* constructors and destructors */        
               Node(int value, Node* prev) : value_(value), prev_(prev) { }
               Node(Node const& other) { value_ = other.value_; prev_ = other.prev_; }
               //there is no ~Node, because the Stack does all the manual management

            /* private data members */
            private:
               /* the value of the node */
               int value_;
               /* a pointer to the previous node on the stack */
               Node* prev_;

            /* getter functions */
            public:
               int value() { return value_; }
               Node* prev() { return prev_; }
        };

    public:
        /* constructors and destructors */
        Stack() : size_(0), top_(0) { }
        ~Stack();     


    private:
        /* pointer to the very top node; important to LIFO phil */
        Node* top_;
        /* size of the stack (main value is whether stack is empty */
        int size_;

    public: 
        //not for public use
        void setTop(Node *top) { top_  = top;  }
        void setSize(int size) { size_ = size; }
        Node* top() { return top_;  }
        int size()  { return size_; }


    public:
        /* insertion, deletion, and traversal functions */
        void push(int);
        int pop();
        int peek();
};

Stack::~Stack() 
{ 
    while (top() != NULL)
    { 
        Node* tempPtr = top()->prev();
        delete top_;
        setTop(tempPtr);
    }
} 

void Stack::push(int value)
{ 
    setSize(size() + 1);
    Node *newTop = new Node(value, top());
    setTop(newTop);
}

int Stack::peek()
{
    return top()->value();
}


int Stack::pop()
{    
    if (size() == 0)
    {
        throw; //up
    }

    setSize(size() - 1);

    Node* tempPtr = top()->prev();
    int tempVal = top()->value();
    delete top();
    setTop(tempPtr);

    return tempVal;    
}

【讨论】:

    【解决方案8】:
    • push(value) - 将一个值放在栈顶
    • pop() - 移除并返回栈顶的值
    • peek() - 返回(但不删除)堆栈顶部的值

    【讨论】:

    • 我不确定这如何回答这个问题。
    猜你喜欢
    • 1970-01-01
    • 2015-04-03
    • 2015-04-15
    • 1970-01-01
    • 1970-01-01
    • 2019-07-23
    • 2011-04-01
    • 2020-03-23
    • 1970-01-01
    相关资源
    最近更新 更多