前言:智能指针某些情况下能够避免内存泄露,这里我做了一次重复制造轮子,目的是为了深入理解智能指针的实现原理。这里只实现了auto_point,根据原理我们不难推出其他智能指针的实现方式。
一、实现:
#pragma once
//基于auto_ptr的实现,不抛出任何异常
template<class T>
class auto_pnt{
private:
T* m_pnt;
public:
typedef T element_type;
//避免无意的隐式类型转换
explicit auto_pnt(element_type* p = 0) throw():m_pnt(p){}
auto_pnt (auto_pnt& a) throw():m_pnt(a.release()){}
template<class T1>
auto_pnt(auto_pnt<T1>& a):m_pnt(a->release()) throw(){}
//复制操作符,注意销毁当前指针指向的对象,并将a指针置为0,防止多次delete的异常
auto_pnt& operator=(auto_pnt& a) throw()
{
reset(a->release());
return *this;
}
template<class T1>
auto_pnt& operator=(auto_pnt<T1>& a) throw()
{
reset(a->release());
return *this;
}
~auto_pnt(){delete m_pnt;}
//区指针指向的对象
element_type& operator*() const throw()
{
return *m_pnt;
}
//注意指针为空的情况
element_type* operator->() const throw()
{
return m_pnt;
}
//返回当前指针的值
element_type* get() const throw()
{
return m_pnt;
}
//返回该指针值作为临时变量,并将当前指针置0
element_type* release() throw()
{
element_type* tmp = m_pnt;
m_pnt = 0;
return tmp;
}
//将指针指向p,并销毁当前指向的对象
void reset(element_type* p = 0) throw()
{
if (p != m_pnt)
{
delete m_pnt;
m_pnt = p;
}
}
};
二、测试
class Item{
public:
Item(){ cout << "Item Constructor." << endl;}
~Item(){ cout << "Item Deconstructor." << endl;}
void Print(char* str = NULL){
if(str == NULL)
cout << "hello world."<< endl;
else
cout << str << endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
char str[] = "hi, how are you.";
{
auto_pnt<Item> pt(new Item());
pt->Print(str);
auto_pnt<Item> pt2 = pt;
pt2->Print();
}
system("pause");
return 0;
}
三、与其他智能指针的一些比较
STL一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr和weak_ptr。模板auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,并提供了另外两种解决方案。
1、auto_ptr的缺点
看下面的赋值语句:
auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr<string> vocation;
vocaticn = ps;
注意:如果ps和vocation是常规指针,则两个指针将指向同一个string对象。这是不能接受的,因为程序将试图删除同一个对象两次——一次是ps过期时,另一次是vocation过期时。
要避免这种问题,方法有多种:
定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的构造函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptr和uniqiie_ptr 的策略,但unique_ptr的策略更严格。创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete。这是shared_ptr采用的策略。当然,同样的策略也适用于复制构造函数。
2、摒弃auto_ptr的原因:避免潜在的内存崩溃问题
下面举个例子来说明。#include <iostream>
#include <string>
#include <memory>
using namespace std;
int main() {
auto_ptr<string> films[5] =
{
auto_ptr<string> (new string("Fowl Balls")),
auto_ptr<string> (new string("Duck Walks")),
auto_ptr<string> (new string("Chicken Runs")),
auto_ptr<string> (new string("Turkey Errors")),
auto_ptr<string> (new string("Goose Eggs"))
};
auto_ptr<string> pwin;
pwin = films[2]; // films[2] loses ownership. 将所有权从films[2]转让给pwin,此时films[2]不再引用该字符串从而变成空指针
cout << "The nominees for best avian baseballl film are\n";
for(int i = 0; i < 5; ++i)
cout << *films[i] << endl;
cout << "The winner is " << *pwin << endl;
cin.get();
return 0;
}
运行下发现程序崩溃了,原因在上面注释已经说的很清楚,films[2]已经是空指针了,下面输出访问空指针当然会崩溃了。但这里如果把auto_ptr换成shared_ptr或unique_ptr后,程序就不会崩溃,原因如下:
使用shared_ptr时运行正常,因为shared_ptr采用引用计数,pwin和films[2]都指向同一块内存,在释放空间时因为事先要判断引用计数值的大小因此不会出现多次删除一个对象的错误。
使用unique_ptr时编译出错,与auto_ptr一样,unique_ptr也采用所有权模型,但在使用unique_ptr时,程序不会等到运行阶段崩溃,而在编译器因下述代码行出现错误:
unique_ptr<string> pwin;
pwin = films[2]; // films[2] loses ownership.
3、如何选择智能指针
在掌握了这几种智能指针后,应使用哪种智能指针呢?
(1)如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:
有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;
两个对象包含都指向第三个对象的指针;
STL容器包含指针。
很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。
(2)如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。可将unique_ptr存储到STL容器在那个,只要不调用将一个unique_ptr复制或赋给另一个算法(如sort())。
例如,可在程序中石油类似于下面的代码段。
unique_ptr<int> make_int(int n)
{
return unique_ptr<int>(new int(n));
}
void show(unique_ptr<int> &p1)
{
cout << *a << ' ';
}
int main()
{
...
vector<unique_ptr<int> > vp(size);
for(int i = 0; i < vp.size(); i++)
vp[i] = make_int(rand() % 1000); // 拷贝临时unique_ptr
vp.push_back(make_int(rand() % 1000)); <span style="white-space:pre"> </span> // ok参数是临时的
for_each(vp.begin(), vp.end(), show); // 使用宏定义的for_each
}
其中push_back调用没有问题,因为它返回一个临时unique_ptr,该unique_ptr被赋给vp中的一个unique_ptr。另外,如果按值而不是按引用给show()传递对象,for_each()将非法,因为这将导致使用一个来自vp的非临时unique_ptr初始化pi,而这是不允许的。前面说过,编译器将发现错误使用unique_ptr的企图。
在unique_ptr为右值时,可将其赋给shared_ptr,这与将一个unique_ptr赋给一个需要满足的条件相同。与前面一样,在下面的代码中,make_int()的返回类型为unique_ptr<int>:
unique_ptr<int> pup(make_int(rand() % 1000)); // ok
shared_ptr<int> spp(pup); //不允许, pup 左值
shared_ptr<int> spr(make_int(rand() % 1000)); // ok
模板shared_ptr包含一个显式构造函数,可用于将右值unique_ptr转换为shared_ptr。shared_ptr将接管原来归unique_ptr所有的对象。
在满足unique_ptr要求的条件时,也可使用auto_ptr,但unique_ptr是更好的选择。如果你的编译器没有unique_ptr,可考虑使用Boost库提供的scoped_ptr,它与unique_ptr类似。
最后,要想完全弄透彻看一两篇博客是不可能的,这里我也是参考别人博客加自己理解,进行整理的。对于unique_ptr、shared_ptr和weak_ptr等智能指针我并没有重复制造轮子,SGI的STL源码阅读也是蛮不方便的,加之篇幅已经过长,对于没耐心的可能很难读完,这里只是一个抛砖引玉,希望能帮到读者。