【C++】智能指针

1. 什么是智能指针

在C++中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露。在编程中很容易造成内存泄漏或者指针非法引用问题,解决这个问题最有效的方法是使用智能指针(smart pointer)。
智能指针的主要思想是,将原始指针包装成一个对象,当对象离开作用域后,会自动调用析构函数,而析构函数中会自动释放原始指针指向的内容。

C++11中提供了三种智能指针,使用这些智能指针时需要引用头文件

std::shared_ptr:共享的智能指针
std::unique_ptr:独占的智能指针
std::weak_ptr:弱引用的智能指针,它不共享指针,不能操作资源,是用来监视shared_ptr的。

2. unique_ptr 独占智能指针
  • std::unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,可以通过它的构造函数初始化一个独占智能指针对象,但是不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr。
  • unique_ptr指定删除器和shared_ptr指定删除器是有区别的,unique_ptr指定删除器的时候需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器。
unique_ptr<string> p1(new string("I AM Luo;"));
unique_ptr<string> p2(new string("I AM Yang;"));
cout << "p1 : " << p1.get() << endl;
cout << "p2 : " << p2.get() << endl;
cout << "p1 : " << *p1 << endl;
cout << "p2 : " << *p2 << endl;
// p1 = p2; 不允许,会报错
p1 = move(p2);
cout << "p1 : " << p1.get() << endl;
cout << "p2 : " << p2.get() << endl;
cout << "p1 : " << *p1 << endl;
// cout << "p2 : " << *p2 << endl;
// 不报错,但是程序会终止。
if(p2 != nullptr){
    cout << "p2 : " << *p2 << endl;
}else{
    cout << "p2 : nullptr " << endl;
}
// unique_ptr<string> p3(p1); 不允许
unique_ptr<string> p3(move(p1));
if(p1 != nullptr){
    cout << "p1 : " << *p1 << endl;
}else{
    cout << "p1 : nullptr " << endl;
}
cout << "p3 : " << p3.get() << endl;
cout << "p3 : " << *p3 << endl;

结果如图
在这里插入图片描述

主动释放对象
up = nullptr ;//释放up指向的对象,将up置为空
或  up = NULL; //作用相同 

放弃对象控制权
up.release();    //放弃对象的控制权,返回指针,将up置为空,不会释放内存

重置
up.reset()//参数可以为 空、内置指针,先将up所指对象释放,然后重置up的值
交换
up.swap(up1);  //将智能指针up 和up1管控的对象进行交换
3. shared_ptr 强引用指针

