前言
"打牢基础,万事不愁" .C++的基础语法的学习
引入
类模板是泛型编程的重要组成部分,通常用来实现数据结构,做成容器类.从几个现成容器探寻类模板定义的内容.引用了参考书<C++ Prime Plus> 6th Edition(以下称"本书")的几个例子:数组栈,指针栈,链队列
类模板回顾
类模板把数据类型---class类型或基本类型作为参数传入,设类型为T,则可以把T*(指针),T**(双重指针), T a[](数组)作为类模板属性.(自用)关于程序的一些概念4:C++泛型初探-CSDN博客
类模板的类名和普通class类相同,构造函数形式和普通class类也相同.在类模板对象使用时,要类名后加上"<具体类型名>",在定义类模板方法时,要在类名后加上"<形参类型名>"
数组栈
本书P569,复制书中stacktp.h代码如下:
// stacktp.h -- a stack template
#ifndef STACKTP_H_
#define STACKTP_H_
template <class Type>
class Stack
{
private:
enum {MAX = 10}; // constant specific to class
Type items[MAX]; // holds stack items
int top; // index for top stack item
public:
Stack();
bool isempty();
bool isfull();
bool push(const Type & item); // add item to stack
bool pop(Type & item); // pop top into item
};
template <class Type>
Stack<Type>::Stack()
{
top = 0;
}
template <class Type>
bool Stack<Type>::isempty()
{
return top == 0;
}
template <class Type>
bool Stack<Type>::isfull()
{
return top == MAX;
}
template <class Type>
bool Stack<Type>::push(const Type & item)
{
if (top < MAX)
{
items[top++] = item;
return true;
}
else
return false;
}
template <class Type>
bool Stack<Type>::pop(Type & item)
{
if (top > 0)
{
item = items[--top];
return true;
}
else
return false;
}
#endif
数据部分:
private:
enum {MAX = 10}; // constant specific to class
Type items[MAX]; // holds stack items
int top; // index for top stack item
其中:items[MAX]是物理容器,必须存在的.MAX是数组长度;top表示当前数组元素个数,同时也是指针,可以获得最后的那个元素items[--top].top值比数组索引值多1.添加第一个元素后,数组里有元素items[0],此时top等于1,依次类推.
初始化:建立数组,把当前元素个数top设为0.
算法:入栈和出栈
指针栈
本书P574,复制代码stcktp1.h,如下:
// stcktp1.h -- modified Stack template
#ifndef STCKTP1_H_
#define STCKTP1_H_
template <class Type>
class Stack
{
private:
enum {SIZE = 10}; // default size
int stacksize;
Type * items; // holds stack items
int top; // index for top stack item
public:
explicit Stack(int ss = SIZE);
Stack(const Stack & st);
~Stack() { delete [] items; }
bool isempty() { return top == 0; }
bool isfull() { return top == stacksize; }
bool push(const Type & item); // add item to stack
bool pop(Type & item); // pop top into item
Stack & operator=(const Stack & st);
};
template <class Type>
Stack<Type>::Stack(int ss) : stacksize(ss), top(0)
{
items = new Type [stacksize];
}
template <class Type>
Stack<Type>::Stack(const Stack & st)
{
stacksize = st.stacksize;
top = st.top;
items = new Type [stacksize];
for (int i = 0; i < top; i++)
items[i] = st.items[i];
}
template <class Type>
bool Stack<Type>::push(const Type & item)
{
if (top < stacksize)
{
items[top++] = item;
return true;
}
else
return false;
}
template <class Type>
bool Stack<Type>::pop(Type & item)
{
if (top > 0)
{
item = items[--top];
return true;
}
else
return false;
}
template <class Type>
Stack<Type> & Stack<Type>::operator=(const Stack<Type> & st)
{
if (this == &st)
return *this;
delete [] items;
stacksize = st.stacksize;
top = st.top;
items = new Type [stacksize];
for (int i = 0; i < top; i++)
items[i] = st.items[i];
return *this;
}
#endif
数据部分:
private:
enum {SIZE = 10}; // default size
int stacksize;
Type * items; // holds stack items
int top; // index for top stack item
其中:物理容器是指针*items,其本质上还是数组,数组长度由stacksize描述.top和上面数组栈含义相同.
初始化: new函数动态生成一个数组,长度为stacksize,默认为SIZE.
算法:入栈和出栈,加上了复制构造函数,和赋值构造函数.析构函数删除指针指向数据
---如果不复制类模板对象,可以不定义复制构造函数和赋值构造函数
指针栈分析
----指针栈的优点
1>可以在生成栈的时候决定栈的长度,比数组栈灵活一点.(当然只是看起来灵活,实际上数组栈也很容易做到,一是在泛型传入表达式参数,二是定义构造函数时传入长度).
2>析构函数把栈内数据全删除,节省空间.
前提:指针栈用的数据类型是内置数据类型或者类的集合,而非指针类型.即Type类型的要求,如果是stack<int>,stack<double>,stack<string>,则可以达到效果.但如果是stack<int *>则不行.原理很简单,new产生基本类型或类类型的数组,delete则删除数组中数据(语法规定);new产生指针类型数组时,delete删除数组中所有指针,而这些指针指向的数据还在.
3>数组栈能做内置数据类型和类的集合,不能做指针的集合.指针栈可以,本书给了例证.
----代码方面
Type可以是指针类型
1>双重指针的一种写法:
items = new Type [stacksize]; //Type是指针类型,items双重指针
2>指针传入指针的引用
含义和"变量传入变量的引用"相同,传入的变量可被修改,传入指针也可被修改
在push()函数中,函数内部的item是指针,被赋给指针数组中的某个值
template <class Type>
bool Stack<Type>::push(const Type & item)
{
if (top < stacksize)
{
items[top++] = item; //items双重指针
return true;
}
else
return false;
}
pop()函数,item被修改
template <class Type>
bool Stack<Type>::pop(Type & item)
{
if (top > 0)
{
item = items[--top];
return true;
}
else
return false;
}
本质上new和引用的用法没有改变:
new函数生成"某个类型"的数组,返回指向该数组的指针,不管"某个类型"是基本类型还是指针类型; 引用做形参时传入"某个类型"的变量或引用,不管是否指针类型
整理概念,一句话概括:new运算符支持指针类型,引用支持指针类型
链队列
本书P613,章节号15.2.2模板中的嵌套,程序清单15.5queuetp.h,代码如下
// queuetp.h -- queue template with a nested class
#ifndef QUEUETP_H_
#define QUEUETP_H_
template <class Item>
class QueueTP
{
private:
enum {Q_SIZE = 10};
// Node is a nested class definition
class Node
{
public:
Item item;
Node * next;
Node(const Item & i):item(i), next(0){ }
};
Node * front; // pointer to front of Queue
Node * rear; // pointer to rear of Queue
int items; // current number of items in Queue
const int qsize; // maximum number of items in Queue
QueueTP(const QueueTP & q) : qsize(0) {}
QueueTP & operator=(const QueueTP & q) { return *this; }
public:
QueueTP(int qs = Q_SIZE);
~QueueTP();
bool isempty() const
{
return items == 0;
}
bool isfull() const
{
return items == qsize;
}
int queuecount() const
{
return items;
}
bool enqueue(const Item &item); // add item to end
bool dequeue(Item &item); // remove item from front
};
// QueueTP methods
template <class Item>
QueueTP<Item>::QueueTP(int qs) : qsize(qs)
{
front = rear = 0;
items = 0;
}
template <class Item>
QueueTP<Item>::~QueueTP()
{
Node * temp;
while (front != 0) // while queue is not yet empty
{
temp = front; // save address of front item
front = front->next;// reset pointer to next item
delete temp; // delete former front
}
}
// Add item to queue
template <class Item>
bool QueueTP<Item>::enqueue(const Item & item)
{
if (isfull())
return false;
Node * add = new Node(item); // create node
// on failure, new throws std::bad_alloc exception
items++;
if (front == 0) // if queue is empty,
front = add; // place item at front
else
rear->next = add; // else place at rear
rear = add; // have rear point to new node
return true;
}
// Place front item into item variable and remove from queue
template <class Item>
bool QueueTP<Item>::dequeue(Item & item)
{
if (front == 0)
return false;
item = front->item; // set item to first item in queue
items--;
Node * temp = front; // save location of first item
front = front->next; // reset front to next item
delete temp; // delete former first item
if (items == 0)
rear = 0;
return true;
}
#endif
数据部分:
private:
enum {Q_SIZE = 10};
// Node is a nested class definition
class Node
{
public:
Item item;
Node * next;
Node(const Item & i):item(i), next(0){ }
};
Node * front; // pointer to front of Queue
Node * rear; // pointer to rear of Queue
int items; // current number of items in Queue
const int qsize; // maximum number of items in Queue
QueueTP(const QueueTP & q) : qsize(0) {}
QueueTP & operator=(const QueueTP & q) { return *this; }
1>表示结点的内部类Node.链表为底层的数据结构,统一写法,把要加入数据集合的数据做成结点.为什么内部类?因为只有外部类QueueTP与之有联系.
2>Node* front //前结点,相当于"物理容器",可以用他遍历整个队列.
3>Node* rear //尾结点,标识链队列里最后一个结点.
4>items //队列当前元素个数
5>qsize //队列允许最大元素个数,默认为Q_SIZE,由默认参数传入
初始化:
前结点,尾结点指空,当前元素个数置0;设置最大元素个数.
算法:
构造函数,析构函数,判断满,判断空,入队,出队.
链队列分析
1>入队采用"尾插法",在队列尾部插入结点(这也是Node* rear意义所在).
和链表相比较,多了一个指向尾结点的指针. 没有采用"头结点",不存在一个头部数据为空的结点,在第一个元素插入时,前结点指向了他.所以插入结点时区分front等于0和不等于0的情况编码.
头结点的插入不需要用if区分编码.那么可以用头插法吗?队列的数据结构要求"先进先出"(FIFO),头插法的形式方便实现"后进先出",适合做链栈.理论上可以做,但是要麻烦一些.可以试试看.
===============================内容分隔线:入队细节==========================
如果自己写,估计会写成下面这样:
Node * add = new Node(item); // create node
items++;
if (front == 0) // 队列为空时
front = add; // 前结点指向新结点
rear=add; // 尾结点指向新结点
else
add->next=0; //插入时先说明后面一个结点是空
rear->next = add; //前面结点是rear
rear = add; //尾结点指向新结点
return true;
好像和 示例有点不一样.仔细看是一样的.rear=add;被提取到外面.add->next=0;在生成add结点时,调用Node的构造方法已经说明了,无需多写.所以示例代码写得更加优雅.
在代码正确的基础上可以追求一种编码的美感
===============================内容分隔线:入队细节==========================
2>析构函数:从前结点开始挨个删除队列内元素.
3>出队操作.删除前结点,当前元素个数减1
此外item=front->item;这一句乍一看没用上,其实是把item的值传给了引用形参.
类模板(容器类)小结
学习的根本目的之一是抽取一些固定的解决问题模式,在三个类模板的示例中,得到一些共通点:
1>容器类属性基本上围绕着"物理容器"而来,这个数组(链表)的当前元素个数,限制最多个数,当前指针等.(少一个固然不行,多两个似乎无所谓).也有和算法相关的属性,比如链队列里的rear.
2>对于链表做物理层的容器,必然有个结点的内部类,有个指向头结点的指针做物理容器.
3>容器类的构造方法,建立起没有数据元素的数据集合.
栈和队列是基本的数据结构,从中了解容器类的基本信息.