目录
一般模板函数
特化模板函数
模板类
成员模板函数
模板特化
类模板特化
拷贝构造函数
析构函数
赋值运算符重载
->、*等运算符重载
main.cpp
引入
C++最重要的特性之一就是代码重用,为 了实现代码重用,代码必须具有通用性。 通用代码需要不受数据类型的影响,并且可以自动适应数据类型的变化。这种程序设计类型称为参数化程序设计。模板是C++支持参数化程序设计的工具,通过它可以实现参数化多态性。所谓参数化多态性,就是将程序所处理的对象的类型参数化,使得一段程序可以用于处理多种不同类型的对象。
一、函数模板
我们上一次学习的是函数重载,可以看出重载函数通常是对于不同的数据类型完成类似的操作。但如果这两个函数只有参数类型不同,功能完全一样,这时如果能写一段通用代码适用于多种不同数据类型,便会使代码的可重用性大大提高,从而提高软件的开发效率;使用函数模板就是为了这一目的。程序员只需对函数模板编写一次,然后基于调用函数时提供的参数类型,C++编译器将自动产生相应的函数来正确的处理该类型的数据。
函数模板的定义形式:
template<typename Type> return_type fuc_name(parameter list) { //函数具体实现 }
<>里的内容是由用逗号分隔的模板参数构成,typename可以接受一个类型参数,也可以用class关键字替代。
我们可以通过一个简单的compare函数来体会模板函数的神奇之处:
template <class Type> int compare(const Type& x1, const Type& x2) { if (x1 < x2) return -1; if (x1 > x2) return 1; return 0; }
模板函数只能在.h头文件中。定义编译器从实参的类型推导出函数模板的类型参数。例如,对于调用表达式compare(a,b),由于实参n为int类型,所以推导出模板中类型参数Type为int;对于调用表达式compare(a,b),由于实参n为double类型,所以推导出模板中类型参数Type为double。当类型参数的含义确定后,编译器将以函数模板为样板,生成一个函数,这一过程称为函数模板的实例化。
main.cpp
int main() { cout << compare(6, 6)<<endl; cout << compare(6.11,6.22)<<endl; cout << compare(6.1,6.0)<<endl; return 0; }
输出:
0 -1 1
有时,遇到某些特定的参数类型我们又需要有另外不同的函数实现,来看一个不理想的结果:
int main() { const char* a = "b"; const char* b = "a"; cout << compare(a, b) << endl; cout << compare(6, 6)<<endl; cout << compare(6.11,6.22)<<endl; cout << compare(6.1,6.0)<<endl; return 0; }
输出:
-1 0 -1 1
很明显,字符串的对比并不是正确的结果;实际上程序是将两个字符串的地址进行对比,这并不是我们想要的对比方式。这时应该使用特化模板函数来实现:
template<> int compare(const char* const& x1, const char* const& x2);
template<> int compare<const char*>(const char* const& x1, const char* const& x2) { return strcmp(x1, x2); } int main() { const char* a = "b"; const char* b = "a"; cout << compare(a, b) << endl; cout << compare(6, 6)<<endl; cout << compare(6.11,6.22)<<endl; cout << compare(6.1,6.0)<<endl; return 0; }
输出:
1 0 -1 1
得到了我们期望的结果。
二、模板类Queue
使用类模板使用户可以为类定义-种模式,使得类中的某些数据成员、某些成员函数的参数、返回值或局部变量能取不同类型(包括系统预定义的和用户自定义的)。类是对一组对象的公共性质的抽象,而类模板则是对不同类的公共性质的抽象,因此类模板是属于更高层次的抽象。由于类模板需要一种或多种类型参数,所以类模板也常常称为参数化类。当然,模板参数的实参也不总是可以用任何类型的。
类模板的定义形式:
template<class Type> class class_name { //类的具体实现 }
下面用一个Queue实例来了解一下类模板:
#ifndef QUEUE_H #define QUEUE_H #include <string> #include <iostream> using namespace std; template<class Type> class Queue; template<class Type> class QueueItem { //构造器初始化,这种初始化效率更高 QueueItem(const Type& t) :item(t), next(0) {}; //队列里的元素 Type item; //队列里实例块的指针 QueueItem* next; //友元类Queue friend class Queue<Type>; //输出运算符重载 friend ostream& operator<<(ostream& os, const Queue<Type>& q); public: //++运算符重载:指针的地址++ QueueItem<Type>* operator++() { return next; } //*取值运算符重载:返回队列实例块中的元素 Type& operator*() { return item; } }; template<class Type> class Queue { private: //头指针 QueueItem<Type>* head; //尾指针 QueueItem<Type>* tail; //销毁 void destroy(); public: //无参构造器且初始化 Queue() :head(0), tail(0) {}; //构造一个队列 怎么构造:拷贝已有的整段Queue Queue(const Queue& q) :head(0), tail(0) { copy_items(q); } //构造一个队列 怎么构造:切片拷贝 template<class It>Queue(It begin, It end) : head(0), tail(0) { copy_items(begin, end); } //拷贝整段队列,加在原有的队列后,但不能自己拷贝自己 void copy_items(const Queue&); //切片拷贝,加在原有的队列后 template<class It> void copy_items(It begin, It end); //切片拷贝,回删原有队列 template<class It> void assign(It begin, It end); //重载赋值运算符 void operator=(const Queue&); //获取头指针的元素 const Type& front() const{ return head->item; } // 获取头指针 const QueueItem<Type>* Head() const { return head; } // 获取尾指针 QueueItem<Type>* End() { return (tail == NULL) ? NULL : tail; } //析构函数 ~Queue() { destroy(); } //进队 void push(const Type&); //出队 void pop(); //判断队列是否为空 bool empty()const { return head == 0; } //重载输出运算符 friend ostream& operator<<(ostream& os, const Queue<Type>& q) { os << "["; //创建Queue里实例块指针 QueueItem<Type>* p; //循环输出Queue里的元素 for (p = q.head; p; p = p->next) { os << p->item << " "; } os << "]\n"; return os; } }; #endif // !QUEUE_H
模板类QueueItem是队列里的实例块,Queue是队列实例。
类模板以外定义其成员函数,则要采用以下形式:
template<class Type> return_type class_name<type>::fuc_name(parameter list) { //函数具体实现 }
我们来看一下具体的实例:
//销毁 template<class Type> void Queue<Type>::destroy() { //一直重复出队列的动作,直到队列为空 while (!empty()) { pop(); } } //元素进队列 template<class Type> void Queue<Type>::push(const Type& val) { //创建元素为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>::pop() { //先进先出 //找到头指针 QueueItem<Type>* p = head; head = head->next; //删除p delete p; } //拷贝整段队列 template<class Type> void Queue<Type>::copy_items(const Queue& orig) { //简单的找到头指针、next、push for (QueueItem<Type>* pt = orig.head; pt; pt = pt->next) { push(pt->item); } } //切片拷贝,加在原有的队列后 template<class Type> template<class It> void Queue<Type>::copy_items(It begin, It end) { while (begin != end) { push(*begin); ++begin; } } //切片拷贝,回删原有的队列 template<class Type> template<class It> void Queue<Type>::assign(It begin, It end) { //先回删 destroy(); //再切片拷贝 copy_items(begin, end); } template<class Type> void Queue<Type>::operator=(const Queue& q) { //要先判断是不是本身,不然是本身的话会出现逻辑错误 if (this != &q) { //删除原队列 destroy(); //再拷贝另外的整段队列 copy_items(q); } }
这些我都是放在头文件里实现的。
main函数:
#include"Cmb.h" #include"Queue.h" #include<iostream> using namespace std; template<> int compare<const char*>(const char* const& x1, const char* const& x2) { return strcmp(x1, x2); } int main() { //const char* a = "b"; //const char* b = "a"; //cout << compare(a, b) << endl; //cout << compare(6, 6)<<endl; //cout << compare(6.11,6.22)<<endl; //cout << compare(6.1,6.0)<<endl; //这个队列q1是int类型,输入遇到其他类型的元素,会强制转换为int Queue<int> q1; double a = 6.66; q1.push(1); q1.push(a); q1.push(3); cout << "q1:" << q1; //构造一个队列q2 怎么构造:拷贝已有的整段Queue q1 Queue<int> q2(q1); cout << "q2:" << q2; //拷贝整段队列q3,加在原有的队列q2后,但不能自己拷贝自己 Queue<int> q3; q3.push(1); q3.push(2); q3.push(3); cout << "q3:" << q3; q2.copy_items(q3); cout << "q2:" << q2; //构造一个队列q4 怎么构造:切片拷贝已有数组 double num[8] = { 1.1,2.2,-3.3,4.4,-5.5,6.66 }; Queue<double> q4(num, num + 8); cout << "q4:" << q4; //切片拷贝,加在原有的队列q2、q4后 q2.copy_items(num, num + 6); q4.copy_items(num+1, num + 5); cout << "q2:" << q2; cout << "q4:" << q4; //切片拷贝,回删原有队列 q4.assign(num + 1, num + 6); cout << "q4:" << q4; //重载后的赋值运算符 Queue<double> q5(num, num + 1); q4 = q5; cout << "q5:" << q5; cout << "q4:" << q4; //QueueItem<double>* c = q5.End(); //cout << c; return 0; }
输出:
q1:[1 6 3 ] q2:[1 6 3 ] q3:[1 2 3 ] q2:[1 6 3 1 2 3 ] q4:[1.1 2.2 -3.3 4.4 -5.5 6.66 0 0 ] q2:[1 6 3 1 2 3 1 2 -3 4 -5 6 ] q4:[1.1 2.2 -3.3 4.4 -5.5 6.66 0 0 2.2 -3.3 4.4 -5.5 ] q4:[2.2 -3.3 4.4 -5.5 6.66 ] q5:[1.1 ] q4:[1.1 ]
我们来看一个不理想的结果:
mian.cpp
Queue<const char*> qst; char str[10]; strcpy(str, "I'm"); qst.push(str); strcpy(str, "Hanyan"); qst.push(str); strcpy(str, "Wei"); qst.push(str); cout << qst;
在使用strcpy()函数时会报错函数存在隐藏危险,只需要在在预处理定义中添加:_CRT_SECURE_NO_WARNINGS
输出:
[Wei Wei Wei ]
这并不是我们想要的结果,因为我们Queue里面存的是str的指针 而最后的str存的是Wei 所以qst存的也一直是Wei,对于类模板的push操作,一般的非指针类型,push方法会自动开辟一个新的空间,因此不必考虑地址重合的问题,但是,这个Queue存的是一个指针类型,就出现了问题。
模板成员函数特化
Queue里存的始终都是str最初的地址空间,因此对于这种指针类型的元素,我们需要对push函数进行模板成员函数特化处理:
template<> void Queue<const char*>::push(const char* const& val) { //创建元素为val的实列块 //这里本来存的const char*指针 //QueueItem<const char*>* pt = new QueueItem<const char*>(val); //但是我们现在想要的是指针所指向的字符串,那我们就需要为每次指针指向的字符串申请新空间 char* pData = new char[strlen(val) + 1]; //然后把每次指针所指向的字符串复制到申请新空间 strcpy(pData, val); //存它 QueueItem<const char*>* pt = new QueueItem<const char*>(pData); //若队列为空,则新进入的实例块既是头指针也是尾指针 if (empty()) { head = tail = pt; } else { //新进入的实例块放在尾指针后,尾指针指向它 tail->next = pt; tail = pt; } } template<> void Queue<const char*>::pop() { //先进先出 //找到头指针 QueueItem<const char*>* p = head; head = head->next; //因为现在不仅存了指针,还另外申请空间存字符串,所以也要另外删除 delete[]p->item; //删除p delete p; }
同样的,这需要在main.cpp中实现
输出:
[I'm Hanyan Wei ]
是我们想要的结果。
这是另外的一种方法——类模板Queue的全特化:实际储存的不是cosnt char*指针,而是string类型
template<> class Queue<const char*> { public: //进队 void push(const char* const& val) { //push函数会把 const char* 类型强制转换为string real_que.push(val); } //出队 void pop() { real_que.pop(); } //判断队列是否为空 bool empty()const { return real_que.empty(); } //获取头指针的元素 const string & front() const { return real_que.front(); } //重载输出运算符 friend ostream& operator<<(ostream& os, const Queue<const char*>& q) { //输出const char* 队列的成员变量real_que os << q.real_que; return os; } private: //实际存储的数据类型是string Queue<string> real_que; };
同样的,这需要在main.cpp中实现
输出:
[I'm Hanyan Wei ]
是我们想要的结果。
三、智能指针(模板类AutoPtr)
我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存被称为自由空间或者堆。程序用堆来存储动态分配的对象即那些在程序运行时分配的对象,当动态对象不再使用时,我们的代码必须显式的销毁它们;所以,动态内存管理经常会出现两种问题:一种是忘记释放内存,会造成内存泄漏;一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象;它能自动判别所指向的内存是否还有常规指针指向它,如果没有,则表示该片内存的生命周期已结束,该片内存自动释放。
其中,有三种智能指针类型:
shared_ptr:允许多个指针指向同一个对象
unique_ptr:“独占”所指向的对象
weak_ptr:它是一种弱引用,指向shared_ptr所管理的对象,与shared_ptr同用,不影响对象生命周期
智能指针会有一个计数器用来代表当前“用户数”也就是判断当前地址是否有常规指针指向它,但”用户数“为0时,这块地址就要被释放了
用一个模板类AutoPtr实例来了解一下智能指针:
构造函数
//指针构造器 初始化为指向pData template<class T> AutoPtr<T>::AutoPtr(T* pData) { //new一个ptr指针初始化为指向pData ptr = pData; //new一个user初始化为1 user = new int(1); }
//指针拷贝构造器 怎么构造:拷贝已有的AutoPtr template<class T> AutoPtr<T>::AutoPtr(const AutoPtr<T>& handle) { //ptr初始化为指向handle数据的指针 ptr = handle.ptr; //user初始化为指向handle用户数的指针 user = handle.user; (*user)++; }
//析构函数 如果自己释放了,那自己指向的数据地址的当前地址的指针以及用户数减一 template<class T> AutoPtr<T>::~AutoPtr() { cout << "智能指针" << this << "已销毁" << endl; decr(); }
//当前地址的指针以及用户数减一 template<class T> void AutoPtr<T>::decr() { //用户数减一 (*user)--; if ((*user) == 0) { delete ptr; //ptr地址赋为空 ptr = 0; delete user; //user地址赋为空 user = 0; cout << "智能指针" << this << "原先指向的地址已经没有指针指向它了 该地址内存释放" << endl; } } //重载赋值运算符 表示指针改指为handle这个数据 template<class T> AutoPtr<T>& AutoPtr<T>:: operator=(const AutoPtr<T>& handle) { //如果handle是本身 则返回本身 if (this == &handle) return *this; //ptr指向的原数据地址的指针以及用户数减一 decr(); //ptr改为指向handle数据的指针 ptr = handle.ptr; //user改为指向handle用户数的指针 user = handle.user; (*user)++; return *this; }
//重载-> 返回指针 代表对智能指针指向的原始数据进行操作,而不是操作智能指针本身 template<class T> T* AutoPtr<T>::operator->() { return ptr; } //重载* 返回指针所指的对象 代表对智能指针所指的对象里的数据进行操作,而不是操作对象 template<class T> T& AutoPtr<T>::operator*() { return *ptr; }
完整AutoPtr.h
#ifndef AUTOPRT_H #define AUTOPRT_H #include<iostream> using namespace std; template<class T> class AutoPtr { public: //指针构造器 初始化为指向pData AutoPtr(T* pData); //指针拷贝构造器 怎么构造:拷贝已有的AutoPtr AutoPtr(const AutoPtr<T>& handle); //重载赋值运算符 表示指针改指为handle这个数据 AutoPtr<T>& operator=(const AutoPtr<T>& handle); //当前地址的指针以及用户数减一 void decr(); //析构函数 如果自己释放了,那自己指向的数据地址的当前地址的指针以及用户数减一 ~AutoPtr(); //重载-> 返回指针 代表对智能指针指向的对象进行操作,而不是操作智能指针本身 T* operator->(); const T* operator->() const { return ptr; } //重载* 返回指针所指的对象 代表对智能指针所指的对象里的数据进行操作,而不是操作对象 T& operator*(); const T& operator*() const { return *ptr; } private: //指向储存数据的指针 T* ptr; //用户数 带*代表这个user是统一指向存储用户数的地址,保证了用户数的统一修改 int* user; }; //指针构造器 初始化为指向pData template<class T> AutoPtr<T>::AutoPtr(T* pData) { //new一个ptr指针初始化为指向pData ptr = pData; //new一个user初始化为1 user = new int(1); } //指针拷贝构造器 怎么构造:拷贝已有的AutoPtr template<class T> AutoPtr<T>::AutoPtr(const AutoPtr<T>& handle) { //ptr初始化为指向handle数据的指针 ptr = handle.ptr; //user初始化为指向handle用户数的指针 user = handle.user; (*user)++; } //当前地址的指针以及用户数减一 template<class T> void AutoPtr<T>::decr() { //用户数减一 (*user)--; if ((*user) == 0) { delete ptr; //ptr地址赋为空 ptr = 0; delete user; //user地址赋为空 user = 0; cout << "智能指针" << this << "原先指向的地址已经没有指针指向它了 该地址内存释放" << endl; } } //重载赋值运算符 表示指针改指为handle这个数据 template<class T> AutoPtr<T>& AutoPtr<T>:: operator=(const AutoPtr<T>& handle) { //如果handle是本身 则返回本身 if (this == &handle) return *this; //ptr指向的原数据地址的指针以及用户数减一 decr(); //ptr改为指向handle数据的指针 ptr = handle.ptr; //user改为指向handle用户数的指针 user = handle.user; (*user)++; return *this; } //析构函数 如果自己释放了,那自己指向的数据地址的当前地址的指针以及用户数减一 template<class T> AutoPtr<T>::~AutoPtr() { cout << "智能指针" << this << "已销毁" << endl; decr(); } //重载-> 返回指针 代表对智能指针指向的原始数据进行操作,而不是操作智能指针本身 template<class T> T* AutoPtr<T>::operator->() { return ptr; } //重载* 返回指针所指的对象 代表对智能指针所指的对象里的数据进行操作,而不是操作对象 template<class T> T& AutoPtr<T>::operator*() { return *ptr; } #endif // !AUTOPRT_H
//指针构造器 新建一个智能指针h1 指向一个CMatrix对象 AutoPtr<CMatrix> h1(new CMatrix); AutoPtr<CMatrix> h2(new CMatrix); AutoPtr<CMatrix> h3(new CMatrix); cout << "h1:" << &h1 << endl; cout << "h2:" << &h2 << endl; cout << "h3:" << &h3 << endl; //拷贝指针构造器 新建一个智能指针h2 指向h1指向的CMatrix对象 AutoPtr<CMatrix> h4(h1); cout << "h4:" << &h4 << endl; cout << "=========================" << endl; double data1[6] = { 1,2,3,4,5,6 }; double data2[6] = { 7,8,9,10,11,12 }; double data3[2] = { 7,8 }; //h1->重载后代表h1智能指针指向的对象 h1->Create(2, 3, data1); h2->Create(2, 3, data2); h3->Create(1, 2, data3); cout << "h1: " << *h1 << endl; cout << "h2: " << *h2 << endl; cout << "h3: " << *h3 << endl; cout << "h4: " << *h4 << endl; cout << "=========================" << endl; //*h4代表h4指向的地址所存的对象的数据 因为h1和h4指向的是统一内存地址 所以h4指向的对象也是h1所指的对象 (*h4).Set(0, 1, 10); cout << "h1: " << *h1 << endl; cout << "h2: " << *h2 << endl; cout << "h3: " << *h3 << endl; cout << "h4: " << *h4 << endl; cout << "=========================" << endl; //把h1指向的内存地址赋给h3 h3就指向了h1的指向 h3原先指向的内存地址就没有指针指向了 所以会出现释放内存 h3 = h1; h1 = h2; cout << "h1: " << *h1 << endl; cout << "h2: " << *h2 << endl; cout << "h3: " << *h3 << endl; cout << "h4: " << *h4 << endl; cout << "--------------------------------" << endl;
输出:
h1:00DEFE14 h2:00DEFE04 h3:00DEFDF4 h4:00DEFDE4 ========================= h1: 2 3 1 2 3 4 5 6 h2: 2 3 7 8 9 10 11 12 h3: 1 2 7 8 h4: 2 3 1 2 3 4 5 6 ========================= h1: 2 3 1 10 3 4 5 6 h2: 2 3 7 8 9 10 11 12 h3: 1 2 7 8 h4: 2 3 1 10 3 4 5 6 ========================= 智能指针00DEFDF4原先指向的地址已经没有指针指向它了 该地址内存释放 h1: 2 3 7 8 9 10 11 12 h2: 2 3 7 8 9 10 11 12 h3: 2 3 1 10 3 4 5 6 h4: 2 3 1 10 3 4 5 6 -------------------------------- 智能指针00DEFDE4已销毁 智能指针00DEFDF4已销毁 智能指针00DEFDF4原先指向的地址已经没有指针指向它了 该地址内存释放 智能指针00DEFE04已销毁 智能指针00DEFE14已销毁 智能指针00DEFE14原先指向的地址已经没有指针指向它了 该地址内存释放
让我来解释一下这个输出是怎么回事:
下面一段对应输出的第一段,h1、h2、h3分别指向三片不同的内存地址,由拷贝指针构造函数构造出的h4拷贝于h1,所以h1和h4指向同一片内存地址。
//指针构造器 新建一个智能指针h1 指向一个CMatrix对象
AutoPtr<CMatrix> h1(new CMatrix);
AutoPtr<CMatrix> h2(new CMatrix);
AutoPtr<CMatrix> h3(new CMatrix);
cout << "h1:" << &h1 << endl;
cout << "h2:" << &h2 << endl;
cout << "h3:" << &h3 << endl;//拷贝指针构造器 新建一个智能指针h2 指向h1指向的CMatrix对象
AutoPtr<CMatrix> h4(h1);
cout << "h4:" << &h4 << endl;
下面一段对应输出的第二段、第三段,主要操作是,对h4指向的对象的数据进行修改,由于h1和h4指向同一片内存地址,所以对h4指向的对象的数据进行修改就是所以对h1指向的对象的数据进行修改
double data1[6] = { 1,2,3,4,5,6 };
double data2[6] = { 7,8,9,10,11,12 };
double data3[2] = { 7,8 };
//h1->重载后代表h1智能指针指向的对象
h1->Create(2, 3, data1);
h2->Create(2, 3, data2);
h3->Create(1, 2, data3);
cout << "h1: " << *h1 << endl;
cout << "h2: " << *h2 << endl;
cout << "h3: " << *h3 << endl;
cout << "h4: " << *h4 << endl;cout << "=========================" << endl;
//*h4代表h4指向的地址所存的对象的数据 因为h1和h4指向的是统一内存地址 所以h4指向的对象也是h1所指的对象
(*h4).Set(0, 1, 10);
cout << "h1: " << *h1 << endl;
cout << "h2: " << *h2 << endl;
cout << "h3: " << *h3 << endl;
cout << "h4: " << *h4 << endl;cout << "=========================" << endl;
下面一段对应输出的第四段,主要操作就是让h3指向h1和h4指向的那片内存地址,h3原先指向的内存地址就没有指针指向了,那片内存地址释放内存;再让h1指向h2指向的那片内存地址,所以最后的输出h1和h2一样,h3和h4一样:
//把h1指向的内存地址赋给h3 h3就指向了h1的指向 h3原先指向的内存地址就没有指针指向了 所以会出现释放内存
h3 = h1;
h1 = h2;
cout << "h1: " << *h1 << endl;
cout << "h2: " << *h2 << endl;
cout << "h3: " << *h3 << endl;
cout << "h4: " << *h4 << endl;
cout << "--------------------------------" << endl;
最后那段输出就是智能指针的智能之处,程序结束:h3、h4指针销毁,h3、h4原先指向的内存地址就没有指针指向了,那片内存地址释放内存;h1、h2指针销毁,h1、h2原先指向的内存地址就没有指针指向了,那片内存地址释放内存。