目录
auto_ptr的设计动机
auto_ptr是一个指针
独特的初始化方式
auto_ptr拥有权的转移
特性一:auto_ptr的拷贝构造和赋值构造的拥有权变更
特性二:auto_ptr只能拿来当作另一个auto_ptr的初值,普通指针是不行的
特性三:起点和终点
某函数是数据的终点
某函数是数据的起点
特性四:如无意将拥有权进行转移,就不要在参数列中使用auto_ptr,也不要以其作为返回值
特性五:auto_ptr搭配pass by reference(引用传递)进行拥有权的转移是非常糟糕的设计
特性六:auto_ptr搭配const reference向函数传递拥有权
写在前面:一般都将auto_ptr和智能型指针(smart pointer)理解为同一个东西,这是不对的,auto_ptr只是智能型指针的一种,还有其他智能型指针的存在,auto_ptr只是一种针对解决具体某一种场景(或某一类问题)而被设计出来的。
如果用一句话总结智能型指针所存在的意义就是:帮助程序员防止“被异常抛出时发生资源泄漏”。
回想一下我们之前在动态的调用内存资源的时候,最经常用到的就是new和delete这一对儿操作符来产生和销毁对象,这种方式为我们动态的创建和析构内存资源,但是,这也是一系列麻烦的根源,比如,如果我们new了一个对象,但后来忘了写delete这个操作符,那这个对象就一直留在内存当中,造成资源的泄漏了。或者,你没忘记写delete操作符,但是在delete操作符上面还有一个return的语句(delete语句位于函数的最尾端),函数进行到return的时候,直接把结果返回出去,进而将当前的函数终止了,根本不会运行到delete所在的语句,那在这个函数中用new创建的对象还是没有被析构掉,还会造成内存的泄漏。那遇到这种状况怎么办呢?常见的方法就是捕捉所有的异常,将所有的内容,都放在异常捕捉模块(try...catch...)当中,一有异常了就报错。但这样又会使代码变得特别冗长和累赘。
这个时候,就是本篇博客的主角,智能型指针——auto_ptr登场的时候了!智能型指针的作用就是保证,无论何种情形下,只要自己被摧毁了,那就一定连带释放其所指的资源一并进行摧毁。因为智能型指针本身就是一个区域变量,无论是正常退出还算异常退出,只要所指向的函数退出了,那它就进行销毁操作,这样就把所占的内存资源给释放掉了嘛!
它是“其所指向的对象”的拥有者。即,当身为对象拥有者的auto_ptr被摧毁时,其所指向的对象也是被摧毁的。这样就推出了auto_ptr的一个性质:auto_ptr要求一个对象只能指向一个拥有者,严禁一物二主(试想一下,如果允许的话,那当一个拥有者摧毁了,其指向的对象也被摧毁了,那第二个也拥有这个对象的拥有者再被摧毁的时候,其所指向的对象已经被摧毁了,这不就造成了错误了嘛)。
auto_ptr本身是一个指针,但是,auto_ptr有自己独特的地方:其不允许使用一般指针惯用的赋值的方式来初始化:
std::auto_ptr ptr2 = new ClassA; //error
必须直接使用参数的进行初始化:
std::auto_ptr ptr1(new ClassA); //ok
上个标题就说,auto_ptr表示的是对某个对象的拥有权,那问题来了,这个拥有权可以转让吗?应该是可以的,因为STL很强大。
由于一个auto_ptr一旦拥有了某个对象,就意味着这个对象不能再被其他auto_ptr所染指,为了保证这点,auto_ptr在转移的时候,就显的和普通的转移规则不太一样:
std::auto_ptr ptr1(new ClassA);
std::auto_ptr ptr2(ptr1);//copy构造函数
std::auto_ptr ptr1(new ClassA);
std::auto_ptr ptr2;
ptr2 = ptr1;//assignment操作符(=)
比如,上面这两行代码,第一行的时候,ClassA是被ptr1所单独拥有,到第二行的时候,拷贝构造的方式创建了一个ptr2,这个时候,ClassA就变成了只被ptr2单独拥有,同时不再被ptr1所拥有了。这种方式总结起来就是:令auto_ptr的copy构造函数和assignment操作符(=)将对象拥有权交出去。
以上的动作都是将拥有权从ptr1转移到ptr2,于是,ptr2拥有了先前被ptr1所拥有的那个对象。如果ptr2被赋值之前正拥有另一个对象,那赋值动作发生时会调用delete,先将原先的ptr2所拥有的那个对象给删除掉,再将ptr1所拥有的对象转交给ptr2。同时,ptr1不再拥有其原来拥有的对象了就。
拥有权的转移并不是简单的拷贝构造,拥有权转移之后,原先的拥有者就失去了拥有权,就只剩下一个null指针在手了。也可以从这点看出,对于auto_ptr的拷贝构造和赋值构造,其实是被重新定义的,和之前的常用的拷贝、赋值构造是不一样的。
std::auto_ptr ptr;
ptr = new ClassA; //拿普通指针作初值, error
ptr = std::auto_ptr(new ClassA);//拿auto_ptr作初值, ok
由于拥有权可以转移,使得auto_ptr有了一个很特殊的用法:某个函数可以利用auto_ptr将拥有权转交给另一个函数,分两种情形出现:
举个例子,如果auto_ptr以传值的方式被当作一个参数传入一个函数当中,此时,这个函数中的这个参数就相当于获得了auto_ptr的拥有权了(相当于将函数外的拥有权,转移到了函数内),如果这个函数在运行的过程中,没有将这个函数给传出去,那么,当函数运行结束的时候,拿这个auto_ptr传进去的控制权就会被删除,同时,这个控制权所控制的对象也会被删除了。
还是,当一个auto_ptr作为返回参数被返回,其拥有权便也被移交了出去,其所指向的内容的生存周期也就相应的扩展到了定义它的函数之外了,那么,即便这个auto_ptr是定义于函数内,也不会随着函数的生存时间的结束而结束。
如下这个代码段:
template
void bad_print(std::auto_ptr p)
{
if (p.get() == NULL)
{
std::cout << "NULL";
}
else
{
std::cout << *p;
}
}
上面这段代码段,是错误的,当外界定义的auto_ptr对象作为参数传入bad_print函数中,当这个函数运行结束后,那么,auto_ptr对应的对象也会被析构(当函数退出时,会删除p所拥有的对象),但作者真想让auto_ptr的对象被析构吗?应该是不想的,但因为auto_ptr作为参数,拥有权的转移,就造成了这种错误。
如果将auto_ptr和引用进行搭配使用,那是非常糟糕的,因为引用会让所有权变的更加难以捉摸,无法预知拥有权是否被转移,会增加非常大的风险和需要解释的概念性的复杂,应该全力避免auto_ptr和引用的搭配使用。
为啥const reference跟auto_ptr搭配是可以的呢?因为传递一个const reference时,通常预期该对象不会被改动。无法变更任何const reference的拥有权。
template
ostream& operator<< (ostream& strm, const auto_ptr& p)
{
if (p.get() == NULL)
{
strm << "NULL";
}
else
{
strm << *p;
}
return strm;
}
通过const reference进行拥有权的转移,将拥有权进行移交,const的作用并非是让操作者不能更改auto_ptr所拥有的对象, 而是让操作者不能更改auto_ptr的拥有权。
std::auto_ptr f()
{
const std::auto_ptr p(new int);
std::auto_ptr q(new int);
*p = 42; //没问题,运行成功
bad_print(p); //会编译报错,相当于更改了拥有权了
*p = *q; //没问题,相当于不是更改拥有权,而是更改拥有权所拥有的对象
p = q; //编译报错,还是因为这样改相当于更改了拥有权了
return p;
}
就特性而言,const auto_ptr作为参数,对新对象的任何赋值操作都将导致编译报错,其相当于一个常量指针(T* const p),不可以更改这个指针变量,但可以更改指针所指向的内容。