在C++中编写堆栈类的各种实现

Writing various implementations of Stack class in C++

本文关键字:实现 堆栈 C++      更新时间:2023-10-16

我最近上了一门Java数据结构课程,我可以编写各种结构,并在Java中以各种方式轻松地实现它们。我目前正在将这些知识转移到C++世界,这有点不同。我目前为 Stack 接口编写了一个头文件(就像你在 Java for Stack 中编写接口一样),我想以各种方式(链表、数组、向量等)实现它,这样我就可以掌握用任何语言实现结构的想法。我目前遇到的C++问题是理解在我的 Stack 和引用 (E&) 中使用指针的概念,并确保我可以编写 Stack.h 的各种源代码实现。这是我当前的堆栈标头...

/*
 * File: stack.h
 * -------------
 * Interface file of the Stack class. Follows a last-in/ first-out (LIFO) order. Includes contracts for the method bodies of Stack.
*/
#ifndef _stack_h
#define _stack_h
/*
 * declaration of Stack using template with type E for any data type or object using the Stack structure.
 *
 */
  template <typename E>
  class Stack {
    public: 
    //constructor blank
    Stack();
    //destructor
    ~Stack();
    //constructor with integer size
    Stack(int);
    //number of items in stack.
    int size() const;
    //is stack empty?
    bool empty() const;
    //top element of stack
    const E& top() const throw(StackEmpty);
    //push e onto the stack.
    void push(const E& e);
    //remove top element from stack
    void pop() throw(StackEmpty);
    //pop and return.
    E popReturn();
    //returns a string form of stack.
    std::string toString();
};
// Error exception
class StackEmpty : public RuntimeException {
    public:
       StackEmpty(const std::string& err) : RuntimeException(err) {}
};
#endif

对不起,格式! :)目前,我正在研究此堆栈的数组和链表实现。我知道头文件在它所包含的文件之间创建了一个链接。我想确保当我创建一个用于测试的堆栈时,我可以使用我用这个标头编写的两个实现。我也不确定我是否应该使用关键字 virtual 来制作官方界面。我知道在java中,当您声明堆栈的实现时,您将使用

Stack test = new ArrayStack();

对于C++来说,使用全球化头文件并使用此堆栈的不同实现是否相同?此外,这段代码是从C++的数据结构书中挖出来的,但遗憾的是,作者没有说是否将这些接口作为头文件,以及在哪里包含空堆栈的错误检查异常。我只是把它放在这个文件中。C++对我来说不是一个很好的语言,但我知道,如果我想构建更大的项目,如编译器、游戏、音频/视频播放器、操作系统,并为一门新语言编写 IDE,那么这个和 C 很重要掌握。如果可能的话,请给我任何关于我目前情况的见解,我将不胜感激。如果有人也能用C++解释指针和参考,我会非常接受这些知识。我相信E&是一个参考,但这本书没有具体说明。谢谢!:)

附言这就是我认为适用于使用标头的不同实现C++的方法......

#include "stack.h"
Stack test = new ArrayStack();
Stack test2 = new LinkedStack();

您的代码存在许多问题。

template <typename E>

除非你真的需要运行时多态性(在这种情况下你几乎肯定不需要),否则你希望传递要用作模板参数的基础存储,而不是使用继承:

template <typename E, typename Container = std::vector<E> >

这样,您可以避免(例如)对实际上不需要它的基本操作进行虚拟函数调用。

//constructor blank
Stack();

您可能根本不需要声明此默认构造函数。(请参阅下面关于您声明的其他构造函数的注释)。

//destructor
~Stack();

如果您打算按照您的建议使用 Stack 作为基类,那么您希望使析构函数virtual

//constructor with integer size
Stack(int);

除非你确实想使用具有固定大小的基础容器,否则实际上不需要在此处指定大小。如果您无论如何都想指定一个大小,我可能会提供一个默认值(这是使前面的默认构造函数声明无关紧要的一部分),因此您最终会得到如下所示的内容:Stack(int size = 20);

//top element of stack
const E& top() const throw(StackEmpty);

这很糟糕。像您的throw(StackEmpty)这样的动态异常规范被证明是一个错误。它们已被弃用了一段时间,并且可能很快就会消失1.此外,如果要使用继承,这些接口函数中的大多数可能应该是virtual的(可能是纯虚拟的)。

//push e onto the stack.
void push(const E& e);

这没关系,但如果你想充分利用C++,你可能想添加一个也采用右值引用的重载。

//remove top element from stack
void pop() throw(StackEmpty);

此动态异常规范具有与上述相同的问题。

//pop and return.
E popReturn();

