C++基础之智能指针


前言

C++程序设计中会频繁的操作堆内存,堆内存的申请和释放都是由程序员自己管理,使用普通指针,容易造成内存泄露,二次释放等问题,使用智能指针能更好的管理堆内存。


提示:以下是本篇文章正文内容

一、智能指针有哪些

C++里面有四个智能指针,auto_ptr, unique_ptr, shared_ptr, weak_ptr。其中后三个是C++11支持,并且第一个auto_ptr已经被c++11弃用。

二、shared_ptr

1.特性

  1. 每一个shared_ptr的拷贝都指向相同的内存。
  2. shared_ptr使用引用计数机制,每当shared_ptr被拷贝时,引用计数会自动加1,每析构一个shared_ptr,引用计数会减1,当引用计数为0,也就是最后一个shared_ptr被析构的时候,shared_ptr指向的内存才会被释放。

简单来说,shared_ptr实现包含了两个部分:一个指向堆上创建的对象的裸指针:raw_ptr;一个指向内部隐藏的,共享的管理对象:share_count_object。这部分也就是当前这个堆对象被多少个shared_ptr引用了,简单来说就是引用计数。

2.初始化

可以通过构造函数、std::shared_ptr辅助函数和reset方法来初始化shared_ptr。
代码如下:

std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3;
p3.reset(new int(2));

if(p3) {
    std::cout << "p3 is not null" << std::endl;
}

注意要包含头文件 #include < memory >

也可以使用make_shared来构造智能指针(优先)
代码如下:

auto sp1 = std::make_shared<int>(100);
if(sp1)
{
    std::cout << "sp1 is not null \n";
}

不能通过原始指针直接赋值给一个智能指针

std::shared_ptr<int> p = new int(1);  //invalid
//编译报错:error: conversion from ‘int*’ to non-scalar type ‘std::shared_ptr<int>’ requested

3.关于shared_ptr的成员函数reset

  1. 调用p.reset(),不带参数,则表明释放该智能指针p指向的空间(也就是引用计数-1),并不指向新的内存空间。
  2. 若调用p.reset(q),说明将p的内置指针换位q,然后调用默认删除器delete释放p之前所指的空间。
  3. 若调用p.reset(q,d),与2相同,但是不是使用默认删除器,而是使用指定的删除器d来释放之前所指的空间 。
  4. 若调用p.reset(q,d,a),第三个参数值指定构造器。

参考文档:cppreference

4.引用计数的例子

代码如下:

std::shared_ptr<int> p1;
p1.reset(new int(1));
std::shared_ptr<int> p2 = p1;

//use_count() == 2
std::cout << "p2.user_count() = " << p2.use_count() << std::endl;

p1.reset();
std::cout << "p1.reset() \n";

//use_count() == 1
std::cout << "p2.user_count() = " << p2.use_count() << std::endl;

if(!p1) {
    std::cout << "p1 is empty\n";
}
if(!p2) {
    std::cout << "p2 is empty\n";
}

p2.reset();
std::cout << "p2 reset()\n";
//use_count() == 0
std::cout << "p2.user_count() = " << p2.use_count() << std::endl;

if(!p2) {
    std::cout << "p2 is empty\n";
}

打印
在这里插入图片描述

5.获取原始指针

std::shared_ptr<int> ptr(new int(8));
int *p = ptr.get();

虽然给出了获取原始指针的接口,但是最好不要去调用它,因为这可能充满着为止的危险。这里有几个约定:

  1. 不要保存get()返回的裸指针。无论是保存为裸指针还是智能指针,都有可能让其变成空悬指针和独立指针的危险。
  2. 不要delete get()返回的指针,这有可能会引发double free的错误。

6.构造时指定删除器

void DeleteIntPtr(int *p)
{
    std::cout << "call delete int ptr function \n";
    delete p;
}

int main()
{
	//指定删除器
    std::shared_ptr<int> p(new int(1), DeleteIntPtr);
    //通过lambda表达式
    std::shared_ptr<int> p1(new int(1), [](int *p){
        std::cout << "call lambda delete p \n";
        delete p;
    });

    /*
    * 智能指针在栈上,后申请的先释放???
    *   call lambda delete p 
    *   call delete int ptr function
    */

   p.reset();
   p1.reset();

   /*
   *
   *    call delete int ptr function 
   *    call lambda delete p 
   */

    //shared_ptr默认删除器不支持数组对象,需要自己指定删除器
    std::shared_ptr<int> p(new int[10], [](int *p){
        delete[] p;
    });
}

7.注意事项

class A
{
    public:
    std::shared_ptr<A> GetSelf()
    {
        return std::shared_ptr<A>(this);
    }
    ~A()
    {
        std::cout << "Deconstruction A" <<  std::endl;
    }
};

class B:public std::enable_shared_from_this<B>
{
    public:
    std::shared_ptr<B> GetSelf()
    {
        return shared_from_this();
    }
    ~B()
    {
        std::cout << "Deconstruction B" <<  std::endl;
    }
};

