一、智能指针的作用
C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
头文件:#include < memory>
C++11 中推出了三种智能指针,unique_ptr、shared_ptr 和 weak_ptr,同时也将 auto_ptr 置为废弃(deprecated)。
二、对象所有权
首先需要理清楚的概念就是对象所有权的概念。所有权在 rust 语言中非常严格,写 rust 的时候必须要清楚自己创建的每个对象的所有权。
但是 C++比较自由,似乎我们不需要明白对象的所有权,写的代码也能正常运行。但是明白了对象所有权,我们才可以正确管理好对象生命周期和内存问题。
C++引入了智能指针,也是为了更好的描述对象所有权,简化内存管理,从而大大减少我们 C++内存管理方面的犯错机会
(一)、 auto_ptr(C++98 的⽅案, C11 已抛弃)采⽤所有权模式。
auto_ptrstd::string p1 (new string ("hello"));
auto_ptrstd::string p2; p2 = p1; //auto_ptr
此时不会报错,p2剥夺了 p1 的所有权,但是当程序运⾏时访问 p1 将会报错。所以 auto_ptr的缺点是:存在潜在的内存崩溃问题!
(二)、 unique_ptr (替换 auto_ptr)
unique_ptr 实现独占式拥有或严格拥有概念,保证同⼀时间内只有⼀个智能指针可以指向该象。它对于避免资源泄露特别有⽤。
采⽤所有权模式,还是上⾯那个例⼦
unique_ptr p3 (new string (auto));
p4 = p3;
此时会报错 编译器认为 p4=p3 ⾮法,避免了 p3 不再指向有效数据的问题。 因此, unique_ptr ⽐ auto_ptr 更安全。
从另一个方面解释来说:
我们大多数场景下用到的应该都是 unique_ptr。unique_ptr 代表的是专属所有权,即由 unique_ptr 管理的内存,只能被一个对象持有。所以,unique_ptr 不支持复制和赋值,如下:
auto w = std::make_unique<Widget>();
auto w2 = w; // 编译错误
如果想要把 w 复制给 w2, 是不可以的。因为复制从语义上来说,两个对象将共享同一块内存。
因此,unique_ptr 只支持移动, 即如下:
auto w = std::make_unique<Widget>();
auto w2 = std::move(w); // w2获得内存所有权,w此时等于nullptr
unique_ptr 代表的是专属所有权,如果想要把一个 unique_ptr 的内存交给另外一个 unique_ptr 对象管理。只能使用 std::move 转移当前对象的所有权。转移之后,当前对象不再持有此内存,新的对象将获得专属所有权。
如上代码中,将 w 对象的所有权转移给 w2 后,w 此时等于 nullptr,而 w2 获得了专属所有权。
unique_ptr 在默认情况下和裸指针的大小是一样的。所以内存上没有任何的额外消耗,性能是最优的。
使用场景 1:忘记 delete
unique_ptr 一个最简单的使用场景是用于类属性。代码如下:
class Box{
public:
Box() : w(new Widget())
{}
~Box()
{
// 忘记delete w
}
private:
Widget* w;
};
如果因为一些原因,w 必须建立在堆上。如果用裸指针管理 w,那么需要在析构函数中delete w; 这种写法虽然没什么问题,但是容易漏写 delete 语句,造成内存泄漏。
如果按照 unique_ptr 的写法,不用在析构函数手动 delete 属性,当对象析构时,属性w将会自动释放内存。
使用场景 2:异常安全
假如我们在一段代码中,需要创建一个对象,处理一些事情后返回,返回之前将对象销毁,如下所示:
void process()
{
Widget* w = new Widget();
w->do_something(); // 可能会发生异常
delete w;
}
在正常流程下,我们会在函数末尾 delete 创建的对象 w,正常调用析构函数,释放内存。
但是如果 w->do_something()发生了异常,那么delete w将不会被执行。此时就会发生内存泄漏。我们当然可以使用 try…catch 捕捉异常,在 catch 里面执行 delete,但是这样代码上并不美观,也容易漏写。
如果我们用 std::unique_ptr,那么这个问题就迎刃而解了。无论代码怎么抛异常,在 unique_ptr 离开函数作用域的时候,内存就将会自动释放。
(三)、shared_ptr:共享所有权
在使用 shared_ptr 之前应该考虑,是否真的需要使用 shared_ptr, 而非 unique_ptr。
shared_ptr 实现共享式拥有概念,多个智能指针可以指向相同对象,该对象和其相关资源会在 “最后⼀个引⽤被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使⽤计数机制来表明资源被⼏个指针共享。
可以通过成员函数 use_count() 来查看资源的所有者个数,除了可以通过 new 来构造,还可以通过传⼊auto_ptr, unique_ptr,weak_ptr来构造。当我们调⽤release()时,当前指针会释放资源所有权,计数减⼀。当计数等于 0 时,资源会被释放。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性 (auto_ptr 是独占的),在使⽤引⽤计数的机制上提供了可以共享所有权的智能指针。shared_ptr 是支持复制的。
shared_ptr性能
内存占用高。 shared_ptr 的内存占用是裸指针的两倍。因为除了要管理一个裸指针外,还要维护一个引用计数。因此相比于 unique_ptr, shared_ptr 的内存占用更高
原子操作性能低。 考虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢。
使用移动优化性能。shared_ptr 在性能上固然是低于 unique_ptr。而通常情况,我们也可以尽量避免 shared_ptr 复制。如果,一个 shared_ptr 需要将所有权共享给另外一个新的 shared_ptr,而我们确定在之后的代码中都不再使用这个 shared_ptr,那么这是一个非常鲜明的移动语义。对于此种场景,我们尽量使用 std::move,将 shared_ptr 转移给新的对象。因为移动不用增加引用计数,因此性能比复制更好。
使用场景
1.shared_ptr 通常使用在共享权不明的场景。有可能多个对象同时管理同一个内存时。
2.对象的延迟销毁。当一个对象的析构非常耗时,甚至影响到了关键线程的速度。可以使用BlockingQueue< std::shared_ptr>将对象转移到另外一个线程中释放,从而解放关键线程。
shared_from_this
我们往往会需要在类内部使用自身的 shared_ptr,例如:
class Widget
{
public:
void do_something(A& a)
{
a.widget = 该对象的shared_ptr;
}
}
我们需要把当前 shared_ptr 对象同时交由对象 a 进行管理。意味着,当前对象的生命周期的结束不能早于对象 a。因为对象 a 在析构之前还是有可能会使用到a.widget。
如果我们直接a.widget = this;, 那肯定不行, 因为这样并没有增加当前 shared_ptr 的引用计数。shared_ptr 还是有可能早于对象 a 释放。
如果我们使用a.widget = std::make_shared< Widget>(this);,肯定也不行,因为这个新创建的 shared_ptr,跟当前对象的 shared_ptr 毫无关系。当前对象的 shared_ptr 生命周期结束后,依然会释放掉当前内存,那么之后a.widget依然是不合法的。
对于这种,需要在对象内部获取该对象自身的 shared_ptr, 那么该类必须继承std::enable_shared_from_this。代码如下:
class Widget : public std::enable_shared_from_this<Widget>
{
public:
void do_something(A& a)
{
a.widget = shared_from_this();
}
}
(四)、 weak_ptr
weak_ptr 是⼀种不控制对象⽣命周期的智能指针,它指向⼀个 shared_ptr 管理的对象。进⾏该对象的内存管理的是那个强引⽤的 shared_ptr。
weak_ptr 只是提供了对管理对象的⼀个访问⼿段。weak_ptr设计的⽬的是为配合shared_ptr ⽽引⼊的⼀种智能指针来协助 shared_ptr ⼯作,它只可以从⼀个shared_ptr 或另⼀个 weak_ptr 对象构造, ,它的构造和析构不会引起引⽤记数的增加或减少。
weak_ptr 是⽤来解决 shared_ptr 相互引⽤时的死锁问题,如果说两个shared_ptr 相互引⽤,那么这两个指针的引⽤计数永远不可能下降为0,也就是资源永远不会释放。它是对对象的⼀种弱引⽤,不会增加对象的引⽤计数,和 shared_ptr 之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调⽤ lock 函数来获得shared_ptr。 当两个智能指针都是 shared_ptr 类型的时候,析构时两个资源引⽤计数会减⼀,但是两者引⽤计数还是为1,导致跳出函数时资源没有被释放(的析构函数没有被调⽤),解决办法:把 其中⼀个改为weak_ptr就可以。
「注意」:不能通过weak_ptr直接访问对象的⽅法,⽐如B对象中有⼀个⽅法print(),我们不能这样访问,pa->pb->print(); 因为pb是⼀个weak_ptr,应该先把它转化为shared_ptr,
如:shared_ptr p = pa->b.lock(); p->print();
指针作为函数传参
很多时候,函数的参数是个指针。这个时候就会面临选择困难症,这个参数应该怎么传,应该是 shared_ptr,还是 const shared_ptr&,还是直接 raw pointer 更合适。
1.只在函数使用指针,但并不保存。假如我们只需要在函数中,用这个对象处理一些事情,但不打算涉及其生命周期的管理,不打算通过函数传参延长 shared_ptr 的生命周期。对于这种情况,可以使用 raw pointer 或者 const shared_ptr&。即:
void func(Widget*);
void func(const shared_ptr<Widget>&)
实际上第一种裸指针的方式可能更好,从语义上更加清楚,函数也不用关心智能指针的类型。
在函数中保存智能指针。假如我们需要在函数中把这个智能指针保存起来,这个时候建议直接传值。
void func(std::shared_ptr ptr);
这样的话,外部传过来值的时候,可以选择 move 或者赋值。函数内部直接把这个对象通过 move 的方式保存起来。这样性能更好,而且外部调用也有多种选择。
总结
对于智能指针的使用,实际上是对所有权和生命周期的思考。