初识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);
template<>
int compare<const char*>(const char* const& v1, const char* const& v2){
return strcmp(v1, v2);
}
同样值得注意的是,函数模板特化的实现只能定义在.cpp文件中,这是因为特化的模板函数与非特化本质相同,因此编译器会报错 multiple definition。
加入特化模板函数的比较结果输出:
1
0
1
2.类模板
和模板函数一样,类模板的定义如下:
template <class type> class class-name {
}
类模板的具体应用在C++容器上已经有了很好的体现,比如说vector,queue或stack等,声明一个容器变量一般需要加一个"<>",用于告诉编译器传入参数的具体类型。
在接下来的例子中,我将手写一个queue队列的模板类(以链表的方式实现),并阐述更多关于模板类的细节:
声明模板类QueueItem ,其产生一个队列中的实例块:
template<class Type> class Queue;
template<class Type> class QueueItem{
QueueItem(const Type &t):item(t), next(0){};
Type item;
QueueItem *next;
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;
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){
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类的处理方式即可:
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:
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();
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;
user = new int(1);
}
template<class T>
AutoPtr<T>::~AutoPtr()
{
cout<<"use "<<this<<" destructor"<<endl;
decr();
}
template<class T>
AutoPtr<T>::AutoPtr(const AutoPtr<T>& handle)
{
ptr = handle.ptr;
user = handle.user;
(*user)++ ;
}
template<class T>
AutoPtr<T> & AutoPtr<T>::operator=(const AutoPtr<T> & handle)
{
if(this == &handle) return *this;
decr();
ptr = handle.ptr;
user= handle.user;
(*user)++;
return * this;
}
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
测试样例:
先说明一下,加入“—”分隔符是为了更直观的看到哪一些内存在函数没运行完时就释放,这也是体现智能指针的智能之处。
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。程序结束。
|