初识C++之:模板与智能指针
在日常编程过程中,无论是定义变量,类成员,还是定义某个函数的传入参数或return类型,亦或者类中的某个实现方法。一般情况下我们都需要赋予这些参数具体的类型(可以是int, float或是某个class),然而在很多情况下,某些函数除了传入参数以外,其中的实现方法其实完全相同。这时候,一般有两种解决方案,一个是定义两个函数,两个函数的差别只是其中传参的类型不同,有点类似于重载(overload)。另一个方案则是本次博客要重点讨论的:利用模板编程(template)。
模板是泛型编程的基础,是创建泛型类或函数的蓝图或公式。像STL中的库容器,还有迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。接下来我们详细讨论模板在C++中的运用。
1.函数模板
模板函数定义的一般形式如下:
template <typename type> ret-type func-name(parameter list)
{
// 函数的主体
}
这里的type就是模板类型的抽象化形式(在具体调用这个函数时就可以传入某种具体的类型),typename
表示type是一个模板类型,也可以使用class
关键字替代。
1.1 一般模板函数
这里举一个简单的使用模板函数的具体栗子:
template <class Type> // 函数模板(实现模板函数都需要加上)
int compare(const Type& v1, const Type& v2)
{
if(v1<v2) return -1;
if(v1>v2) return 1;
return 0;
}
值得注意到是,模板函数只能在.h头文件中定义。
在这个比较函数中,我们将传入的参数的类型使用模板Type来替代,这样一来,编译器就能够根据传入参数的类型,在进行比较操作时自动调用该类型下对应的比较方法。
int main()
{
const char* a = "aba";
const char* b = "aaa";
cout<<compare(a, b)<<endl;
cout<<compare(6,6)<<endl;
cout<<compare(1.321,0.23)<<endl;
return 0;
}
输出:
-1
0
1
1.2 特化模板函数
然而在某些情况下,我们又希望基于特定类型参数的函数的具体实现有所不同,就比如上面例子中的字符串比较,因为仅仅使用比较操作符进行比较实则比较的是字符串的地址,显然并不是我们想要的比较方式。这时候就可以使用特化模板函数另写一个实现方法:
// 函数模板特化声明
// 很多时候,我们既需要一个模板能应对各种情形,又需要它对于某个特定的类型有着特别的处理,这种情形下特化就是需要的了。
template<>
int compare(const char* const& v1, const char* const& v2);
// 函数模板特化
// 实现只能在cpp文件中:
template<>
int compare<const char*>(const char* const& v1, const char* const& v2){
// 调用strcmp函数进行比较
return strcmp(v1, v2);
}
同样值得注意的是,函数模板特化的实现只能定义在.cpp文件中,这是因为特化的模板函数与非特化本质相同,因此编译器会报错 multiple definition。
加入特化模板函数的比较结果输出:
1
0
1
2.类模板
和模板函数一样,类模板的定义如下:
template <class type> class class-name {
// 类的实现
}
类模板的具体应用在C++容器上已经有了很好的体现,比如说vector,queue或stack等,声明一个容器变量一般需要加一个"<>",用于告诉编译器传入参数的具体类型。
在接下来的例子中,我将手写一个queue队列的模板类(以链表的方式实现),并阐述更多关于模板类的细节:
声明模板类QueueItem
,其产生一个队列中的实例块:
// 类模板:
// 先声明Queue因为在QueueItem要用到:
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;}
};
声明模板类Queue
,其产生一个队列容器实例:
template<class Type> class Queue
{
private:
// 头指针
QueueItem<Type>* head;
// 尾指针
QueueItem<Type>* tail;
// 销毁
void destroy();
public:
Queue():head(0),tail(0){};
// 拷贝构造函数(全部拷贝)
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);}
template<class It> void assign(It begin, It end);
// 拷贝函数
void copy_items(const Queue&);
template<class It> void copy_items(It begin, It end);
// 赋值运算符重载
Queue& operator=(const Queue&);
// 析构函数
~Queue(){destroy();}
// 获取头指针元素
Type& front(){return head->item;}
// 不可更改的获取
const Type& front() const{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<<">\n";
return os;
}
// 获取头指针
const QueueItem<Type>* Head() const{return head;}
// 获取尾指针
const QueueItem<Type>* End() const{return (tail==NULL)?NULL:tail->next;}
};
2.1 成员模板函数
模板类的成员函数常常需要用到模板类型参数,同模板函数一样,模板成员函数需要在头文件中实现。接下来我们实现模板类Queue
中的成员函数:
//销毁队列中的所有元素
template<class Type>
void Queue<Type>::destroy(){
// 出队直至头指针为空
while(!empty()){
pop();
}
}
//元素出队列:
template<class Type>
void Queue<Type>::pop(){
QueueItem<Type> * p = head;
head = head->next;
delete p;
}
//元素进队列:
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);
}
}
// 重载赋值运算符:
template<class Type>
Queue<Type>& Queue<Type>::operator=(const Queue& q)
{
//如果自己给自己赋值就什么也不做
if(this!=&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;
}
}
main函数:
int main()
{
Queue<int> q1;
// 如果模板类实例为int,编译器会将double类型转换为int
double d = 3.3;
q1.push(1);
q1.push(d);
q1.push(10);
cout<<q1;
double num[7] = {2.1, -3.3, 1, -4, 0.5};
Queue<double> q2(num, num+7);
cout<<q2;
Queue<double> q3(num, num+3);
q2 = q3;
cout<<q2;
q2.copy_items(num, num+5);
cout<<q2;
q2.assign(num, num + 2);
cout<<q2;
return 0;
}
运行结果:
< 1 3 10 >
< 2.1 -3.3 1 -4 0.5 0 0 >
< 2.1 -3.3 1 >
< 2.1 -3.3 1 2.1 -3.3 1 -4 0.5 >
< 2.1 -3.3 >
然而,对于模板类的push操作,一般的非指针类型,push方法会自动开辟一个新的空间,因此不必考虑地址重合的问题。但是,如果Queue是一个指针类型,push方法就存在缺陷:
Queue<const char *> qchar;
char str[10];
strcpy(str,"htyan");
qchar.push(str);
strcpy(str,"is");
qchar.push(str);
strcpy(str,"me");
qchar.push(str);
cout<<qchar;
输出:
< me me me >
成员模板函数特化
这个结果显然与我们的预期输出< htyan is me >不符,这是因为,即使push方法开辟了一个新的空间,也是指针的地址,最终指向的还是最初的那个地址空间。因此对于指针类型的Queue,我们有必要对其的push方法进行特化处理,这称为模板成员函数特化,本次实现以char* 特化为例(同样的,在头文件声明,在.cpp文件定义):
template<>
void Queue<const char*>::push(const char * const &val){
// 这时候每次传入一个指针,就重新new一个新内存复制指针指向的值,而不是存储新指针。
// 这样就可以避免在同一个地址上反复修改。
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);
if(empty()){
head=tail=pt;
}else{
tail->next = pt;
tail = pt;
}
}
除此之外,对于一般的c++的基本类型,delete和delete[]没有差别,然而对于特化成员模板函数push时,动态new出的空间必须要使用delete[]进行一连串的内存释放,相应的pop成员函数也需要特化:
template<>
void Queue<const char*>::pop(){
QueueItem<const char*> * p = head;
delete[] head->item; // 多了这句
head = head->next;
delete p;
}
即new 和delete ,new[]和delete[]对应使用
这时候再执行main函数里的调用,输出就是:
< htyan is me >
2.2 模板类特化
同样的,对于类而言,当类模板需要单独的对某些类型进行特殊处理时,使用模板类特化,(需要注意的是,类模板特化不要与这个类的成员模板函数特化混合使用,否则会报错explicit specialization of ‘XXX’ after instantiation)。接下来举一个非常简单且直观的例子:
首先定义一个类模板:
template<typename T1, typename T2>
class Test
{
public:
Test(T1 i,T2 j):a(i),b(j){cout<<"模板类"<<endl;}
private:
T1 a;
T2 b;
};
全特化
当一个模板全特化时,它必须具有一个主模板类。同时,模板类型需要全部明确。
// 全特化
template<>
class Test<int , char>
{
public:
Test(int i, char j):a(i),b(j){cout<<"全特化"<<endl;}
private:
int a;
char b;
};
偏特化
如果一个模板类包含多个模板类型,则该模板偏特化时,只明确其中一部分模板类型。
template <typename T2>
class Test<char, T2>
{
public:
Test(char i, T2 j):a(i),b(j){cout<<"偏特化"<<endl;}
private:
char a;
T2 b;
值得一提的是,对于函数模板,只有全特化而不能有偏特化。
实战:模板类Queue
的全特化:
一个更为简单的方式是,对于const char* 类型,我们不需要特化Queue的成员函数push或pop,而只需将其类型转换为string类型(编译器会自动帮我们处理),按照string类的处理方式即可:
// Queue全特化
template<>
class Queue<const char*>{
public:
void Push(const char* str){real_queue.push(str);}
void Pop(){real_queue.pop();}
bool isEmpty() {return real_queue.empty();}
string front() const {return real_queue.front();}
friend ostream & operator<<(ostream& os, Queue<const char*> &que){
os<<que.real_queue;
}
private:
//这里定义一个string类型的queue,所有传入的const char* 参数转换为string类型处理即可
Queue<string> real_queue;
};
3.智能指针
有别于面向对象语言Java,c++在动态分配内存时,并不会自动的释放申请的内存。因此对于很多c++程序员而言,在使用C++的动态内存管理时,如果不够了解代码的逻辑,常常就会产生一些令人摸不着头脑的bug。这些bug通常就是由于动态内存管理失误产生的:比如忘记释放内存,导致内存泄漏,或者在尚有指针引用内存的情况下提前释放内存,这时可能就会产生指针引用非法内存的错误(这种bug的确很令人头秃!👴)
为了避免繁琐的内存管理问题,同时更加安全的使用系统内存,c++引入了智能指针。智能指针与常规指针的区别在于,它能够自动的判断所指向的内存是否还指针指向它,如果这片内存已没有任何常规指针指向它,则认为这片内存的生命周期已经结束,自动的释放这片内存占用的区域,留给他人使用。
对于C++11标准,内置了三种智能指针类型:
std::unique_ptr<T>
:独占资源所有权的指针。std::shared_ptr<T>
:共享资源所有权的指针。std::weak_ptr<T>
:共享资源的观察者,需要和 std::shared_ptr 一起使用,不影响资源的生命周期。
但是为了巩固所学知识,今天我们来自己动手实现一个智能指针类:
根据上述,智能指针需要具备以下功能:
① 智能指针需要拥有一个全局的计数器绑定指向的内存地址用于判断当前这个地址是否还有别的指针使用它。
②当智能指针指向的地址的计数器值为0时,这块地址需要被释放(自动管理)。
需要注意的是,智能指针释放指向的内存并不代表自己本身也要被释放,反过来说也不成立。
#ifndef AUTOPTR_H
#define AUTOPTR_H
#include<iostream>
using namespace std;
// 由于数据的类型是多样的,因此智能指针要使用模板类
template<class T>
class AutoPtr
{
public:
AutoPtr(T* pData);
AutoPtr(const AutoPtr<T>& handle);
~AutoPtr();
//=重载
AutoPtr<T> & operator=(const AutoPtr<T> & handle);
void decr();
//->重载,直接取AutoPtr里的Ptr,即直接操作智能指针指向的原始的数据而不是智能指针本身
T* operator->(){return ptr;}
const T* operator->() const {return ptr;}
//*重载,同->重载
T& operator*(){return ptr;}
const T& operator*() const {return ptr;}
friend ostream& operator<<(ostream& os, const AutoPtr<T> &q){
os<<&q<<'\n';
return os;
}
private:
// 指向这个数据存储内存的指针
T * ptr = NULL;
// 记录有多少用户在使用这个数据
// 使用* 是因为需要统一修改
int * user = 0;
};
// 构造函数,构建使用智能指针的某个实例
template<class T>
AutoPtr<T>::AutoPtr(T* pData)
{
ptr = pData;
// 用智能指针开辟空间时,用户数默认为1(因为包括自己)
user = new int(1);
}
// 析构函数:
template<class T>
AutoPtr<T>::~AutoPtr()
{
cout<<"use "<<this<<" destructor"<<endl;
// 如果自己被释放了,则自己指向的变量的用户数也相应减1
decr();
}
//智能指针赋初值为另一个智能指针指向的变量:
template<class T>
AutoPtr<T>::AutoPtr(const AutoPtr<T>& handle)
{
ptr = handle.ptr;
user = handle.user;
// 那个变量的用户数+1
(*user)++ ;
}
//=运算表示改变这个数据为handle
template<class T>
AutoPtr<T> & AutoPtr<T>::operator=(const AutoPtr<T> & handle)
{
// 自己引用自己不算指向别人
if(this == &handle) return *this;
//在我指向别人之前,我自己目前指向的变量的用户数减1:
decr();
ptr = handle.ptr;
user= handle.user;
(*user)++;
return * this;
}
// 用户数减少时,应该判断是否释放变量内存(=0时)
template<class T>
void AutoPtr<T>::decr()
{
(*user)-- ;
if ((*user)==0){
delete ptr;
ptr = 0;
delete user;
user = 0;
cout<<"release "<<this<<" points' caches"<<endl;
}
}
#endif // AUTOPTR_H
测试样例:
先说明一下,加入“—”分隔符是为了更直观的看到哪一些内存在函数没运行完时就释放,这也是体现智能指针的智能之处。
int main()
{
AutoPtr<Queue<int>> autoq1(new Queue<int>);
AutoPtr<Queue<int>> autoq2(new Queue<int>);
AutoPtr<Queue<int>> autoq3(new Queue<int>);
cout<<"autoq1:"<<autoq1;
cout<<"autoq2:"<<autoq2;
cout<<"autoq3:"<<autoq3;
cout<<"========================="<<endl;
autoq1->push(10);
autoq2->push(1);
autoq3 = autoq1;
autoq1 = autoq2;
AutoPtr<Queue<int>> autoq4(autoq3);
cout<<"-------------------------"<<endl;
return 0 ;
}
运行结果:
运行结果分析:
首先,前两句为对智能指针指向的变量进行操作,不涉及到内存地址引用数的变化。
第三句,autoq3 = autoq1;
开始涉及到内存地址引用数的变化:autoq3不再引用原始的内存地址,因此原始的内存地址引用数减1,这时候,原始的内存地址引用数为0了,因此释放掉该内存地址,终端输出release 0x61fea0 points’ caches。
第四句,autoq1 = autoq2;
同样涉及到内存地址引用数的变化,autoq1原始的内存地址引用数减1,然而由于autoq3还引用着这块内存,因此该内存不会被释放。
第五句,AutoPtr<Queue<int>> autoq4(autoq3);
表示创建一个新的智能指针指向autoq3指向的内存,这时候这块内存的引用数就为2。
然和整个程序就运行完毕了,这时候会调用所有开辟空间的类的析构函数,因此这些智能指针会被释放掉,终端输出use 0x61fe98 destructor表示释放智能指针autoq4 ,autoq4指向的内存引用数减1;然后释放autoq3,输出use 0x61fea0 destructor,此时autoq3指向的内存的引用数减为0,释放该内存,输出release 0x61fea0 points’ caches。然后释放autoq2,输出use 0x61fea8 destructor,autoq2指向的内存引用数减1,然后释放autoq1,输出use 0x61feb0 destructor,此时autoq1指向的内存的引用数减为0,释放该内存,输出release 0x61feb0 points’ caches。程序结束。