C++智能指针

文章目录

  • 前言
  • 一、为什么需要智能指针
  • 二、强引用共享式指针shared_ptr
      • shared_ptr的内存模型
      • shared_ptr的基本用法和常用函数
            • 初始化操作
            • reset()函数详解
            • get()函数详解
      • 使用shared_ptr应注意的问题
            • 不要用一个原始指针初始化多个shared_ptr
            • 不要在函数实参中创建shared_ptr
            • 通过shared_from_this()返回this指针
            • 避免循环引用
  • 三、独占的智能指针 unique_ptr
      • unique_ptr是一个独占型的智能指针,不能将其赋值给另一个unique_ptr
      • unique_ptr可以指向一个数组
      • unique_ptr需要确定删除器的类型
  • 四、弱引用的智能指针 weak_ptr
      • weak_ptr指针介绍
      • weak_ptr的基本用法
            • use_count
            • expired()
            • lock()
      • 解决循环引用问题


前言

在正式学习智能指针内容之前,我们先要了解下,为什么要使用智能指针,智能指针解决了什么问题。

一、为什么需要智能指针

智能指针主要解决以下问题:

  1. 内存泄漏:内存手动释放,使用智能指针可以自动释放
  2. 共享所有权指针的传播和释放,比如多线程使用同一个对象时析构问题
    C++智能指针_第1张图片
    多线程进行数据帧的拷贝,每拷贝一次,引用计数加1,释放一次减1,直到为0,进行释放。
    此时,我们不仅引出个疑问,智能指针在多线程中是安全的吗?
    在多线程中,引用计数是安全的(原子技术),
    指向对象数据,如果修改,它是不安全的
    (如果要数据数据安全,还要加锁机制)

其中,C++里面的四个智能指针: auto_ptr,shared_ptr,unique_ptr, weak_ptr 其中后三个是C++11支持,并且
第一个已经被C++11弃用。
几个指针的特点:
unique_ptr独占对象的所有权,由于没有引用计数,因此性能较好。
shared_ptr共享对象的所有权,但性能略差。
weak_ptr配合shared_ptr,解决循环引用的问题。

接下来我们先学习shared_ptr。

二、强引用共享式指针shared_ptr

shared_ptr的内存模型

C++智能指针_第2张图片
由图可见,shared_ptr 内部包含两个指针,一个指向对象,另一个指向控制块(control block),控制块中包含一个引用计数(reference count), 一个弱计数(weak count)和其它一些数据。
接下来我们对shared_ptr的reference count进行解析,一起揭开智能指针可以自动释放的神秘面纱。

C++智能指针_第3张图片
通过代码实例,我们可以看出,每拷贝和赋值一次shared_ptr,引用计数就加1,每删除一次,就减1。直到引用计数为0时,才释放。可以看出当计数为0时,智能指针自动释放了,不需要我们手动释放。其实,智能指针本质是存放在栈的模板对象,只是在栈内部包了一层指针。而栈在其生命周期结束时,其中的指针指向的堆内存也自然被释放了。因而实现了智能管理的效果,不需要考虑内存问题了,其实有点类似某种单例写法,程序运行结束,也不用考虑单例对象内存问题。
有个地方需要注意,当删除一个智能指针时,并不影响其它两个智能指针的继续使用。

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(1));

注意我们应该优先使用make_shared来构造智能指针,因为他更高效。

shared_ptr<int> sp1 = make_shared<int>(100);

并且,需要注意shared_ptr不能通过“直接将原始这种赋值”来初始化,需要通过构造函数和辅助方法来初始化。看下面代码的初始化是错误的

std::shared_ptr<int> p = new int(1);
reset()函数详解

reset( )不带参数时,若智能指针s是唯一指向该对象的指针,则释放,并置空。若智能指针P不是唯一指向该对象的指针,则引用计数减少1,同时将P置空。
reset( )带参数时,若智能指针s是唯一指向对象的指针,则释放并指向新的对象。若P不是唯一的指针,则只减少引用计数,并指向新的对象。

use_count() :返回shared_ptr的强引用计数;
unique() :若use_count()为1,返回true,否则返回false。

get()函数详解

返回shared_ptr中保存的裸指针;
**谨慎使用p.get()的返回值,**如看下面代码的错误

std::shared_ptr<int> ptr(new int(1));
int *p = ptr.get(); 
//不小心 delete p;

不要delete p.get()的返回值 ,会导致对一块内存delete两次的错误

使用shared_ptr应注意的问题

不要用一个原始指针初始化多个shared_ptr

看如下代码,

int *ptr = new int; 
shared_ptr<int> p1(ptr); 
shared_ptr<int> p2(ptr); // 逻辑错误
cout<<"p1 的计数"<<p1.use_count()<<endl;
cout<<"p2 的计数"<<p2.use_count()<<endl;

在这里插入图片描述
可以看出,p1,p2的计数都为1.这样就会出现什么问题呢
如下图,同时指向new int,当彼此都计数为0时,都会进行析构,释放两次,导致重复释放的问题。
C++智能指针_第4张图片