class C;
class D;

class C
{
    public:
    std::shared_ptr<D>  dptr;
    ~C() {
        std::cout << "C is deleted" << std::endl;
    }
};

class D
{
    public:
    std::shared_ptr<C>  cptr;
    ~D() {
        std::cout << "D is deleted" << std::endl;
    }
};

//1.不要用一个原始指针初始化多个shared_ptr. 会引发double free
int *ptr = new int;
std::shared_ptr<int> p1(ptr);
std::shared_ptr<int> p2(ptr);
//打印:
//free(): double free detected in tcache 2

//2.不要在函数实参中创建shared_ptr
function(shared_ptr<int>(new int), g());
//对于函数参数的计算顺序在不同编译器不同的约定下可能是不一样的,一般是从右到左,但也可能从左到右
//所以,可能先new int, 然后在调用g(),如果g()发生异常,而shared_ptr还没有创建,则int内存泄露了,应该先创建智能指针,
shared_ptr<int> p(new int);
function(p, g());

//3.通过shared_from_this()返回this指针,不要将this指针作为shared_ptr返回出来,因为this指针本质上是一个裸指针,因此可能会导致重复析构
std::shared_ptr<A> sp1(new A);
std::shared_ptr<A> sp2 = sp1->GetSelf();
//打印:
//Deconstruction A
//Deconstruction A
//free(): double free detected in tcache 2

//正确的做法:让目标类通过继承std::enable_shared_from_this类,然后通过父类的成员函数shared_from_this()来返回this的shared_ptr
std::shared_ptr<B> sp1(new B);
std::shared_ptr<B> sp2 = sp1->GetSelf();
//打印:
//Deconstruction B
//析构一次,不会发生double free的错误。

//避免循环引用  
{
    std::shared_ptr<C> cp(new C);
    std::shared_ptr<D> dp(new D);
    cp->dptr = dp;
    dp->cptr = cp;
    std::cout << "cp.user_count() = " << cp.use_count() << std::endl; //2
    std::cout << "dp.user_count() = " << dp.use_count() << std::endl; //2
}
std::cout << "main leave \n"; 
//循环引用导致cp dp退出了作用域,引用计数器减为1,并没有减为0,所以都没有析构。
//解决办法是把A和B的任何一个成员变量改为weak_ptr。

三、unique_ptr

1.特性

  1. 独占式智能指针
  2. 不能做赋值操作
  3. 可以通过move转移

2.初始化

std::unique_ptr<int> p(new int);
//std::unique_ptr<int> p1 = p;   //报错,不允许复制。
std::unique_ptr<int> p1 = std::move(p); //正确,p对指针的所有权转移给p1,所以p为空了。
if(!p) {
    std::cout << "p is empty\n";   //打印这一句,因为通过move把p指向的内存空间和引用计数转移给了p1,并释放了p的资源。
}
if(!p1) {
    std::cout << "p1 is empty\n";  //不会打印这一句
}

c++14引入了make_unique来初始化一个unique_ptr指针

auto p = std::make_unique<int>(10);

3.shared_ptr 和unique_ptr的区别

  1. 在C++17之前,不能使用unique_ptr指向一个数组,但是C++17之后是可以的。
//shared_ptr 和 unique_ptr的区别
//1.数组  测试可以!!难道那个标准把这个加入 了?? c++17
std::unique_ptr<int []> p(new int[10]);
p[9] = 9;
std::shared_ptr<int []> p2(new int[10]);
p2[9] = 9;
  1. 删除器
std::shared_ptr<int> ptr(new int(1), [](int *p){delete  p;}); //valid
// std::unique_ptr<int> ptr1(new int(1), [](int *p){delete  p;}); //invalid
std::unique_ptr<int, void(*)(int*)> ptr2(new int(1), [](int *p){delete p;}); //vaild
//第二句会出现编译错误
//error: no matching function for call to ‘std::unique_ptr<int>::unique_ptr(int*, main()::<lambda(int*)>)’

四、weak_ptr

1.特性

  1. 弱引用,不控制对象的生命周期,主要是解决shared_ptr出现的循环引用的问题。如下面的代码:
class A;
class B;

class A
{
    public:
    std::weak_ptr<B>  bptr;
    ~A()
    {
        std::cout << "Deconstruction A" <<  std::endl;
    }
};

class B
{
    public:
   	std::shared_ptr<A>  aptr;
    ~B()
    {
        std::cout << "Deconstruction B" <<  std::endl;
    }
};

int main()
{
	{
        std::shared_ptr<A> ap(new A);
        std::shared_ptr<B> bp(new B);
        bp->aptr = ap;
        ap->bptr = bp;
    }
    std::cout << "main leave \n";
}

