C++中的智能指针

一、智能指针的作用-管理堆内存

1、什么是智能指针?

是一个类,用来存储指针(指向动态分配对象的指针)。

2、智能指针的作用?

C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

3、生命周期?

对于编译器来说,智能指针实际上是一个栈对象,并非指针类型,在栈对象生命期即将结束时,智能指针通过析构函数释放有它管理的堆内存。

二、智能指针的用法

智能指针在C++11版本之后提供,包含在头文件中,shared_ptr、unique_ptr、weak_ptr

1、shared_ptr的使用

shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每关联一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的对象内存。shared_ptr内部的引用计数是线程安全的,但是shared_ptr的读取需要加锁。

1.1、初始化

注意不要用一个原始指针初始化多个shared_ptr,否则会造成重复释放同一内存。

  • make_shared

//make_shared分配一块int类型大小的内存,并值初始化为100
//返回值是shared_ptr类型,因此可以直接赋值给sp
shared_ptr<int> sp = make_shared<int>(100);

  • new
    接受指针参数的只能指针构造函数是explicit的,因此,我们不能将一个内置指针隐式转化为一个只能指针,必须使用直接初始化形式

//错误! 不会进行隐式转换,类型不符合
shared_ptr<int> sp1 = new int(100);
//正确,直接初始化调用构造函数
shared_ptr<int> sp2(new int(100000));

1.2、shared_ptr的操作
  • p.get()
    返回p保存的指针

  • swap(p,q)
    交换p、q中保存的指针

  • shared_ptr p(q)
    p是q的拷贝,它们指向同一块内存,拷贝使得对象的引用计数增加1

  • p = q
    用q为p赋值,之后p、q指向同一块内存,q引用计数+1,p(原来内存空间的)引用计数-1

  • p.use_count()
    返回与p共享对象的智能指针数量

  • shared_ptr p(q,d)
    q是一个可以转换为T*的指针,d是一个可调用对象(作为删除器),p接管q所指对象的所有权,用删除器d代替delete释放内存

  • p.reset()
    相当于释放当前所控制的对象,对象的引用计数减1

  • p.reset( q )
    释放当前所控制的对象,然后接管q所指的对象

  • p.reset(p,d)
    将p重置为p(的值)并使用d作为删除器

1.3、关联与独立

多个共享指针指向同一个空间,它们的关系可能是关联(我们所期望的正常关系)或是独立的(一种错误状态)

    shared_ptr<int> sp1(new int(10));
    shared_ptr<int> sp2(sp1), sp3;
    sp3 = sp1;
    
    shared_ptr<int> sp4(sp1.get()); //一个典型的错误用法
    
    cout << sp1.use_count() << " " << sp2.use_count() << " " 
    << sp3.use_count() << " " << sp4.use_count() << endl;
    //输出 3 3 3 1

sp1,sp2,sp3是相互关联的共享指针,共同控制所指内存的生存期,sp4虽然指向同样的内存,却是与sp1,sp2,sp3独立的,sp4按自己的引用计数来关联内存的释放。
只有用一个shared_ptr为另一个shared_ptr赋值时,才将这连个共享指针关联起来,直接使用地址值会导致各个shared_ptr独立。

2、unique_ptr的使用

2.1、auto_ptr,已被C++11抛弃

auto_ptr 采用所有权模式,能够方便的管理单个堆内存对象。

void TestAutoPtr2() {
  std::auto_ptr<Simple> my_memory(new Simple(1));
  if (my_memory.get()) {
    std::auto_ptr<Simple> my_memory2;   // 创建一个新的 my_memory2 对象
    my_memory2 = my_memory;             // 复制旧的 my_memory 给 my_memory2
    my_memory2->PrintSomething();       // 输出信息,复制成功
    my_memory->PrintSomething();        // 崩溃
  }
}

最终如上代码导致崩溃,罪魁祸首是“my_memory2 = my_memory”,这行代码,my_memory2 完全夺取了 my_memory 的内存管理所有权,导致 my_memory 变空指针,最后使用时导致崩溃

