当前位置: 首页 > news >正文

智能指针(一):auto_ptr浅析

前言

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

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

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

使用环境

  1. VS2015 + Windows7(应该是C++11标准)
  2. 头文件#include <memory>
  3. 命名空间using namespace std;

测试过程

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

class Example
{
public:
    Example(int param = 0)
    {
        number = param;
        cout << "Example: " << number << endl;
    }

    ~Example() { cout << "~Example: " << number << endl; }

    void test_print() { cout << "in test print: number = " << number << endl; }

    void set_number(int num) { number = num; }

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

    void test1()
    {
     auto_ptr<Example> ptr1(new Example(6));     // Example: 6(输出内容)
     if (ptr1.get())                             // 判断内部指针的有效性
     {
         // 以下为访问成员的3种方法
         ptr1.get()->test_print();               // in test print: number = 6(输出内容)
         ptr1->set_number(8);
         (*ptr1).test_print();                   // in test print: number = 8(输出内容)
     }
    }                                               // ~Example: 8(输出内容) // 出作用域被析构
  2. 测试函数release错误用法
    release函数是很容易让人误解的函数,一般看到release会想起释放、回收的含义,函数的作用通常就是回收掉申请的资源,但是这里就要注意了,auto_ptr对象的release函数只有释放的意思,指的是释放指针的所有权,说简单点就是auto_ptr的对象与原始的指针脱离关系,但是并不回收原始指针申请的内存,如果不主动释放就会造成内存泄露,就像下面这样:

    void test2()
    {
     //auto_ptr<Example> ptr2 = new Example(6);  // 编译错误,不支持不同指针到智能指针的隐式转换
     auto_ptr<Example> ptr2(new Example(6));     // Example: 6(输出内容)
     if (ptr2.get())                             // 判断内部指针的有效性
     {
         ptr2.release();                         // 调用release之后会释放内存所有权,但是不会析构,造成内存泄漏
         if (!ptr2.get())
             cout << "ptr2 is invalid" << endl;  // ptr2 is invalid(输出内容)
    
         ptr2.release();                         // 多写一遍没有任何作用
     }
    }   
  3. 测试函数release正确用法
    知道了relsease函数的错误用法,那么正确用法也就应该清楚了,需要自己调用delete,话说如果自己调用了delete那还用智能指针干什么,下面展示正常的用法:

    void test3()
    {
     auto_ptr<Example> ptr3(new Example(3));     // Example: 3(输出内容)
     if (ptr3.get())                             // 判断内部指针的有效性
     {
         Example *p = ptr3.release();            // release函数调用之后会释放内存的所有权,并且返回原始指针
         if (!ptr3.get())
             cout << "ptr3 is invalid" << endl;  // ptr3 is invalid(输出内容)
    
         delete p;                               // ~Example: 3(输出内容) // 主动析构Example对象
     }
    }
  4. 测试函数reset用法
    reset函数取其字面含义,就是重新设置的意思,也就是给一个指着对象设置一个新的内存对象让其管理,如果设置之前智能指针的已经管理了一个对象,那么在设置之后原来的对象会被析构掉,具体看测试结果:

    void test4()
    {
     auto_ptr<Example> ptr4(new Example(4));     // Example: 4(输出内容)
     cout << "after declare ptr4" << endl;       // after declare ptr4 
     ptr4.reset(new Example(5));                 // Example: 5
                                                 // ~Example: 4 
     cout << "after function reset" << endl;     // after function reset
    }   
  5. 测试函数operator=用法
    operator=也就是赋值运算符,是智能指针auto_ptr最具争议的一个方法,或者说一种特性,它的种种限制完全来自于这个赋值操作,作为面向的对象中的一部分,如果把一个对象赋值给另一个对象,那么两个对象就是完全一样的,但是这一点却在auto_ptr上打破了,智能指针auto_ptr的赋值,只是移交了所有权,将内部对象的控制所有权从等号的右侧转移到左侧,等号右侧的智能指针丧失对原有内部对象的控制,如果右侧的对象不检测内部对象的有效性,就会造成程序崩溃,测试如下:

    void test5()
    {
     auto_ptr<Example> ptr5(new Example(5));     // Example: 5(输出内容)
     auto_ptr<Example> ptr6 = ptr5;              // 没有输出
    
     if (ptr5.get())
         cout << "ptr5 is valid" << endl;        // 没有输出,说明ptr5已经无效,如果再调用就会崩溃
    
     if (ptr6.get())
         cout << "ptr6 is valid" << endl;        // ptr6 is valid(输出内容)
    
     ptr6->test_print();                         // in test print: number = 5(输出内容)
     //ptr5->test_print();                       // 直接崩溃 
    }
  6. 测试auto_ptr类型返回
    一些文章中指出,auto_ptr不能作为函数的返回值,但是在我的测试环境下,可以正常执行,并且结果正确,但是还是不建议这样做,原因就是operator=,后面统一总结,先看下这个正常的例子:

    auto_ptr<Example> test6_inner()
    {
     auto_ptr<Example> ptr6(new Example(6));     // Example: 6(输出内容)
     return ptr6;
    }
    
    void test6()
    {
     auto_ptr<Example> ptr6 = test6_inner();     // 测试auto_ptr类型返回值
     ptr6->test_print();                         // in test print: number = 6(输出内容)
    }                                               // ~Example: 6(输出内容) // 主动析构Example对
  7. 测试auto_ptr作为参数
    这是常常容易出错的情况,原因还是operator=的操作引起的,因为auto_ptr的赋值会转移控制权,所以你把auto_ptr的对象作为参数传递给一个函数的时候,后面再使用这个对象就会直接崩溃:

