使用c++,除了c++的语法外,指针是我们面临的最的大一个问题,由于使用不当就会导致程序意外退出,或着内存的占用越来越多,总结起来这些错误由以下三个原因造成。

       1 野指针:指针指向的内存已经被释放,但是我们还在使用该指针,或者还在使用之前指向的指针,此时程序会崩溃,也有可能导致已经释放的内存被重新分配给程序使用,造成意想不到的后果。

       2 重复释放:程序尝试释放已经被释放的内存单元,或者释放已经被重新分配过的内存单元,会导致重复释放错误。

       3 内存泄漏:不需要的内存单元没有释放,一旦程序一直在重复这样的操作,会导致程序的内存占用月来越高。

      虽然显示的手动管理内存,给程序的内存管理带来了很大的自由度,可以高效的利用系统的内存,但是也是非常容易出错的,随着多线程程序的出现和广泛使用,这样的问题会更加严重,因此c++11通过智能指针来摆脱显示的内存管理,标准库还实现了‘最小垃圾回收’的支持。

      c++11中通过unique_ptr,shared_ptr和weak_ptr等智能指针来实现自动释放堆内存。


从RAII说起

  • RAII , Resource Aquisition Is Initailization , 资源分配即初始化。
  • 这是一种编程的观念——以对象管理资源。
  • 为什么我们需要以对象来管理资源呢?大家都知道,作为程序员的我们,虽说大多时候都保持着高度细致的战斗力,但是人无完人,编程到了懈怠期,难免就会出现一些小的错误,比如:忘记释放内存空间,或者因为在长久的维护期中,忽略了goto、return或者捕获异常等会使程序中途跳转的语句,从而忘记delete。
  • 因此,为了确保资源总是被释放,我们需要将资源放入对象之内,当控制流离开其所调用的函数,该对象的析构函数则会自动释放那些资源。
  • 简而言之,就是耍了一个小小的心机,利用了C++的“析构函数自动调用机制”,从而确保资源都得到释放。
  • 于是乎,智能指针由此应运而生。
  • 程序员们从此满面红光地高举智能指针大旗走向罪恶(笑…..

auto_ptr

因为auto_ptr并不是完美无缺的,它的确很方便,但也有缺陷,在使用时要注意避免。首先,不要将auto_ptr对象作为STL容器的元素。C++标准明确禁止这样做,否则可能会碰到不可预见的结果

auto_ptr的另一个缺陷是将数组作为auto_ptr的参数: auto_ptr<char> pstr (new char[12] ); //数组;为定义
然后释放资源的时候不知道到底是利用delete pstr,还是 delete[] pstr;

然后收集了关于auto_ptr的几种注意事项:
1、auto_ptr不能共享所有权。
2、auto_ptr不能指向数组
3、auto_ptr不能作为容器的成员。
4、不能通过赋值操作来初始化auto_ptr
std::auto_ptr<int> p(new int(42)); //OK
std::auto_ptr<int> p = new int(42); //ERROR
这是因为auto_ptr 的构造函数被定义为了explicit
5、不要把auto_ptr放入容器


shared_ptr

1. shared_ptr是Boost库所提供的一个智能指针的实现,shared_ptr就是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针.
2. shared_ptr比auto_ptr更安全
3. shared_ptr是可以拷贝和赋值的,拷贝行为也是等价的,并且可以被比较,这意味这它可被放入标准库的一般容器(vector,list)和关联容器中(map)。

boost::shared_ptr的管理机制其实并不复杂,就是对所管理的对象进行了引用计数,当新增一个boost::shared_ptr对该对象进行管理时,就将该对象的引用计数加一;减少一个boost::shared_ptr对该对象进行管理时,就将该对象的引用计数减一,如果该对象的引用计数为0的时候,说明没有任何指针对其管理,才调用delete释放其所占的内存。


weak_ptr

一个强引用当被引用的对象活着的话,这个引用也存在(就是说,当至少有一个强引用,那么这个对象就不能被释放)。boost::share_ptr就是强引用。

相对而言,弱引用当引用的对象活着的时候不一定存在。仅仅是当它存在的时候的一个引用。弱引用并不修改该对象的引用计数,这意味这弱引用它并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存


虽然shared_ptr能够解决大多数问题,但是shared_ptr有一个缺陷,就是循环使用没办法解决

智能指针_强引用

  引用计数永远减不下来,永远释放不了


解决方法::weak_ptr指针的引用

弱指针,不能单独存在,用于解决shared_ptr循环引用的问题

实现的原理是:当遇到这种原理是把指针给给_ptr, 但是引用计数是不增加的



在初始化一个unique_ptr或者shared_ptr时,我们最好优先使用std::make_unique和std::make_shared。


优点

效率更高

shared_ptr

  • 强引用, 用来记录当前有多少个存活的 shared_ptrs 正持有该对象. 共享的对象会在最后一个强引用离开的时候销毁( 也可能释放).
  • 弱引用, 用来记录当前有多少个正在观察该对象的 weak_ptrs. 当最后一个弱引用离开的时候, 共享的内部信息控制块会被销毁和释放 (共享的对象也会被释放, 如果还没有释放的话).

如果你通过使用原始的 new 表达式分配对象, 然后传递给 shared_ptr (也就是使用 shared_ptr 的构造函数) 的话, shared_ptr 的实现没有办法选择, 而只能单独的分配控制块:


1
2
auto p = new widget(); shared_ptr sp1{ p }, sp2{ sp1 };


智能指针_强引用_02

如果选择使用 make_shared

1
auto sp1 = make_shared(), sp2{ sp1 };


智能指针_强引用_03

内存分配的动作, 可以一次性完成. 这减少了内存分配的次数, 而内存分配是代价很高的操作.

关于两种方式的性能测试可以看这里 Experimenting with C++ std::make_shared

异常安全

看看下面的代码:


1
2
3
4
void F(const std::shared_ptr<Lhs>& lhs, 
  const std::shared_ptr<Rhs>& rhs) 
  { /* ... */ } F(std::shared_ptr<Lhs>(new Lhs
  ("foo")), std::shared_ptr<Rhs>(new Rhs("bar")));


C++ 是不保证参数求值顺序, 以及内部表达式的求值顺序的, 所以可能的执行顺序如下:

  1. new Lhs(“foo”))
  2. new Rhs(“bar”))
  3. std::shared_ptr
  4. std::shared_ptr