所以auto_ptr的缺点是:存在潜在的内存崩溃问题!

C++11以后,采样unique_ptr ,它优于允许两种赋值的auto_ptr 。

2.2、unique_ptr
  • unique_ptr“唯一”拥有其所指对象(与share_ptr相反),同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。
  • unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。
  • unique_ptr指针与其所指对象所有权关系:创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过move方法移动语义转移所有权
  • 无法进行复制构造和赋值操作
#include <iostream>
#include <memory>

int main() {
    {
        std::unique_ptr<int> uptr(new int(10));  //绑定动态对象
        //std::unique_ptr<int> uptr2 = uptr;  //不能賦值
        //std::unique_ptr<int> uptr2(uptr);  //不能拷貝
        std::unique_ptr<int> uptr2 = std::move(uptr); //轉換所有權
        uptr2.release(); //释放所有权
    }
    //超過uptr的作用域,內存釋放
}
//智能指针的创建  
unique_ptr<int> u_i; 	//创建空智能指针
u_i.reset(new int(3)); 	//绑定动态对象  
unique_ptr<int> u_i2(new int(4));//创建时指定动态对象
unique_ptr<T,D> u(d);	//创建空 unique_ptr,执行类型为 T 的对象,用类型为 D 的对象 d 来替代默认的删除器 delete

//所有权的变化  
int *p_i = u_i2.release();	//释放所有权  
unique_ptr<string> u_s(new string("abc"));  
unique_ptr<string> u_s2 = std::move(u_s); //所有权转移(通过移动语义),u_s所有权转移后,变成“空指针” 
u_s2.reset(u_s.release());	//所有权转移
u_s2=nullptr;//显式销毁所指对象,同时智能指针变为空指针。与u_s2.reset()等价

3、weak_ptr的使用

weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况

3.1、weak_ptr 由来----解决 shared_ptr 因循环引有不能释放资源的问题

对于引用计数法实现的计数,总是避免不了循环引用(或环形引用)的问题,shared_ptr也不例外。

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class ClassB;

class ClassA
{
public:
    ClassA() { cout << "ClassA Constructor..." << endl; }
    ~ClassA() { cout << "ClassA Destructor..." << endl; }
    shared_ptr<ClassB> pb;  // 在A中引用B
};

class ClassB
{
public:
    ClassB() { cout << "ClassB Constructor..." << endl; }
    ~ClassB() { cout << "ClassB Destructor..." << endl; }
    shared_ptr<ClassA> pa;  // 在B中引用A
};

int main() 
{
    shared_ptr<ClassA> spa = make_shared<ClassA>();
    shared_ptr<ClassB> spb = make_shared<ClassB>();
    spa->pb = spb;
    spb->pa = spa;

    std::cout << "spa use_cout:" << spa.use_count() << " spb use_cout:" << spb.use_count() << std::endl;  //spa: 2 spb:2

  // 函数结束,思考一下:spa和spb会释放资源么? 超过作用于时引用计数减一,此时为2,减一后不为0,所以内存不释放
}

从上面代码中,ClassA和ClassB间存在着循环引用,从运行结果中我们可以看到:当main函数运行结束后,spa和spb管理的动态资源并没有得到释放,产生了内存泄露。为了解决类似这样的问题,C++11引入了weak_ptr,来打破这种循环引用。

class ClassB;

class ClassA
{
public:
    ClassA() { cout << "ClassA Constructor..." << endl; }
    ~ClassA() { cout << "ClassA Destructor..." << endl; }
    weak_ptr<ClassB> pb;  // 在A中引用B,weak_ptr
};

class ClassB
{
public:
    ClassB() { cout << "ClassB Constructor..." << endl; }
    ~ClassB() { cout << "ClassB Destructor..." << endl; }
    weak_ptr<ClassA> pa;  // 在B中引用A,weak_ptr
};