    void test7_inner(auto_ptr<Example> ptr7)
    {
     ptr7->test_print();                         // in test print: number = 6(输出内容)
    }                                               // ~Example: 7(输出内容) // 主动析构Example对象
    
    void test7()
    {
     auto_ptr<Example> ptr7(new Example(7));     // Example: 7(输出内容)
     test7_inner(ptr7);                          // 传递参数
     //ptr7->test_print();                       // 直接崩溃
    }
  8. 两个auto_ptr管理一个指针
    这种错误稍微出现的明显一点,因为智能指针的对象在析构时会回收内部对象的内存,如果两个智能指针同时管理一个内部对象,那么两个auto_ptr对象析构时都会试图释放内部对象的资源,造成崩溃问题:

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

    void test9()
    {
     vector<auto_ptr<Example>> v(10);
     int i = 0;
     for (; i < 10; i++)
     {
         v[i] = auto_ptr<Example>(new Example(i));// windows下正常构造、析构,linux下无法通过编译
     }
    }
  10. 测试auto_ptr的引用作为参数传递
    这个例子比较正常,就是将auto_ptr的对象进行引用传递,这种方式不会造成控制权转移,所以不会出现问题:

    void test10_inner(auto_ptr<Example>& ptr10)
    {
     ptr10->test_print();                        // in test print: number = 6(输出内容)
    }                                               // 这里没有析构
    
    void test10()
    {
     auto_ptr<Example> ptr10(new Example(10));   // Example: 10(输出内容)
     test10_inner(ptr10);                        // 传递引用参数
     ptr10->test_print();                        // in test print: number = 10(输出内容)
    }                                               //~Example: 10(输出内容) // 主动析构Example对象
  11. 测试auto_ptr的指针作为参数传递
    这个例子本质上同上个例子一样,就是将auto_ptr的对象的地址传递,这种指针的方式不会造成控制权转移,所以也不会出现问题:

    void test11_inner(auto_ptr<Example>* ptr11)
    {
     (*ptr11)->test_print();                     // in test print: number = 11(输出内容)
    }                                               // 这里没有析构
    
    void test11()
    {
     auto_ptr<Example> ptr11(new Example(11));   // Example:11(输出内容)
     test11_inner(&ptr11);                       // 传递地址参数
     ptr11->test_print();                        // in test print: 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(const std::auto_ptr<Example>&)’的调用没有匹配的函数,这样就避免了把auto_ptr作为容器的元素。

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

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

总结

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

相关文章:

  • 智能指针(二):shared_ptr浅析
  • 智能指针(四):unique_ptr浅析
  • Lua中关于table对象引用传递的注意事项
  • VS2015调试dump文件时提示打不开KERNELBASE.dll
  • Mysql中使用select into语句给变量赋值没有匹配记录时的结果
  • 排序算法系列之(四)——抓扑克牌风格的插入排序
  • linux环境下服务器程序的查看与gdb调试
  • linux环境下运行程序常用的nohup和的区别
  • 排序算法系列之(五)——为目标打好基础的希尔排序
  • linux环境下查找包含指定内容的文件及其所在行数
  • Mysql查询可通过给条件字段添加索引提高查询速度
  • Mysql开启、查看慢查询日志
  • IP地址常见分类:A类、B类、C类、D类、E类
  • Mysql表连接:内连接、外连接、交叉连接、自然连接真的都不一样吗
  • C/C++版本更迭历程
  • [微信小程序] 使用ES6特性Class后出现编译异常
  • 【附node操作实例】redis简明入门系列—字符串类型
  • 07.Android之多媒体问题
  • Create React App 使用
  • docker容器内的网络抓包
  • Fastjson的基本使用方法大全
  • Meteor的表单提交:Form
  • Mocha测试初探
  • open-falcon 开发笔记(一):从零开始搭建虚拟服务器和监测环境
  • RxJS: 简单入门
  • SpiderData 2019年2月16日 DApp数据排行榜
  • ⭐ Unity 开发bug —— 打包后shader失效或者bug (我这里用Shader做两张图片的合并发现了问题)
  • 不上全站https的网站你们就等着被恶心死吧
  • 机器学习中为什么要做归一化normalization
  • 开发基于以太坊智能合约的DApp
  • 来,膜拜下android roadmap,强大的执行力
  • 数据科学 第 3 章 11 字符串处理
  • 详解NodeJs流之一
  • 一个完整Java Web项目背后的密码
  • 一个项目push到多个远程Git仓库
  • const的用法,特别是用在函数前面与后面的区别
  • mysql 慢查询分析工具:pt-query-digest 在mac 上的安装使用 ...
  • 回归生活:清理微信公众号
  • ​【原创】基于SSM的酒店预约管理系统(酒店管理系统毕业设计)
  • ​软考-高级-系统架构设计师教程(清华第2版)【第1章-绪论-思维导图】​
  • # Pytorch 中可以直接调用的Loss Functions总结:
  • (16)Reactor的测试——响应式Spring的道法术器
  • (day 12)JavaScript学习笔记(数组3)
  • (k8s中)docker netty OOM问题记录
  • (第9篇)大数据的的超级应用——数据挖掘-推荐系统
  • (定时器/计数器)中断系统(详解与使用)
  • (附源码)ssm高校升本考试管理系统 毕业设计 201631
  • (附源码)ssm基于微信小程序的疫苗管理系统 毕业设计 092354
  • .360、.halo勒索病毒的最新威胁:如何恢复您的数据?
  • .equals()到底是什么意思?
  • .halo勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复
  • .mysql secret在哪_MySQL如何使用索引
  • .NET CF命令行调试器MDbg入门(四) Attaching to Processes
  • .net core MVC 通过 Filters 过滤器拦截请求及响应内容
  • .net 反编译_.net反编译的相关问题