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;

测试过程

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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函数作用相同,示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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的对象与原始的指针脱离关系,但是并不回收原始指针申请的内存,如果不主动释放就会造成内存泄露,就像下面这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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那还用智能指针干什么,下面展示正常的用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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函数取其字面含义,就是重新设置的意思,也就是给一个指着对象设置一个新的内存对象让其管理,如果设置之前智能指针的已经管理了一个对象,那么在设置之后原来的对象会被析构掉,具体看测试结果:

    1
    2
    3
    4
    5
    6
    7
    8
    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的赋值,只是移交了所有权,将内部对象的控制所有权从等号的右侧转移到左侧,等号右侧的智能指针丧失对原有内部对象的控制,如果右侧的对象不检测内部对象的有效性,就会造成程序崩溃,测试如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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=,后面统一总结,先看下这个正常的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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的对象作为参数传递给一个函数的时候,后面再使用这个对象就会直接崩溃:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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对象析构时都会试图释放内部对象的资源,造成崩溃问题:

    1
    2
    3
    4
    5
    6
    7
    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=的锅,也可以说是拷贝构造函数的锅,但最根本的问题还是赋值时控制权转移导致的,测试代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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的对象进行引用传递,这种方式不会造成控制权转移,所以不会出现问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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的对象的地址传递,这种指针的方式不会造成控制权转移,所以也不会出现问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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容器的需求和安全性以及容器的语义有关。
Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客