int main() {
    shared_ptr<ClassA> spa = make_shared<ClassA>();
    shared_ptr<ClassB> spb = make_shared<ClassB>();
    spa->pb = spb;
    spb->pa = spa;
    // 函数结束,思考一下:spa和spb会释放资源么?因为没改变shared_ptr的引用计数,此时引用计数为1,超过作用域后自动释放
}
3.2、weak_ptr 初始化

weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。由于是弱共享,它的构造不会引起指针引用计数的增加。

3.3、weak_ptr 如何操作资源
  • expired 用于检测所管理的对象是否已经释放, 如果已经释放, 返回 true; 否则返回 false.
  • lock 用于获取所管理的对象的强引用(shared_ptr). 如果 expired 为 true, 返回一个空的 shared_ptr; 否则返回一个 shared_ptr, 其内部对象指向与 weak_ptr 相同.lock成功后shared_ptr的共享的对象的引用计数+1
  • use_count 返回与 shared_ptr 共享的对象的引用计数.
  • reset 将 weak_ptr 置空.
  • weak_ptr 支持拷贝或赋值, 但不会影响对应的 shared_ptr 内部对象的计数。只有lock()会影响。
#include <iostream>
#include <memory>

int main()     {
        std::shared_ptr<int> fsPtr(new int(5));
        std::weak_ptr<int> fwPtr = fsPtr;

        //weak_ptr不会改变shared_ptr,但是会和shared_ptr的引用保持一致
        std::cout << "fsPtr use_count:" << fsPtr.use_count() << " fwPtr use_count:" << fwPtr.use_count() << std::endl;

        //fwPtr.lock()后会该变shared_ptr的引用计数(+1)
        //std::shared_ptr<int> fsPtr2 = fwPtr.lock();
        //std::cout << "fsPtr use_count:" << fsPtr.use_count() << " fwPtr use_count:" << fwPtr.use_count() << std::endl;

        //编译报错,weak_ptr没有重载*,->操作符,因此不可直接通过weak_ptr使用对象,只能通过lock()使用shared_ptr来操作
        //std::cout << " number is " << *fwPtr << std::endl;

        fsPtr.reset();
        if (fwPtr.expired())
        {
            std::cout << "shared_ptr object has been destory" << std::endl;
        }

        std::shared_ptr<int> fsPtr3 = fwPtr.lock();                //fsPtr3为NULL
        std::cout << " number is " << *fsPtr3 << std::endl;     //运行时中断
    }
weak_ptr<T> w;	 	//创建空 weak_ptr,可以指向类型为 T 的对象
weak_ptr<T> w(sp);	//与 shared_ptr 指向相同的对象,shared_ptr 引用计数不变。T必须能转换为 sp 指向的类型
w=p;				//p 可以是 shared_ptr 或 weak_ptr,赋值后 w 与 p 共享对象
w.reset();			//将 w 置空
w.use_count();		//返回与 w 共享对象的 shared_ptr 的数量
w.expired();		//若 w.use_count() 为 0,返回 true,否则返回 false
w.lock();			//如果 expired() 为 true,返回一个空 shared_ptr,否则返回非空 shared_ptr

4、小结

  • 智能指针主要用于解决忘记主动释放内存而造成内存泄露问题。

  • auto_ptr符合智能指针的初衷,每个指针都有析构函数用于释放内存。但是缺点在于所有权转移时,使用赋值操作会旧指针悬空。

  • unique_ptr吸取教训,特点是指针与内存一对一。无法进行复制构造和赋值操作,可以进行移动构造和移动赋值操作。

  • 能否多个指针指向一块内存呢?那就使用shared_ptr,内置了引用计数器,记录着多少个指针引用了这块内存。只有计数器为0时内存才会被释放。但是缺点在于,如果在2个类中各自声明了shared_ptr 并互相引用对方类,则可能会造成计数器总不为0,内存无法释放的问题。

  • 因此,又出现了weak_ptr,用于弱声明,接近循环引用的问题,并且监视着shared_ptr的使用情况。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值