多个shared_ptr指向同一处资源,当所有shared_ptr都全部释放时,该处资源才释放。(有某个对象的所有权(访问权,生命控制权) 即是 强引用,所以shared_ptr是一种强引用型指针。

缺陷:模型循环依赖(互相引用或环引用)时,计数会不正常

shared_ptr 初始化方式:

  • 1.裸指针直接初始化,但不能通过隐式转换来构造
  • 2.允许移动构造,也允许拷贝构造
  • 3.通过make_shared构造
  std::shared_ptr<Frame> f(new Frame());              
  // 裸指针直接初始化
  // std::shared_ptr<Frame> f1 = new Frame();           
  // Error,explicit禁止隐式初始化
  std::shared_ptr<Frame> f2(f);                       
  // 拷贝构造函数
  std::shared_ptr<Frame> f3 = f;
  // 拷贝构造函数  

shared_ptr 内存详情:
在这里插入图片描述

shared_ptr包含了一个指向对象的指针和一个指向控制块的指针。每一个由std::shared_ptr管理的对象都有一个控制块,它除了包含引用计数之外,还包含了自定义删除器的副本和分配器的副本以及其他附加数据。

(1)std::make_shared总是创建一个控制块
(2)从具备所有权的指针出发构造一个std::shared_ptr时,会创建一个控制块
(3)当std::shared_ptr构造函数使用裸指针作为实参时,会创建一个控制块。这意味从同一个裸指针出发来构造不止一个std::shared_ptr时会创建多重的控制块,也意味着对象会被析构多次。如果想从一个己经拥有控制块的对象出发创建一个std::shared_ptr,可以传递一个shared_ptr或weak_ptr而非裸指针作为构造函数的实参,这样则不会创建新的控制块。

经验:

  • 尽量避免将一个裸指针传递给std::shared_ptr的构造函数,常用的替代手法是使用std::make_shared。如果必须将一个裸指针传递给shared_ptr的构造函数,就直接传递new运算符的结果,而非传递一个裸指针变量,不然会报错。
  • 不要将this指针返回给shared_ptr。 当希望将this指针托管给shared_ptr时,类需要继承自std::enable_shared_from_this,并且从shared_from_this()中获得shared_ptr指针。
  • 不要使用相同的原始指针作为实参来创建多个shared_ptr对象。可以使用拷贝构造或者直接使用重载运算符=进行操作。
  • 慎用get()返回的指针,返回只能指向对象对应的裸指针,get返回的指针不能delete,否则可能会异常。
  • 避免循环引用:能够导致内存泄露
//1. 陷阱:用同一裸指针创建多个shared_ptr
    //1.1 错误做法
    auto pw = new Widget;
    std::shared_ptr<Widget> spw1(pw); //强引用计数为1,为pw创建一个控制块
    //std::shared_ptr<Widget> spw2(pw); //强引用计数为1,为pw创建另一个新的控制块,会导致多次析构
    auto sp = new Widget;
    func(shared_ptr<Widget>(sp)); //慎用裸指针,sp将在func结束后被释放!
    //1.2 正确做法
    std::shared_ptr<Widget> spw3(spw1); 
    //ok,pw的强引用计数为2。使用与spw1同一个控制块。
    std::shared_ptr<Widget> spw4(new Widget); 
    //将new的结果直接传递给shared_ptr
    std::shared_ptr<Widget> spw5 = std::make_shared<Widget>();
    //强烈推荐的做法!
    //2. 陷阱:在函数实参中创建shared_ptr
    //2.1 shared_ptr与异常安全问题
    //由于参数的计算顺序因编译器和调用约定而异。假定按如下顺序计算
    //A.先前new int,然后funcException();
    //B.假设恰好此时funcException产生异常。
    //C.因异常出现shared_ptr还来不及创建,于是int内存泄露
    demo(shared_ptr<int>(new int(100)), funcException());

    //2.2 正确做法
    auto p1 = std::make_shared<int>(100);
    demo(p1, funcException());

make_shared的优缺点

  • 避免代码冗余:创建智能指针时,被创建对象的类型只需写1次,而用new创建智能指针时,需要写2次;
  • 异常安全:make系列函数可编写异常安全代码,改进了new的异常安全性;
  • 提升性能:编译器有机会利用更简洁的数据结构产生更小更快的代码。使用make_shared时会一次性进行内存分配,该内存单块(single chunck)既保存了T对象又保存与其相关联的控制块。而直接使用new表达式,除了为T分配一次内存,还要为与其关联的控制块再进行一次内存分配。
    But:
  • 所有的make系列函数都不允许自定义删除器
  • make系列函数创建对象时,不能接受{}初始化列表(这是因为完美转发的转发函数是个模板函数,它利用模板类型进行推导。 因此无法将{}推导为initializer_list)。换言之,make系列只能将圆括号内的形参完美转发;
  • 自定义内存管理的类(如重载了operator new和operator delete),不建议使用make_shared来创建。 因为:重载operator new和operator delete时,往往用来分配和释放该类精确尺寸(sizeof(T))的内存块;而make_shared创建的shared_ptr,是一个自定义了分配器(std::allocate_shared)和删除器的智能指针,由allocate_shared分配的内存大小也不等于上述的尺寸,而是在此基础上加上控制块的大小;
  • 对象的内存可能无法及时回收。 因为:make_shared只分配一次内存,减少了内存分配的开销,使得控制块和托管对象在同一内存块上分配。而控制块是由shared_ptr和weak_ptr共享的,因此两者共同管理着这个内存块(托管对象+控制块)。当强引用计数为0时,托管对象被析构(即析构函数被调用),但内存块并未被回收,只有等到最后一个weak_ptr离开作用域时,弱引用也减为0才会释放这块内存块。原本强引用减为0时就可以释放的内存, 现在变为了强引用和弱引用都减为0时才能释放, 意外的延迟了内存释放的时间。这对于内存要求高的场景来说, 是一个需要注意的问题。
#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <mutex>
struct Base
{
    Base() { std::cout << "  Base::Base()\n"; }
    // Note: non-virtual destructor is OK here
    ~Base() { std::cout << "  Base::~Base()\n"; }
};
 
struct Derived: public Base
{
    Derived() { std::cout << "  Derived::Derived()\n"; }
    ~Derived() { std::cout << "  Derived::~Derived()\n"; }
};
 
void thr(std::shared_ptr<Base> p)
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::shared_ptr<Base> lp = p; // thread-safe, even though the
                                  // shared use_count is incremented
    {
        static std::mutex io_mutex;
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << "local pointer in a thread:\n"
                  << "  lp.get() = " << lp.get()
                  << ", lp.use_count() = " << lp.use_count() << '\n';
    }
}
 