代码解析:由于bptr是weak_ptr,当执行ap->bptr=bp的时候,它并不会增加引用计数,所以bp的引用计数还是为1,当离开作用域之后,bp的引用计数减为0,B的指针会被析构,析构之后ap的引用计数减为1,当离开作用域的时候ap的引用计数减为0,然后A也被析构。不会发生内存泄露的问题。

  1. weak_ptr没有重载*和->运算符,因为它不共享指针,不能操作资源。
  2. weak_ptr纯粹作为一个旁观者来监视shared_ptr中管理的资源是否存在。
  3. weak_ptr使用lock函数来获得shared_ptr的智能指针。
  4. weak_ptr可以返回this指针。解决double free的问题,如下面代码:
class B:public std::enable_shared_from_this<B>
{
    public:
    std::shared_ptr<B> GetSelf()
    {
        return shared_from_this();
    }
    ~B()
    {
        std::cout << "Deconstruction B" <<  std::endl;
    }
};

代码解析:通过派生
std::enable_shared_from_this类,并通过其方法shared_from_this来返回指针,原因是
std::enable_shared_from_this类中有一个weak_ptr,这个weak_ptr用来观察this智能指针,调用
shared_from_this()方法是,会调用内部这个weak_ptr的lock()方法,将所观察的shared_ptr返回。

2.基本用法

  1. 使用user_count()获取当前观察资源的引用计数
std::shared_ptr<int> sp(new int(10));
std::weak_ptr<int> wp(sp); 
std::cout << wp.use_count() << std::endl; //结果讲输出1
  1. 使用expired()方法判断所观察资源是否已经释放
std::shared_ptr<int> sp(new int(10)); 
std::weak_ptr<int> wp(sp); 
if(wp.expired()) 
	std::cout << "weak_ptr无效,资源已释放"; 
else
	std::cout << "weak_ptr有效";
  1. 使用lock()方法获取监视的shared_ptr
std::weak_ptr<int> gw;

void f()
{
    if(gw.expired())
    {
        std::cout << "gw 资源已经被释放\n";
    }else{
        auto spt = gw.lock();
        std::cout << "gw有效, *spt = " << *spt << std::endl;
    }
}

int main()
{
	{
      auto sp = std::shared_ptr<int>(new int(42));
       gw  = sp;
       f();
   }
   f();
}

打印:
在这里插入图片描述

3.weak_ptr使用注意事项

第一段代码:

std::weak_ptr<int> wp;
{
    std::shared_ptr<int> sp(new int(1));
    std::cout << "sp.user_count() = " << sp.use_count() << std::endl; //1
    wp = sp;
    std::cout << "sp.user_count() = " << sp.use_count() << std::endl; //1
    std::cout << "wp.user_count() = " << wp.use_count() << std::endl; //1
    std::shared_ptr<int> sp_ok = wp.lock();
    std::cout << "sp_ok.user_count() = " << sp_ok.use_count() << std::endl; //2
    std::cout << "wp.user_count() = " << wp.use_count() << std::endl; //2
}
std::cout << "wp.user_count() = " << wp.use_count() << std::endl; //0
if(wp.expired()) 
{ 
    std::cout << "shared_ptr is destroy" << std::endl; 
} else { 
    std::cout << "shared_ptr no destroy" << std::endl; 
}
std::shared_ptr<int> sp_null = wp.lock();

打印输出:
在这里插入图片描述

代码解析:
sp和sp_ok在离开作用域后,引用计数减为0,指向的资源被释放,所以最后通过弱引用只能指针wp打印观察的sp的user_count()计数为0,sp被销毁,所以在使用wp前需要调用wp.expired()函数判断一下。

第二段代码:

std::weak_ptr<int> wp;
std::shared_ptr<int> sp_ok;
{
    std::shared_ptr<int> sp(new int(1));
    std::cout << "sp.user_count() = " << sp.use_count() << std::endl; //1
    wp = sp;
    std::cout << "sp.user_count() = " << sp.use_count() << std::endl; //1
    std::cout << "wp.user_count() = " << wp.use_count() << std::endl; //1
    sp_ok = wp.lock();
    std::cout << "sp_ok.user_count() = " << sp_ok.use_count() << std::endl; //2
    std::cout << "wp.user_count() = " << wp.use_count() << std::endl; //2
}
std::cout << "sp_ok.user_count() = " << sp_ok.use_count() << std::endl; //1
std::cout << "wp.user_count() = " << wp.use_count() << std::endl; //1 
if(wp.expired()) 
{ 
    std::cout << "shared_ptr is destroy" << std::endl; 
} else { 
    std::cout << "shared_ptr no destroy" << std::endl; 
}

打印输出:
在这里插入图片描述
代码解析:
sp离开了作用域,引用计数减1,但是sp_ok尚未离开其作用域 ,所以wp最后打印的user_count()为1,智能指针sp尚未被销毁。

五、智能指针安全性问题

在多线程环境下:

  1. 引用计数本事是安全的。
  2. 多线程操作同一个shared_ptr对象是不安全的。(引用传递或者指针传递)
  3. 多线程操作不同的shared_ptr,但是指向同一个资源,是安全的。(按值传递)
  4. 多线程的各自的sp管理者同一个资源对象A,需要A自己保证多线程安全。

参考:智能指针的线程安全

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值