javaee论坛

普通会员

225648

帖子

345

回复

359

积分

楼主
发表于 2019-11-07 13:32:08 | 查看: 406 | 回复: 0

之前简单的列举了一下各种智能指针的特点,其中提到了这个历经沧桑的指针,C++98中引入,C++11中弃用,C++17中被移除,弃用的原因主要是使用不当容易造成内存崩溃,不能够作为函数的返回值和函数的参数,也不能在容器中保存auto_ptr。其实说这个指针“不能够作为函数的返回值和函数的参数,也不能在容器中保存”,这个结论过于武断了,经过一系列的测试后发现,原来真正的结论不应该说“不能”,准确来说是“不建议”。

auto_ptr本身是一个模板类,那么一般情况下直接用它来定义一个智能指针的对象,例如std::auto_ptr<Test>pa(newTest);需要注意的是pa虽然叫智能指针,但是它是一个对象,在它的内部保存着一个原始的对象的指针,其原理就是RAII(ResourceAcquisitionIsInitialization),在智能指针构造的时候获取资源,在析构的时候释放资源,并进行相关指针操作的重载,使其使用起来就像普通的指针一样方便。

查看auto_ptr的代码时发现,它主要有get、release、reset、operator*、operator->、operator=几个函数,下面通过一些例子来了解一下auto_ptr的具体用法。

使用环境VS2015+Windows7(应该是C++11标准)头文件#include<memory>命名空间usingnamespacestd;测试过程

首先我们先编写一些测试类,用来测试智能指针各个函数的作用,以及可能出现的问题,测试类的代码如下:

classExample{public:Example(intparam=0){number=param;cout<<"Example:"<<number<<endl;}~Example(){cout<<"~Example:"<<number<<endl;}voidtest_print(){cout<<"intestprint:number="<<number<<endl;}voidset_number(intnum){number=num;}private:intnumber;};

测试函数get、operator*、operator->get函数可以获得智能指针包装的原始指针,可以用来判断被包装对象的有效性,也可以用来访问被包装对象,operator*可以直接对智能指针包装的原始指针解引用,获得被包装的对象,operator->用来取得原始对象的指针,引用成员时与get函数作用相同,示例代码如下:

