前言
之前简单的列举了一下各种智能指针的特点,其中提到了这个历经沧桑的指针,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的代码时发现,它主要有get
、release
、reset
、operator*
、operator->
、operator=
几个函数,下面通过一些例子来了解一下auto_ptr
的具体用法。
使用环境
- VS2015 + Windows7(应该是C++11标准)
- 头文件
#include <memory>
- 命名空间
using namespace std;
测试过程
首先我们先编写一些测试类,用来测试智能指针各个函数的作用,以及可能出现的问题,测试类的代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class 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;
};
测试函数
get、operator*、operator->
get
函数可以获得智能指针包装的原始指针,可以用来判断被包装对象的有效性,也可以用来访问被包装对象,operator*
可以直接对智能指针包装的原始指针解引用,获得被包装的对象,operator->
用来取得原始对象的指针,引用成员时与get
函数作用相同,示例代码如下:1
2
3
4
5
6
7
8
9
10
11void 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(输出内容) // 出作用域被析构测试函数release错误用法
release
函数是很容易让人误解的函数,一般看到release会想起释放、回收的含义,函数的作用通常就是回收掉申请的资源,但是这里就要注意了,auto_ptr
对象的release
函数只有释放的意思,指的是释放指针的所有权,说简单点就是auto_ptr
的对象与原始的指针脱离关系,但是并不回收原始指针申请的内存,如果不主动释放就会造成内存泄露,就像下面这样:1
2
3
4
5
6
7
8
9
10
11
12
13void 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(); // 多写一遍没有任何作用
}
}测试函数release正确用法
知道了relsease
函数的错误用法,那么正确用法也就应该清楚了,需要自己调用delete,话说如果自己调用了delete那还用智能指针干什么,下面展示正常的用法:1
2
3
4
5
6
7
8
9
10
11
12void 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对象
}
}测试函数reset用法
reset
函数取其字面含义,就是重新设置的意思,也就是给一个指着对象设置一个新的内存对象让其管理,如果设置之前智能指针的已经管理了一个对象,那么在设置之后原来的对象会被析构掉,具体看测试结果:1
2
3
4
5
6
7
8void 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
}测试函数
operator=
用法operator=
也就是赋值运算符,是智能指针auto_ptr
最具争议的一个方法,或者说一种特性,它的种种限制完全来自于这个赋值操作,作为面向的对象中的一部分,如果把一个对象赋值给另一个对象,那么两个对象就是完全一样的,但是这一点却在auto_ptr
上打破了,智能指针auto_ptr
的赋值,只是移交了所有权,将内部对象的控制所有权从等号的右侧转移到左侧,等号右侧的智能指针丧失对原有内部对象的控制,如果右侧的对象不检测内部对象的有效性,就会造成程序崩溃,测试如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14void 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(); // 直接崩溃
}测试
auto_ptr
类型返回
一些文章中指出,auto_ptr
不能作为函数的返回值,但是在我的测试环境下,可以正常执行,并且结果正确,但是还是不建议这样做,原因就是operator=
,后面统一总结,先看下这个正常的例子:1
2
3
4
5
6
7
8
9
10
11auto_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对测试auto_ptr作为参数
这是常常容易出错的情况,原因还是operator=
的操作引起的,因为auto_ptr
的赋值会转移控制权,所以你把auto_ptr
的对象作为参数传递给一个函数的时候,后面再使用这个对象就会直接崩溃:1
2
3
4
5
6
7
8
9
10
11void 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(); // 直接崩溃
}两个auto_ptr管理一个指针
这种错误稍微出现的明显一点,因为智能指针的对象在析构时会回收内部对象的内存,如果两个智能指针同时管理一个内部对象,那么两个auto_ptr
对象析构时都会试图释放内部对象的资源,造成崩溃问题:1
2
3
4
5
6
7void test8()
{
Example *p = new Example(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(const std::auto_ptr<Example>&)’的调用没有匹配的函数
,其实可以说是operator=
的锅,也可以说是拷贝构造函数的锅,但最根本的问题还是赋值时控制权转移导致的,测试代码如下:1
2
3
4
5
6
7
8
9void test9()
{
vector<auto_ptr<Example>> v(10);
int i = 0;
for (; i < 10; i++)
{
v[i] = auto_ptr<Example>(new Example(i));// windows下正常构造、析构,linux下无法通过编译
}
}测试auto_ptr的引用作为参数传递
这个例子比较正常,就是将auto_ptr
的对象进行引用传递,这种方式不会造成控制权转移,所以不会出现问题:1
2
3
4
5
6
7
8
9
10
11void 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对象测试auto_ptr的指针作为参数传递
这个例子本质上同上个例子一样,就是将auto_ptr
的对象的地址传递,这种指针的方式不会造成控制权转移,所以也不会出现问题:1
2
3
4
5
6
7
8
9
10
11void 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
作为容器元素也没有什么不可。
总结
- 既然
auto_ptr
在C++17中已经被移除,那么我们也应该顺应潮流,尽量不使用auto_ptr
了。 - 虽然不建议使用
auto_ptr
了,但是他的用法和注意事项我们还是应该了解,毕竟存在了这么多年,还有很多老代码中在用着。 - 由于各平台差异很大,目前
auto_ptr
作为容器元素不可移植,无论你使用的STL平台是否允许auto_ptr容器,你都不应该这样做。 - 通过分析发现
auto_ptr
能不能作为容器的元素并非绝对的,不仅与STL的实现有关,而且与STL容器的需求和安全性以及容器的语义有关。