前言
C++ STL 提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr 和 weak_ptr。其中auto_ptr 是 C++98 提供的解决方案,C+11 已将其摒弃,并提出了 unique_ptr 作为 auto_ptr 替代方案。虽然 auto_ptr 已被摒弃,但在实际项目中仍可使用,但建议使用较新的 unique_ptr,因为 unique_ptr 比 auto_ptr 更加安全。shared_ptr 和 weak_ptr 则是 C+11 从准标准库 Boost 中引入的两种智能指针。此外,Boost 库还提出了 boost::scoped_ptr、boost::scoped_array、boost::intrusive_ptr 等智能指针,虽然尚未得到 C++ 标准采纳,但是在开发实践中可以使用。
C++11智能指针介绍
智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。C++ 11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个shared_ptr引用。该引用计数的内存在堆上分配,当新增一个时引用计数加1,当引用过期时计数减一。只有引用计数为0时,shared_ptr才会自动释放引用的内存资源。对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针,并可以通过get函数获得普通指针。
为什么要使用智能指针
智能指针的作用是管理一个指针,在使用普通指针时存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
指针
普通指针存在的问题
auto_ptr<string> p1 (new string ("I reigned lonely as a cloud."));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错
如果p1和p2是普通指针,那么两个指针将指向同一个string对象。那么在删除同一个对象两次的时候,会出错。要避免这种问题,方法有多种:
(1)定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。
(2)建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的析构函数会删除该对象。然后让赋值操作转让所有权。这就是用于 auto_ptr 和 unique_ptr 的策略,但 unique_ptr 的策略更严格。
(3)创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加 1,而指针过期时,计数将减 1,。当减为 0 时才调用 delete。这是 shared_ptr 采用的策略。
auto_ptr
(C++98的方案,C++11已经抛弃)auto_ptr定义在头文件<memory>
中。采用所有权模式。
auto_ptr<string> p1 (new string ("I reigned lonely as a cloud."));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错
此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!
再来一个例子:
#include <iostream>
#include <string>
#include <memory>
using namespace std;
int main()
{
auto_ptr<string> films[5] = {
auto_ptr<string>(new string("Fowl Balls")),
auto_ptr<string>(new string("Duck Walks")),
auto_ptr<string>(new string("Chicken Runs")),
auto_ptr<string>(new string("Turkey Errors")),
auto_ptr<string>(new string("Goose Eggs"))
};
auto_ptr<string> pwin;
pwin = films[2]; // films[2] loses ownership. 将所有权从films[2]转让给pwin,此时films[2]不再引用该字符串从而变成空指针
cout << "The nominees for best avian baseballl film are\n";
for (int i = 0; i < 5; ++i)
{
cout << *films[i] << endl;
}
cout << "The winner is " << *pwin << endl;
return 0;
}
编译时程序不会出错,但是运行时程序崩溃。因为films[2] 已经是空指针,*films[2]
访问空指针时程序会崩溃。但这里如果把 auto_ptr 换成 shared_ptr 或 unique_ptr 后,程序就不会崩溃,原因如下:
使用 shared_ptr 时运行正常,因为 shared_ptr 采用引用计数,pwin 和films[2] 都指向同一块内存,在释放空间时因为事先要判断引用计数值的大小,因此不会出现多次删除一个对象的错误。
使用 unique_ptr 时编译出错,与 auto_ptr 一样unique_ptr 也采用所有权模型,但在使用 unique_ptr 时,程序不会等到运行阶段崩溃,而在编译下述代码行出现错误:
pwin = films[2]; //films[2] loses ownership
提示你发现潜在的内存错误。这就是为何要摒弃 auto_ptr 的原因,一句话总结就是:避免因潜在的内存问题导致程序崩溃。
从上面可见,unique_ptr 比 auto_ptr 更加安全,因为 auto_ptr 有拷贝语义,拷贝后原对象变得无效,再次访问原对象时会导致程序崩溃;unique_ptr 则禁止了拷贝语义,但提供了移动语义,即可以使用std::move() 进行控制权限的转移,如下代码所示:
unique_ptr<string> upt(new string("lvlv"));
unique_ptr<string> upt1(upt); //编译出错,已禁止拷贝
unique_ptr<string> upt1=upt; //编译出错,已禁止拷贝
unique_ptr<string> upt1=std::move(upt); //控制权限转移
auto_ptr<string> apt(new string("lvlv"));
auto_ptr<string> apt1(apt); //编译通过
auto_ptr<string> apt1=apt; //编译通过
这里要注意,在使用std::move将unique_ptr的控制权限转移后,不能够再通过unique_ptr来访问和控制资源了,否则同样会出现程序崩溃。我们可以在使用unique_ptr访问资源前,使用成员函数get()进行判空操作。
unique_ptr<string> upt1=std::move(upt); //控制权限转移
if(upt.get()!=nullptr) //判空操作更安全
{
//do something
}