还记得吗?我们在《C++学习与基础算法专栏》提到过的STL库中的Stack类,它允许客户端通过在类型名称之后指定尖括号中的基类型来创建不同类型的堆栈。 RPN计算器使用Stack 来保存值。 编写参数化类需要使用我们预备知识中描述的模板。
目前,我们目标是看到Stack类如何使用动态分配来管理内存。为了这个目的,堆栈的基本类型并不重要。我们当然,同样也可以定义一种特定类型的栈,在我们讨论的这种情况下,这是一堆字符。
charstack.h的建立
鉴于我们已经在之前就介绍过STL的使用方法,所以我们现在对他们就应该没有那么的陌生了,了解了栈支持的通用Stack类的操作,导出CharStack类的接口就很容易编写。charstack.h接口的内容如图所示。 由接口导出的条目包括默认构造函数和各种方法大小,isEmpty,clear,push,pop和peek - 定义栈抽象的行为。
我们主要实现我们自己的栈,方便我们操作,所以我们可以这样:
方法 | 用法 |
pop() | 移除栈顶元素,返回它的值 |
push() | 添加一个元素到栈顶 |
size() | 返回栈中的元素个数 |
isEmpty() | 判断栈中的元素是否为空,返回的为bool类型 |
clear | 清空栈内元素 |
peek | 返回堆栈上的最上面的值,而不删除它。 调用在空堆栈上查看会产生错误。 |
定义charstack的共有部分
一般的,一个抽象的抽象行为通常是公开的,所以其共有部分如下:
/*
*这个文件定义了charstack类,它用来实现char类型的栈抽象
*/
#ifndef _charstack_h
#define _charstack_h
/*
*这个类模拟char型的栈,它的基本类型类似于
* stack <char>,我们现在用指定的基本类型去实现
*这些抽象的操作,然后我们可以利用前面的模板知识
*将这些转换为一般的模板
*/
class CharStack{
public:
/*
* 构造函数: CharStack
* 用法: CharStack cstk;
* ----------------------
* 初始化一个新的空栈,使其能够装下一系列的字符
*/
CharStack();
/*
* 析构函数: ~CharStack
* 用法: 常常隐式调用
* -------------------------
* 释放这个结构在堆中占用的空间
*/
~CharStack();
/*
* 方法: size
* 用法: int nElems = cstk.size();
* --------------------------------
* 返回栈中的字符数量
*/
int size();
/*
* 方法: getCapacity()
* 用法: int n = cstk.getCapacity();
* --------------------------------
* 返回栈中的容量
*/
int getCapacity();
/*
* 方法: isEmpty
* 用法: if (cstk.isEmpty()) . . .
* --------------------------------
* 当栈中没有字符元素的时候,返回true
*/
bool isEmpty();
/*
* 方法: clear
* 用法: cstk.clear();
* --------------------
* 移除栈中所有的元素
*/
void clear();
/*
* 方法: push
* 用法: cstk.push(ch);
* ---------------------
* 将一个元素压入栈中.
*/
void push(char ch);
/*
* 方法: pop
* 用法: char ch = cstk.pop();
* ----------------------------
* 移除栈顶元素并返回其值.
*/
char pop();
/*
* 方法: peek
* 用法: char ch = cstk.peek();
* -----------------------------
* 返回堆栈上的最上面的值,而不删除它。
* 在空栈上调用查看会产生错误
*/
char peek();
#include "charstackpriv.h"
};
#endif
对比我们以前见过的接口,这个接口的唯一新功能是析构函数的原型,看起来像这样:
~CharStack();
析构函数从不被显式调用。该函数出现在接口中,是让编译器知道CharStack类定义了一个析构函数,每当CharStack对象超出范围时,需要调用析构函数.客户端不再需要注意CharStack类是如何使用堆的,因为类自己管理自己的内存。当CharStack变量超出范围时,析构函数负责释放在堆上分配的任何内存。
用vector实现charstack
与我们之前定义的大多数类一样,该类的私有部分实际上并不会出现在charstack.h接口中。该信息被隐藏在文件charstackpriv.h中。该文件的内容完全取决于你如何选择在堆栈中表示数据值。
你应该问自己的第一个问题是什么信息需要存储在栈中。字符栈必须清楚地跟踪按照它们出现的顺序推送的字符。与任何STL类一样,我们没有理由对栈可以包含的字符数进行任意限制(即我们不能限制可以输入的字符)。因此,你需要选择一种可随程序运行而动态扩展的数据结构。
当你想到这样的结构可能是什么样的时候,你可能会考虑的一个想法是使用vector < char>来保存栈的元素。因为vector动态增长。实际上,使用vector < char>作为底层代表,使得实现非常容易,如图:
/*
*这个部分包含了charstack类的私有数据
*在这个实现里,数据保存在vector中
*/
private:
/* Instance variables */
std::vector<char> elements;
在争论Vector类不适合这个例子之前,重要的是要强调,选择使用Vector 作为底层代表,是为了只表示这样做是正确的。你应该永远在寻找可以根据已经解决的其他问题来重新定位每个新问题的方法。此外,作为软件工程策略,使用vector来实现栈是绝对没有错的。实现向量比实现堆栈要复杂得多。使用Vector作为底层的代表不会使CharStack类的操作变得神秘,而只是隐藏在一个更大的石头之下的神秘面纱。
charstack.cpp实现文件:
#include <vector>
#include "charstack.h"
#include "error.h"
using namespace std;
CharStack::CharStack() {
/* Empty */
}
CharStack::~CharStack() {
/* Empty */
}
int CharStack::size() {
return elements.size();
}
bool CharStack::isEmpty() {
return elements.empty();
}
void CharStack::clear() {
elements.clear();
}
void CharStack::push(char ch) {
elements.push_back(ch);
}
char CharStack::pop() {
if (elements.empty()) error("pop: Attempting to pop an empty stack");
char result = elements[elements.size() - 1];
elements.erase(elements.begin()+(elements.size() - 1));
return result;
}
char CharStack::peek() {
if (isEmpty()) error("peek: Attempting to peek at an empty stack");
return elements[elements.size() - 1];
}