好了, 现在我们假设在第 2 步的时候, 抛出了一个异常 (比如 out of memory, 总之, Rhs 的构造函数异常了), 那么第一步申请的 Lhs 对象内存泄露了. 这个问题的核心在于, shared_ptr 没有立即获得裸指针.

我们可以用如下方式来修复这个问题.


1
2
3
auto lhs = std::shared_ptr<Lhs>(new Lhs("foo")); auto rhs = std::shared_ptr<Rhs>(new Rhs("bar")); F(lhs, rhs);


当然, 推荐的做法是使用 std::make_shared

1
F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));


缺点

构造函数是保护或私有时,无法使用 make_shared

make_shared 虽好, 但也存在一些问题, 比如, 当我想要创建的对象没有公有的构造函数时, make_shared 就无法使用了, 当然我们可以使用一些小技巧来解决这个问题, 比如这里 How do I call ::std::make_shared on a class with only protected or private constructors?

对象的内存可能无法及时回收

make_shared 只分配一次内存, 这看起来很好. 减少了内存分配的开销. 问题来了, weak_ptr 会保持控制块(强引用, 以及弱引用的信息)的生命周期, 而因此连带着保持了对象分配的内存, 只有最后一个 weak_ptr 离开作用域时, 内存才会被释放. 原本强引用减为 0 时就可以释放的内存, 现在变为了强引用, 若引用都减为 0 时才能释放, 意外的延迟了内存释放的时间. 这对于内存要求高的场景来说, 是一个需要注意的问题. 关于这个问题可以看这里 make_shared, almost a silver bullet

参考