int main()
{
    std::shared_ptr<Base> p = std::make_shared<Derived>();
    std::cout << "Created a shared Derived (as a pointer to Base)\n"
              << "  p.get() = " << p.get()
              << ", p.use_count() = " << p.use_count() << '\n';
    std::thread t1(thr, p), t2(thr, p), t3(thr, p);
    p.reset(); // release ownership from main
    std::cout << "Shared ownership between 3 threads and released\n"
              << "ownership from main:\n"
              << "  p.get() = " << p.get()
              << ", p.use_count() = " << p.use_count() << '\n';
    t1.join(); t2.join(); t3.join();
    std::cout << "All threads completed, the last one deleted Derived\n";
}

在这里插入图片描述(shared_ptr)的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化。

4. weak_ptr 强引用指针

weak_ptr的创建

  1. 直接初始化:weak_ptr wp(sp); //其中sp为shared_ptr类型
  2. 赋值: wp1 = sp; //其中sp为shared_ptr类型
        wp2 = wp1; //其中wp1为weak_ptr类型

常用操作

函数说明
use_count()获取当前控制块中资源的强引用计数。
expired()判断所观测的资源是否失效(即己经被释放),即use_count是否为0。(1)shared_ptr sp1 = wp.lock();//如果wp失效,则sp为空(其中wp为weak_ptr类型)(2)shared_ptr sp2(wp); //如果wp失效,则抛std::bad_weak_ptr异常。
lock()获取当前控制块中资源的强引用计数。
use_count()获取所监视资源的shared_ptr,如shared_ptr sp = wp.lock(); //wp为weak_ptr类型。
reset()重置weak_ptr,影响弱引用计数。

注意

  1. weak_ptr不是独立的智能指针,它是shared_ptr的助手,只是监视shared_ptr管理的资源是否释放,不会影响强引用计数,不能管理资源。
  2. weak_ptr没有重载操作符*和->,因为它不共享指针,不能操作资源。
  3. weak_ptr主要用来代替可能空悬的shared_ptr。

应用

观察者模式

  1. 观察者模式是在subject状态发生改变时,通知观察者的一种设计模式。
  2. 在多数实现中,每个subject持有指向观察者的指针,这使得当subject状态改变时可以很容易通知观察者。
  3. subject不会控制其观察者的生存期,因此应该是持有观察者的weak_ptr指针。同时在subject的使用某个指针时,可以先确定是否空悬。

解决循环引用

A、B、C三个对象的数据结构中,A和C共享B的所有权,因此各持有一个指向B的std::shared_ptr。假设有一个指针从B指回A,则该指针的类型应为weak_ptr,而不能是裸指针或shared_ptr,原因如下:

①假如是裸指针,当A被析构时,由于C仍指向B,所以B会被保留。但B中保存着指向A的空悬指针(野指针),而B却检测不出来,但解引用该指针时会产生未定义行为。
②假如是shared_ptr时。由于A和B相互保存着指向对方的shared_ptr,此时会形成循环引用,从而阻止了A和B的析构。
③假如是weak_ptr,这可以避免循环引用。假设A被析构,那么B的回指指针会空悬,但B可以检测到这一点,同时由于该指针是weak_ptr,不会影响A的强引用计数,因此当shared_ptr不再指向A时,不会阻止A的析构。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值