智能指针是对普通原指针的一种封装类,使得对原指针的管理变得更加简便和智能化。
总的来说它们主要为了解决这样一些问题:
1.显式分配资源后的释放问题,比如内存泄漏(new后忘了delete)。
2.资源的共享问题,当多个实例共享某个显式申请的资源(如内存)时,由谁来管理和释放该资源。
3.资源使用过程中的异常处理。在资源使用过程中,当异常发生时,函数将立刻退出(除非你捕获了所有的异常),此时显示分配的资源还没有得到释放,导致资源泄漏。
STL中的auto_ptr就是用于解决以上1,3问题的。
简单的来说,auto_ptr就是原指针的封装,使得可以以auto_ptr本身的局部变量特性来控制和管理原指针的释放问题。因为auto_ptr保证无论在何种情况下(包括异常发生时),只要自己被摧毁,就一定会连带释放其原指针所指的资源。
如;
- <span style="font-size:14px;"><strong><span style="white-space:pre"> </span> void fun()
- {
- ClassA* pA = new ClassA;
- ....
- delete pA;
- }</strong></span>
当do something中出现异常后,将立刻跳出函数(没有异常捕获),此时pA得不到释放,发生内存泄漏。
常规解决办法是:
- <span style="font-size:14px;"><strong> void fun()
- {
- Class A* pA = new ClassA;
- try{
- ....
- }catch(...)
- {
- delete pA;
- throw;
- }
- delete pA;
- }</strong></span>
上面的代码显得比较繁杂,如果用auto_ptr即可简化成:
void fun()
{
auto_ptr<ClassA> pA(new ClassA);
....// do something
}
无需捕获异常,甚至无需(也不要)delete pA。当智能指针pA退出其作用域fun时,将自动调用ClassA析构函数。
注:
1.当使用auto_ptr时,最好不要手动调用delete
如:
void fun()
{
auto_ptr<ClassA> pA(new ClassA);
delete pA;
}
编译会报错:delete无法从std::auto_ptr<_Ty>转换为void*
或:
- <span style="font-size:14px;"><strong> void fun()
- {
- ClassA* p = new ClassA;
- auto_ptr<ClassA> pA(p);
- delete p;
- }</strong></span>
运行会出错,错误定位在auto_ptr的析构函数处。因为p已经被手动释放过一次了,而当函数结束时,pA还会调用ClassA的析构函数,这样一个内存块被释放了两次。
auto_ptr实现了"->" "." "*"等运算符,这使得它能够像原指针那样进行基本操作,但是auto_ptr没有实现指针运算(如++等操作未定义)。
auto_ptr 特性:
1.拥有权转移:
就像前面提到的手动delete会出现的问题,一个指针只允许被释放一次,并且由于构造auto_ptr使用的值传递,因此每个auto_ptr类都不知道所指指针是否已经被释放过。所以要正确使用auto_ptr,需要关注的第一个问题就是如何管理原指针的控制权,即一个原指针任何时刻只能被一个auto_ptr所管理。为了保证这一性质,我们需要对auto_ptr的构造函数,复制构造函数和赋值运算符进行管理(因为这些都有可能使得多个auto_ptr指向一个原指针)。
在这里有两种方案:一是不允许复制构造和赋值运算,二是在复制构造和赋值运算时,将原指针的控制权交予新的auto_ptr。STL中的auto_ptr基于第二种方案,而另一种与auto_ptr功能相似的智能指针boost::scoped_ptr则是基于第一种方案。两种指针在除了这一点上的其他地方都非常相似。auto_ptr的这种实现方案叫拥有权(Ownership)转移:
拥有权转移代表用于构造或赋值传入的auto_ptr将失去之前所指内容的所有权,也就是说,该auto_ptr包含的原指针将被置为空。
如下例:
- <span style="font-size:14px;"><strong> void f()
- {
- auto_ptr<string> p(new string("I am auto"));
- auto_ptr<string> p2(new string("I am auto"));
- cout<<*p<<endl;
- cout<<p->size()<<endl;
-
- p2 = p;
- cout<<*p2<<endl;
- cout<<*p<<endl;
- } </strong></span>
程序将在运行到最后一句出错,因为此时string的控制权交由了p2,p里面的原指针为空。
关于这一点,和之前学C++的“执行赋值操作时,右值(赋值运算符右边的值)不变”大相径庭。这时就需要程序员来保证不再使用拥有权被剥夺后的auto_ptr,因为此时它已经没有任何内容。
对于auto_ptr的赋值操作来说,由于一个auto_ptr和拥有的对象只能是一一对应的关系,如果p2在赋值之前拥有另一个对象,那么赋值操作发生时将会调用该对象的delete将该对象删除。
还有一点就是auto_ptr赋值运算的右值只能是auto_ptr,不能是普通指针。也就是说:
int* p = new int ;
auto_ptr<int> pAuto;
pAuto = p;
以上语句将会编译出错:
auto_ptr这个特性可以更好保证拥有权的唯一性,否则不小心将p赋给其他auto_ptr将导致运行时释放内存错误。而是用auto_ptr作为右值则不会有这个问题,因为控制权会转移。
对于复制构造函数的限制和赋值运算类似,也会发生拥有权转移
由于构造函数以对象指针为参数,因此会涉及到隐式转换的问题。如同上面对赋值运算的限制一样,设计者总希望通过各种方式避免用户犯错。比如用户这样做:
- <span style="font-size:14px;"><strong> void f()
- {
- int * p = new int;
- auto_ptr<int> ptr = p;
- .....
- auto_ptr<int> otherptr = p;
- }</strong></span>
这样虽然auto_ptr没有提供以普通指针为参数的赋值运算操作,但是用户仍然很不小心的将p赋给了多个auto_ptr,并且能够通过编译(构造函数的隐式转换)。关于这一点,设计者通过将构造函数声明explicit关键字来解决。因此上面的代码不会通过编译,编译器会提示:
2.拥有权转移的一些应用:
1.通过返回值可以把一个对象的控制权交由被调用方
2.通过参数(值传递)可以把一个对象的控制权交由被调用方,注意在调用该函数后,调用方的auto_ptr不再有效,因此最好保证别再使用。除非让被调用函数在使用后返回该auto_ptr,但这需要保证被调用方的auto_ptr未转移。
如:
如果这样使用 虽然 p = print(p) 显得有些怪异,但是程序仍然正常运行,但是如果在print中cout后加上一句:
auto_ptr<int> q = p; (或者是p = 另一个auto_ptr)理所当然的,程序运行时内存访问出错,因为print返回的是无效甚至是指向其他对象的p。
出于种种考虑,如何来限制这些调用或者修改?或许传递引用似乎是一个不错的方法,因为这样使得print可以不用返回auto_ptr,而调用方还能继续使用p。但是这仍然建立在被调用方没有转移拥有权的前提下。如何将print参数改为const auto_ptr& 这样可以保证参数不被修改(包括被赋值和用于赋值),此时print中存在auto_ptr<int> q = p;这种语句会编译出错:没有可用的复制构造函数或复制构造函数声明为explicit。其实要做到让p不参数传递或者在函数不部被修改,声明 const auto_ptr<int> p即可:
在main中,注释中的语句无法通过编译。虽然用auto_ptr看起来得时刻小心翼翼。但只需记住两个特性:
auto_ptr的拥有权转移,注意auto_ptr的每一个赋值与构造(无论左值还是右值)
const auto_ptr 类似于常指针: T* const p 而不是指向常数的指针: const T* p。
3.在类中使用auto_ptr:
使用普通指针作为成员的类需要面临的一大问题就是在构造函数中显式分配的资源在发生异常时将无法被合理释放。因为此时对象构造还不完整,无法调用其析构函数。比如:
- <span style="font-size:14px;"><strong> class A
- {
- private:
- m_p1;
- m_p2;
- public:
- A():m_p1(new int), m_p2(new B)
- {
- }
- ~A()
- {
- delete m_p1;
- delete m_p2;
- }
- };
- void f()
- {
- A a;
- }</strong></span>
当构造函数中 new B操作抛出异常时,m_p1指向的内存将无法被释放,因为当异常抛出时,a还不是一个真正的对象,因此栈展开时不会调用其析构函数,导致内存泄漏。解决办法只需简单的将m_p1 m_p2换为auto_ptr即可。
4.auto_ptr的其他限制:
1.auto_ptr不能用于保存数组名,因为auto_ptr通过delete p而不是delete[]p来释放原指针。将数组地址传入auto_ptr可能导致其得不到正常释放。
2.标准容器和auto_ptr:
由于所有STL容器提供的内部接口都类似于:
- <span style="font-size:14px;"><strong> template <class T>
- void container::insert(const T& value)
- {
- ...
- x = value;
- ...
- }</strong></span>
基于前面所提到的拥有权转移和const auto_ptr的特性。auto_ptr明显不适用于STL容器,否则会导致编译错误。
使用auto_ptr的注意事项:
1. auto_ptr之间不能共享拥有权
2.别将数组地址放入auto_ptr。STL没有为数组提供智能指针
3.auto_ptr不支持指针运算
4.auto_ptr不满足容器对元素的要求
5.勿用普通指针构造多个auto_ptr,虽然设计者想尽办法来避免用户这样做(如构造函数声明explicit关键字,不提供直接将普通指针赋给auto_ptr的操作,等)。但是如果用户这样:
- <span style="font-size:14px;"><strong> int *p = new int(8);
- auto_ptr<int> ptr1(p);
- auto_ptr<int> ptr2(p);</strong></span>
编译能通过,但是显然运行就会出问题。面对auto_ptr的种种特殊性质,设计者也显得力不从心。