这有一个严重的设计缺陷——如果E的复制构造函数抛出异常,它将(不可避免地)破坏数据。有两种众所周知的方法可以避免此问题。一种是消除它并要求用户编写如下代码:

Foo f = myStack.top();
myStack.pop();

这样,如果复制构造函数引发,myStack.pop()永远不会执行。另一个众所周知的可能性是这样的:

void pop(E &dest) { 
    dest = top();
    pop();
}

无论哪种方式,我们最终都会得到异常安全代码 - 要么它完成(并且值从堆栈顶部传输到目标),要么它完全失败(并且数据保留在堆栈上),所以没有影响。

//returns a string form of stack.
std::string toString();

在C++,这通常被命名为to_string

// Error exception
class StackEmpty : public RuntimeException {
    public:
       StackEmpty(const std::string& err) : RuntimeException(err) {}
};

该标准已经定义了std::runtime_error .最好使用它,直到/除非您足够了解C++,确切地知道您想做什么不同的事情。

另请注意:如果您采纳最初的建议并将堆栈设置为纯模板而不是尝试使用继承,那么许多其他评论将变得毫无意义。

最后,C++的stack类最终可以更像这样:

template <class T, class C = std::vector<T> >
class Stack { 
    C data;
public:
    void push(T const &t) { 
        data.push_back(t);
    }
    T top() const {  return data.back(); }
    void pop(T &d) { 
        if (data.empty())
            throw std::runtime_error("Attempt to pop empty stack");
        d = data.top();
        data.pop_back();
    }
    bool empty() const { return data.empty(); }
};

你已指示要使用不同的基础容器对其进行测试。以下是使用三个标准容器的方法:

#include "stack.h"
#include <vector>
#include <list>
#include <deque>
...
// These two are equivalent:
Stack<int, std::vector<int>> vs;
Stack<int> vs1;
Stack<int, std::list<int>> ls;  
Stack<int, std::deque<int>> ds;

1. 是的,Java 几乎完整地复制了这个错误,然后通过添加紧密耦合实际上使它变得更糟,这破坏了异常处理的最大优势之一。我只能说:这很痛;不要再这样做了。

我不打算评论该堆栈的质量、可用性、正确性或"C++性"。对于任何(C++)程序员来说,Java程序员编写堆栈类是显而易见的,这在Java中可能很好,但是在C++中还有更好的方法。

我要做的是尝试回答您关于接口和使用它们的问题,与包含文件有关。(再次强调堆栈类本身并不好。

你可能想要做的是一个"stack.h",内容类似于这样:

template <typename E>
class Stack
{
public:
    // These are *usually* useless in an abstract interface 
    // Stack();
    // ~Stack();
    // Stack(int);
    // Every concrete class inheriting from this class must implement all these:
    virtual int size() const = 0;
    virtual bool empty() const = 0;
    virtual const E& top() const throw(StackEmpty) = 0;
    virtual void push(const E& e) = 0;
    virtual void pop() throw(StackEmpty) = 0;
    virtual E popReturn() = 0;
};

现在,您可能有一个像这样的"vector_stack.h":

#include "stack.h"
template <typename E>
class VectorStack
    : public Stack<E>
{
public:
    VectorStack() {...}
    virtual ~VectorStack() {...}
    Virtual VectorStack(int) {...}
    virtual int size() const override {...}
    virtual bool empty() const override {...}
    virtual const E& top() const throw(StackEmpty) override {...}
    virtual void push(const E& e) override {...}
    virtual void pop() throw(StackEmpty) override {...}
    virtual E popReturn() override {...}
};

你会对你想要做的任何其他实现做完全相同的事情。

现在,在要使用所有这些的代码文件中,执行以下操作:

#include "VectorStack.h"
#include "ListStack.h"
#include "WhateverStack.h"
...
// I apologize to all C++ programmers, for not using unique_ptr<>!
Stack<int> * s1 = new VectorStack<int>;
Stack<int> * s2 = new ListStack<int>;
Stack<int> * s3 = new WhateverStack<int>;
...
...
// Don't forget to delete if you use raw pointers; you're
// not in Kansas anymore!
delete s3;
delete s2;
delete s1;

但这不是很"C++",更不用说灵活或高效了。特别是在这种情况下,对于像堆栈这样的容器。但是,如果您坚持使用该接口(在一般意义上),这可能是您必须做的。

哦,不要忘记#include您使用的标题,例如 <string> .请注意,如果你想要这些异常规范(不要这样做!),异常类的声明必须放在类的声明之前

(顺便说一句,我很确定这是抛出的popReturn方法,而不是pop