voidtest1(){auto_ptr<Example>ptr1(newExample(6));//Example:6(输出内容)if(ptr1.get())//判断内部指针的有效性{//以下为访问成员的3种方法ptr1.get()->test_print();//intestprint:number=6(输出内容)ptr1->set_number(8);(*ptr1).test_print();//intestprint:number=8(输出内容)}}//~Example:8(输出内容)//出作用域被析构

测试函数release错误用法release函数是很容易让人误解的函数,一般看到release会想起释放、回收的含义,函数的作用通常就是回收掉申请的资源,但是这里就要注意了,auto_ptr对象的release函数只有释放的意思,指的是释放指针的所有权,说简单点就是auto_ptr的对象与原始的指针脱离关系,但是并不回收原始指针申请的内存,如果不主动释放就会造成内存泄露,就像下面这样:

voidtest2(){//auto_ptr<Example>ptr2=newExample(6);//编译错误,不支持不同指针到智能指针的隐式转换auto_ptr<Example>ptr2(newExample(6));//Example:6(输出内容)if(ptr2.get())//判断内部指针的有效性{ptr2.release();//调用release之后会释放内存所有权,但是不会析构,造成内存泄漏if(!ptr2.get())cout<<"ptr2isinvalid"<<endl;//ptr2isinvalid(输出内容)ptr2.release();//多写一遍没有任何作用}}

测试函数release正确用法知道了relsease函数的错误用法,那么正确用法也就应该清楚了,需要自己调用delete,话说如果自己调用了delete那还用智能指针干什么,下面展示正常的用法:

voidtest3(){auto_ptr<Example>ptr3(newExample(3));//Example:3(输出内容)if(ptr3.get())//判断内部指针的有效性{Example*p=ptr3.release();//release函数调用之后会释放内存的所有权,并且返回原始指针if(!ptr3.get())cout<<"ptr3isinvalid"<<endl;//ptr3isinvalid(输出内容)deletep;//~Example:3(输出内容)//主动析构Example对象}}

测试函数reset用法reset函数取其字面含义,就是重新设置的意思,也就是给一个指着对象设置一个新的内存对象让其管理,如果设置之前智能指针的已经管理了一个对象,那么在设置之后原来的对象会被析构掉,具体看测试结果:

voidtest4(){auto_ptr<Example>ptr4(newExample(4));//Example:4(输出内容)cout<<"afterdeclareptr4"<<endl;//afterdeclareptr4ptr4.reset(newExample(5));//Example:5//~Example:4cout<<"afterfunctionreset"<<endl;//afterfunctionreset}

测试函数operator=用法operator=也就是赋值运算符,是智能指针auto_ptr最具争议的一个方法,或者说一种特性,它的种种限制完全来自于这个赋值操作,作为面向的对象中的一部分,如果把一个对象赋值给另一个对象,那么两个对象就是完全一样的,但是这一点却在auto_ptr上打破了,智能指针auto_ptr的赋值,只是移交了所有权,将内部对象的控制所有权从等号的右侧转移到左侧,等号右侧的智能指针丧失对原有内部对象的控制,如果右侧的对象不检测内部对象的有效性,就会造成程序崩溃,测试如下:

voidtest5(){auto_ptr<Example>ptr5(newExample(5));//Example:5(输出内容)auto_ptr<Example>ptr6=ptr5;//没有输出if(ptr5.get())cout<<"ptr5isvalid"<<endl;//没有输出,说明ptr5已经无效,如果再调用就会崩溃if(ptr6.get())cout<<"ptr6isvalid"<<endl;//ptr6isvalid(输出内容)ptr6->test_print();//intestprint:number=5(输出内容)//ptr5->test_print();//直接崩溃}

测试auto_ptr类型返回一些文章中指出,auto_ptr不能作为函数的返回值,但是在我的测试环境下,可以正常执行,并且结果正确,但是还是不建议这样做,原因就是operator=,后面统一总结,先看下这个正常的例子:

auto_ptr<Example>test6_inner(){auto_ptr<Example>ptr6(newExample(6));//Example:6(输出内容)returnptr6;}voidtest6(){auto_ptr<Example>ptr6=test6_inner();//测试auto_ptr类型返回值ptr6->test_print();//intestprint:number=6(输出内容)}//~Example:6(输出内容)//主动析构Example对

测试auto_ptr作为参数这是常常容易出错的情况,原因还是operator=的操作引起的,因为auto_ptr的赋值会转移控制权,所以你把auto_ptr的对象作为参数传递给一个函数的时候,后面再使用这个对象就会直接崩溃:

voidtest7_inner(auto_ptr<Example>ptr7){ptr7->test_print();//intestprint:number=6(输出内容)}//~Example:7(输出内容)//主动析构Example对象voidtest7(){auto_ptr<Example>ptr7(newExample(7));//Example:7(输出内容)test7_inner(ptr7);//传递参数//ptr7->test_print();//直接崩溃}

两个auto_ptr管理一个指针这种错误稍微出现的明显一点,因为智能指针的对象在析构时会回收内部对象的内存,如果两个智能指针同时管理一个内部对象,那么两个auto_ptr对象析构时都会试图释放内部对象的资源,造成崩溃问题:

voidtest8(){Example*p=newExample(8);//Example:7(输出内容)auto_ptr<Example>ptr8(p);auto_ptr<Example>ptr9(p);}//~Example:8(输出内容)//主动析构Example对象//~Example:-572662307(输出内容)//第二次析构崩溃

测试auto_ptr作为容器元素这是一个被广泛讨论的问题,可能你已经猜到了,一般说auto_ptr不能作为容器的元素也是因为operator=操作,但是我在Windows平台上成功运行了下面的代码,并且输出了正常的对象构造信息和析构信息,但是在Linux平台根本就编译不过去,出现大段的编译错误,其中重要的一句就是.../bits/stl_construct.h:73:错误:对‘std::auto_ptr<Example>::auto_ptr(conststd::auto_ptr<Example>&)’的调用没有匹配的函数,其实可以说是operator=的锅,也可以说是拷贝构造函数的锅,但最根本的问题还是赋值时控制权转移导致的,测试代码如下:

voidtest9(){vector<auto_ptr<Example>>v(10);inti=0;for(;i<10;i++){v[i]=auto_ptr<Example>(newExample(i));//windows下正常构造、析构,linux下无法通过编译}}

测试auto_ptr的引用作为参数传递这个例子比较正常,就是将auto_ptr的对象进行引用传递,这种方式不会造成控制权转移,所以不会出现问题:

voidtest10_inner(auto_ptr<Example>&ptr10){ptr10->test_print();//intestprint:number=6(输出内容)}//这里没有析构voidtest10(){auto_ptr<Example>ptr10(newExample(10));//Example:10(输出内容)test10_inner(ptr10);//传递引用参数ptr10->test_print();//intestprint:number=10(输出内容)}//~Example:10(输出内容)//主动析构Example对象

测试auto_ptr的指针作为参数传递这个例子本质上同上个例子一样,就是将auto_ptr的对象的地址传递,这种指针的方式不会造成控制权转移,所以也不会出现问题:

voidtest11_inner(auto_ptr<Example>*ptr11){(*ptr11)->test_print();//intestprint:number=11(输出内容)}//这里没有析构voidtest11(){auto_ptr<Example>ptr11(newExample(11));//Example:11(输出内容)test11_inner(&ptr11);//传递地址参数ptr11->test_print();//intestprint:number=11(输出内容)}//~Example:11(输出内容)//主动析构Example对象现象分析

上述这些例子比较简单,主要是说明auto_ptr的用法,其中比较有争议的也就是6,7,9三个例子,也就是我们前文所说的“不建议”将auto_ptr作为函数返回值、函数参数、容器内的元素,这三个例子中只有作为函数参数的那个例子崩溃了,但是如果我们调用完函数test7_inner之后,不在使用智能指针ptr7也就不会崩溃了,那么是不是说只要我们注意到可能发生的问题,就可以使用auto_ptr在这些情况呢,目前来看是这样的。

但是为什么在Windows上成功运行的test9在Linux上却编译不过呢?简单点说就是为了怕你犯错,而对你采取管制措施,实际上你可以把auto_ptr作为容器的元素,但是因为这样太容易出错了,所以压根就不允许你这样做。

那么Linux是怎样在编译时期就提示auto_ptr这种错误,而Windows又是怎样绕过这种错误的呢?其实从应用的方便性和安全角度出发,容器应该要求其元素对象的拷贝与原对象相同或者等价,但是很明显auto_ptr做不到这一点,因为它的赋值是实质上是控制权的转移,而不是等价的复制,所以拷贝之后原对象必然被改变,linux版本的auto_ptr就是利用了这一点,使其违反C++的静态类型安全规则,这个版本的auto_ptr只实现构造函数auto_ptr(auto_ptr&other)和赋值函数auto_ptr&operator=(auto_ptr&other),因为参数都是非const,在构造或者赋值的时候原对象可能会发生变化,所以与容器对元素要求的不符合,这样在编译阶段就会检查出错误,也就是我们上面test9函数中提示的错误.../bits/stl_construct.h:73:错误:对‘std::auto_ptr<Example>::auto_ptr(conststd::auto_ptr<Example>&)’的调用没有匹配的函数,这样就避免了把auto_ptr作为容器的元素。

关于Windows平台上正常运行test9函数的疑惑,实际上可以从两个方面来考虑,一种方式就是放宽容器对元素的要求,也就是说允许容器中的元素赋值之后,原对象被改变;另一种方式就是auto_ptr只提供构造函数auto_ptr(constauto_ptr&other)和赋值函数auto_ptr&operator=(constauto_ptr&other),这样就就可以通过容器的检测了,但是还有一个问题需要解决,那就是auto_ptr肯定要改变原对象,const类型就没法改变了,其实还有一种神奇的操作叫强制类型转换,使用const_cast就可以改变const对象,这样就达到了使用auto_ptr作为容器元素的目的,具体细节参考:auto_ptr到底能不能作为容器的元素?

前面提到把auto_ptr作为容器元素时很容易出错,这是为什么各个版本的auto_ptr实现的差异会这么大的原因,出错的根本原因就是auto_ptr构造和赋值时控制权的转移,试想一下,对一个容器进行排序,然后提供一个排序函数,然后排序时把容器中的元素传入比较函数,结果容器中元素的内部对象全都被清空了,这显然不是我们想要的,但是如果你不使用类似操作,那么把auto_ptr作为容器元素也没有什么不可。

总结既然auto_ptr在C++17中已经被移除,那么我们也应该顺应潮流,尽量不使用auto_ptr了。虽然不建议使用auto_ptr了,但是他的用法和注意事项我们还是应该了解,毕竟存在了这么多年,还有很多老代码中在用着。由于各平台差异很大,目前auto_ptr作为容器元素不可移植,无论你使用的STL平台是否允许auto_ptr容器,你都不应该这样做。通过分析发现auto_ptr能不能作为容器的元素并非绝对的,不仅与STL的实现有关,而且与STL容器的需求和安全性以及容器的语义有关。

您需要登录后才可以回帖 登录 | 立即注册

触屏版| 电脑版

技术支持 历史网 V2.0 © 2016-2017