不要在函数实参中创建shared_ptr

在C++的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的,一般是从右到左,但也可能从左到右,所以,可能的过程是先new int,然后调用g(),如果恰好g()发生异常,而shared_ptr还没有创建, 则int内存泄漏了。如下代码,便是错误的

function(shared_ptr<int>(new int), g()); //有缺陷

正确的写法应是下面

shared_ptr<int> p(new int); 
function(p, g());
通过shared_from_this()返回this指针

不要将this指针作为shared_ptr返回出来,因为this指针本质上是一个裸指针,因此,这样可能会导致重复析构,看下面的例子。

class A 
{
	public:
		 shared_ptr<A> GetSelf() 
		 { return shared_ptr<A>(this); // 不要这么做 }
		 ~A() 
		 { cout << "Destructor A" << endl; } 
};
int main()
{ 
	shared_ptr<A> sp1(new A); 
	shared_ptr<A> sp2 = sp1->GetSelf(); 
	return 0; 
}

在这里插入图片描述
导致重复析构,此情况和第一个应注意的问题类似。

正确返回this的shared_ptr的做法是:让目标类通过std::enable_shared_from_this类,然后使用基类的成员函shared_from_this()来返回this的shared_ptr,如下所示

class A: public std::enable_shared_from_this<A> 
{
	public: 
		shared_ptr<A>GetSelf() 
		{ return shared_from_this(); }
		~A() 
		{ cout << "Destructor A" << endl;}
};
避免循环引用

“循环引用”简单来说就是:两个对象互相使用一个shared_ptr成员变量指向对方的会造成循环引用。导致引用计数失效。下面给段代码来说明循环引用:

#include
#include 
using namespace std;
class B;
class A
{
public:// 为了省去一些步骤这里 数据成员也声明为public
    //weak_ptr pb;
    shared_ptr<B> pb;
    void doSomthing()
    {
//        if(pb.lock())
//        {
//
//        }
    }

    ~A()
    {
        cout << "kill A\n";
    }
};

class B
{
public:
    //weak_ptr pa;
    shared_ptr<A> pa;
    ~B()
    {
        cout <<"kill B\n";
    }
};

int main(int argc, char** argv)
{
    shared_ptr<A> sa(new A());
    shared_ptr<B> sb(new B());

    if(sa && sb)
    {
        sa->pb=sb;
        sb->pa=sa;
    }
    cout<<"sa use count:"<<sa.use_count()<<endl;
    return 0;
}

上面的代码运行结果为:sa use count:2, 注意此时sa,sb都没有释放,产生了内存泄露问题!!!
即A内部有指向B,B内部有指向A, 这样对于A,B必定是在A析构后B才析构,对于B,A必定是在B析构后才析构A,这就是循环引用问题,违反常规,导致内存泄露。

一般来讲,解除这种循环引用有下面有三种可行的方法参考

1 当只剩下最后一个引用的时候需要手动打破循环引用释放对象。

2 当A的生存期超过B的生存期的时候,B改为使用一个普通指针指向A。

3 使用弱引用的智能指针打破这种循环引用。

虽然这三种方法都可行,但方法1和方法2都需要程序员手动控制,麻烦且容易出错。我们一般使用第三种方法:弱引用的智能指针weak_ptr。

三、独占的智能指针 unique_ptr

unique_ptr是一个独占型的智能指针,不能将其赋值给另一个unique_ptr

unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr。下面的错误示例。

unique_ptr<T> my_ptr(new T); 
unique_ptr<T> my_other_ptr = my_ptr; // 报错,不能复制

unique_ptr可以指向一个数组

std::unique_ptr<int []> ptr(new int[10]);
ptr[9] = 9; 
std::shared_ptr<int []> ptr2(new int[10]); // 这个是不合法的

unique_ptr需要确定删除器的类型

unique_ptr需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器,可以这样写:

std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;}); // 正确

如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。

四、弱引用的智能指针 weak_ptr

在上面介绍shared_ptr指针的注意事项时,我们提到避免循环引用,并且提到了解决循环引用的方法,其中有一个方法就是用到了weak_ptr。接下来我们认识下weak_ptr智能指针

weak_ptr指针介绍

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。

weak_ptr的基本用法

use_count

通过use_count()方法获取当前观察资源的引用计数,如下所示:

shared_ptr<int> sp(new int(10));
 weak_ptr<int> wp(sp); 
 cout << wp.use_count() << endl; //结果讲输出1
expired()

通过expired()方法判断所观察资源是否已经释放,如下所示:

shared_ptr<int> sp(new int(10));
 weak_ptr<int> wp(sp); 
 if(wp.expired()) 
 	cout << "weak_ptr无效,资源已释放"; 
 else
 	cout << "weak_ptr有效";
lock()

通过lock方法获取监视的shared_ptr

解决循环引用问题

通过weak_ptr解决该问题,只要将A或B的任意一个成员变量改为weak_ptr
原因就是:weak_ptr不会增加计数

推荐一个零声学院免费公开课程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:服务器课程

你可能感兴趣的:(c++,开发语言)