C++学习03:模板类,模板函数的学习,利用模板特性创造Queue队列数据结构和智能指针
文章目录
前言
无论什么编程语言中,都有多种数据类型。整形数,浮点数,字符串等。但有时候我们无法做到提前获取需要存储的数据的类型。例如python语言的列表中允许存储的每个元素的数据类型不一致:
list1 = ['1', 1, 1.0, "sssssssssss"]
#这种形式在python语言中的列表是允许的
C++自然也允许在队列,堆栈数据结构中这么做,但是这么做自然出现一个问题我们在定义队列,堆栈数据结构中各式各样的功能函数如何定义其中传递的数据类型,这就是我们本次博客中C++模板特性可以解决的问题🖖🖖🖖。
1.模板函数
让我们了解模板先从模板函数讲起,我们编写以下这个函数:
//这是一个简单的比较整形数大小的函数,当v1<v2时返回-1,v1=v2时返回0,v1>v2时返回1
int compare(const int& v1, const int& v2){
if(v1<v2) return -1;
if(v1>v2) return 1;
return 0;
}
该函数解决了整形数的比较大小,但是如果我想比较浮点数呢?比较字符型呢?比较麻烦的办法是,我们为每个需要比较的型都写一个比较函数,但是如果后续用户使用的时候想比较一个我们没实现的数据类型呢?为了避免这种麻烦的问题出现,我们可以使用模板函数来解决这个问题:
//使用template 定义Type变量表示Type可以是任何数据类型
template <class Type> //这一行其实跟后面的函数是一体的
int compare(const Type& v1, const Type& v2){
if(v1<v2) return -1;
if(v1>v2) return 1;
return 0;
}
改写后的函数传入的参数是任意的数据类型,这样我们就不需要专门为了浮点数,字符型等专门实现一个函数了,系统在运行的时候会根据传入数据类型和模板函数自动生成一个比较函数。但是,这样其实还是出现另一个问题:我们写的模板函数可能不是所有数据类型都能适用,比如字符串型,字符串比较不能单纯使用"<", ">"来进行比较。那么要如何继续设计呢?于是C++又提供了特化模板函数功能。
1.2 特化模板函数
模板特化就是在实例化模板时,对特定类型的实参进行特殊处理,即实例化一个特殊的实例版本,当以特化定义时的形参使用模板时,将调用特化版本。所以我们为了继续维护模板函数的完整性,对于字符串这个漏洞,我们就需要针对这个类制作特化模板函数(可以理解成游戏补丁)来维护模板函数的完整性:
template <>//声明是模板特化函数
int compare<const char *>(const char* const& v1, const char * const& v2) //进行全特化
{
return strcmp(v1,v2); //调用字符串比较函数
}//模板函数只能写在头文件内,而特化函数的实现只能写在CPP内
通过上面简单的模板函数和特化实例,相信大家对于模板特化已经有了简单的认识。那么接下来我们利用模板构造队列数据结构,从而来具体解释模板类的用法和如何特化模板类。
2. 使用模板类构造Queue队列数据结构
2.1 管理队列元素类QueueItem的构造
在创造Queue数据结构前,我们先要对如何管理队列元素进行思考?我们需要创造一个数据类型来对他们进行管理。当然为了队列能够容纳所有类型的数据,这个我们需要设置为模板类:
//为类取一种模式使得Type变量可以取任意返回值
template <class Type> class Queue; //提前声明模板类方便后续使用
template <class Type>//为类取一种模式使得Type变量可以取任意返回值
class QueueItem
{
Type item;
QueueItem * next;//指向队列下一个元素的指针
//参数列表构造器方法
QueueItem(const Type & data):item(data),next(0){};
friend class Queue<Type>;//将队列定义为友元类方便访问私有变量
friend ostream& operator<<(ostream& os, const Queue<Type> & q); //定义输出元素操作符
public:
QueueItem<Type>* operator++()
{
return next; //返回队列下一个元素的指针
}
Type & operator*() //取出存储的元素
{
return item;
}
};
我们利用模板类创造的队列元素管理类QueueItem存储的数据类型可以为任意类,那么在创建完管理队列元素的类,我们接下来就开始创造队列数据结构:
2.2 队列Queue类的构造
template <class Type> class Queue
{
private:
void copy_items(const Queue &orig); //拷贝起始元素
QueueItem<Type>* head;//队列跟需要头和尾两个指针
QueueItem<Type>* tail;//使用含有模板类组成的QueueItem类需要使用模板声明
void destroy(); //释放队列空间
template<class It> void copy_items(It beg, It end); //指定范围拷贝队列元素
public:
Queue():head(0),tail(0){}; //参数列表构造器,初始化head指针,tail指针
Queue(const Queue& q):head(0),tail(0){
copy_items(q); //拷贝构造器
}
template<class It>
Queue(It beg, It end):head(0),tail(0){copy_items(beg,end);} //指定范围拷贝构造器
template<class It> void assign(It beg, It end);
Queue& operator=(const Queue&);
~Queue(){destroy();} //析构函数
Type& front(){return head->item;} //返回队列最前头的
void push(const Type&); //将元素放入队列
void pop(); //去除队列头部元素
bool empty() const{return head==0;} //判断队列元素是否为空
friend ostream& operator<<(ostream& os, const Queue<Type> &q)
{
os<<"< ";
QueueItem<Type> * p;
for(p=q.head;p;p=p->next)
{
os<<p->item<<" ";
}
os<<">";
return os;
}
//访问头部和尾部的函数
const QueueItem<Type>* Head() const{return head;}
const QueueItem<Type>* End() const {return(tail==NULL)?NULL:tail;}
};
在上面的Queue类中,我们定义了许多模板函数,许多简单的函数使用隐式内联函数(直接写在类里面)的方法来书写。但是为了美观,我们将一些复杂的函数放在类外部来写,但是有一定要注意模板类和模板函数只能写在定义类的头文件中。这样编译器才能找到,而相反特化模板类特化模板函数只能在同名CPP文件里写, 那么我们接下来对之前类中定义的函数一一实现。
//去除队列头部的数据
template <class Type> void Queue<Type>::pop(){
QueueItem<Type> * p =head;
head = head->next;
delete p; //释放空间
}
template <class Type> void Queue<Type>::destroy()
{ //删除整个队列数据
while(!empty()){
pop();
}
}
//队列插入数据,是在尾部进行插入
template <class Type> void Queue<Type>::push(const Type& val){
QueueItem<Type> * pt = new QueueItem<Type>(val);
if(empty()){
head = tail = pt; //头部和尾部指向同一个数据
}
else{
tail->next = pt;
tail=pt; //尾部指针需要始终指向最后一个元素
}
}
template<class Type>
void Queue<Type>::copy_items(const Queue &orig){
for(QueueItem<Type> * pt=orig.head;pt;pt=pt->next){
push(pt->item);
}
} //将队列orig的所有元素插入其他队列,原队列元素仍然保留
template <class Type>
Queue<Type>& Queue<Type>::operator=(const Queue& q){
destroy();
copy_items(q);
}
template <class Type>
template<class It> void Queue<Type>::assign(It beg, It end)
{
destroy();
copy_items(beg, end);//拷贝指定范围的队列区域
}
template <class Type>
template<class It> void Queue<Type>::copy_items(It beg, It end)
{
while(beg!=end){
push(beg);
++beg;
} //拷贝指定范围的队列元素插入原队列
}
2.3 模板函数特化
虽然我们的队列可以存储任何类型的数据,但是依然会遇到之前的问题。当我们所写的模板函数无法对所有数据类型生效的时候,我们就需要对部分函数进行特化,在这里我依然针对字符串数据进行特化。
//针对 char *数据特化push函数
template <>
void Queue<const char *>::push(const char * const & val){
char* new_item = new char[strlen(val)+1]; //根据字符串长度进行创建字符数组
strncpy(new_item,val,strlen(val)+1); //拷贝字符内容
QueueItem<const char* >*pt = new QueueItem<const char *>(new_item); //声明模板特化char *类
if(empty()){
head = tail = pt;
}
else{
tail->next = pt;
tail = pt;
}
}
template <>
void Queue<const char*>::pop(){
QueueItem<const char *> *p = head; //特化模板类QueueItem
delete head->item;//char *数据需要自己管理,所以自己释放
head = head->next;
delete p; //释放指针空间
}
2.4 模板类的特化
这里我依旧以const char* 类特化队列,特化时需要注意,有些函数是不能直接复用的需要,我们进行重写:
template <> class Queue<const char *>
{
private:
void copy_items(const Queue &orig); //拷贝起始元素
QueueItem<const char *>* head;//队列跟需要头和尾两个指针
QueueItem<const char *>* tail;//使用含有模板类组成的QueueItem类需要使用模板声明
void destroy(); //释放队列空间
template<class It> void copy_items(It beg, It end); //指定范围拷贝队列元素
public:
Queue():head(0),tail(0){}; //参数列表构造器,初始化head指针,tail指针
Queue(const Queue& q):head(0),tail(0){
copy_items(q); //拷贝构造器
}
template<class It>
Queue(It beg, It end):head(0),tail(0){copy_items(beg,end);} //指定范围拷贝构造器
template<class It> void assign(It beg, It end);
Queue& operator=(const Queue&);
~Queue(){destroy();} //析构函数
const char *& front(){return head->item;} //返回队列最前头的
void push(const char *&val){ //对const char *模板函数进行特化
char* new_item = new char[strlen(val)+1]; //根据字符串长度进行创建字符数组
strncpy(new_item,val,strlen(val)+1); //拷贝字符内容
QueueItem<const char* >*pt = new QueueItem<const char *>(new_item); //声明模板特化char *类
if(empty()){
head = tail = pt;
}
else{
tail->next = pt;
tail = pt;
}
}; //将元素放入队列
void pop(){
QueueItem<const char *> *p = head; //特化模板类QueueItem
delete head->item;//char *数据需要自己管理,所以自己释放
head = head->next;
delete p; //释放指针空间
}; //去除队列头部元素
bool empty() const{return head==0;} //判断队列元素是否为空
friend ostream& operator<<(ostream& os, const Queue<const char *> &q)
{
os<<"< ";
QueueItem<const char *> * p;
for(p=q.head;p;p=p->next)
{
os<<p->item<<" ";
}
os<<">";
return os;
}
//访问头部和尾部的函数
const QueueItem<const char *>* Head() const{return head;}
const QueueItem<const char *>* End() const {return(tail==NULL)?NULL:tail;}
};
3. 模板类智能指针的创建
使用模板类小试牛刀之后,接下来我们来继续利用模板类创造智能指针。智能指针是什么呢?他和普通的指针有什么区别呢?在C++中,普通类型的指针向内存中申请内存空间后。系统不会自动在指针变量生命周期后自动释放指针的空间。需要用户自己去管理,这就带来了许多内存泄漏的风险。所以C++提供了智能指针,会自动判断指针当前是否应该释放空间,从而由系统代替用户管理空间内存🧐🧐🧐。当然如果我们的智能指针需要针对所有类型的话,我们同样需要使用模板类创建智能指针。
在创建智能指针之前,我们要思考智能指针Auto_ptr有什么特性呢:
1.智能指针拥有判断指针是否仍然绑定数据的标志,在变量生命周期结束后将会根据该标志来决定是否释放空间。
2.智能指针在进行赋值拷贝操作中,原有的指针取消对数据的绑定,而转为被赋值的指针,这样可以防止多个指针指向同一个值。
3.智能指针可以跟普通的指针一样,使用"*“和”->"等操作。
#ifndef AUTOPTR_H
#define AUTOPTR_H
#include<iostream>
using namespace std;
template<class T>
class Auto_ptr {
private:
T *ptr; //智能指针包含的指针指向的值
mutable bool owns; //使用mutable标记号使得该变量始终是可以被改变的,无论有没有const声明都可以修改
//owns代表是否拥有该指针
public:
//参数列表构造器
Auto_ptr(T *p = 0):ptr(p),owns((bool)p){}
//拷贝构造函数
//创建名为新的Auto_ptr对象,新的智能指针保存原来存在的指针。将所有权转给新指针,原来的智能指针成为未绑定的Auto_ptr对象,会在生命周期结束后自动释放
template <class U>
Auto_ptr(const Auto_ptr<U>& a):ptr(a.ptr),owns(a.owns){ a.owns = 0;}
//“=”重载赋值操作符
template<class U>
Auto_ptr& operator=(Auto_ptr<U>& a)
{
if (&a != this)
{
if(owns) //如果指针已有绑定对象那么释放该对象
delete ptr;
owns = a.owns;
ptr = a.ptr;
a.owns = false; //a指针将所有权转给新指针,自己成为未绑定的对象
}
return *this;
}
T& operator *() const {return *ptr;} //运算符*号重载,取出指针指向地址中的内容
T* operator ->() const {return ptr;} //运算符->重载,返回ptr指针
T* get() const { return ptr;} //返回保存的指针
void reset(T *p = 0) //如果自己与p的值不同,则删除ap指向的地址并且将ap绑定到p
{
if(owns) //
{
if(ptr != p) //如果p 和 ptr的值不同
{
delete ptr; //删除原来指向的对象
}
}
owns =1;
ptr = p; //即使ptr与p相等这里赋值也没有问题
}
T* release() const{ owns = false;return ptr;} //原智能指针成为未绑定的返回绑定的指针
~Auto_ptr(){if(!owns) {cout << "~Auto_ptr()!"<< endl;delete ptr;}} //在生命周期结束如果自己不再绑定指针将自己删除
};
#endif // AUTOPTR_H
这里我们可以利用测试代码测试一下我们设计的智能指针是否可靠:
#include<iostream>
#include "AutoPtr.h"
using namespace std;
int main()
{
Auto_ptr<int> Ap;
Auto_ptr<int> Ap1(new int(1024)); //指向数据
if(Ap.get() == NULL)//AP一直没有指向
{
cout << "Ap is NULL!" << endl;
}
cout << "Before = Ap1 value is:" << Ap1.get() << endl;
Auto_ptr<int> Ap3 ;
Ap3 = Ap1; //AP1在这里不再绑定了
cout << "After = Ap1 value is:" << Ap1.get() << endl;
Ap3.release();//释放指针
cout << "Ap3 value is:" << Ap3.get() << endl;
Ap3.reset(new int(12)); //AP3在这里被重新绑定了,所以
cout << "Ap3 value is:" << Ap3.get() << endl;
return 0;
}
观察上述代码可以发现,在上面的三个智能指针,Ap, Ap1, Ap3,这三个指针在程序生命周期结束之后都是需要被释放的防止内存泄漏。所以在结尾会调用三个析构函数,让我们来测试看看效果:
可以看到析构函数动用了三次,和我们分析的输出结果一致,说明我们构造的智能指针实现了基本功能。
结语
在本次实验我们进行了模板类的深度学习,同时掌握了模板函数特性,模板类的特性。以及系统完整性使用了特化模板类构造了队列数据结构并构造了他的const char *类的特化方法,掌握了智能指针的基本性质和并构造了自己的智能指针。