还在因为指针的释放问题而烦恼吗?快来看下C++11智能指针shared_ptr

智能指针是这样一种类,即其对象的特征类似于指针。例如,智能指针可以存储new分配的内存地址,也可以被解除引用。

因为智能指针是一个类对象,因此它可以修改和扩充简单指针的行为。例如,智能指针可以建立引用技术,这样多个对象可共享由智能指针跟踪同一个值,当使用该值的对象数为0是,智能指针将删除这个值。

智能指针可以提高内存的使用效率,帮助防止内存泄漏,但并不要求用户熟悉新的编程技术

00 | 前言

void test1(int& i_num)
{
    int* num = new string(i_num);
    // something to do
    i_num = num;
    return;
}


上面这个函数中存在这样一种缺陷,你发现了吗?

每当调用上面这个函数时,都会从堆中分配内存,但是却没有回收内存,这就导致了内存泄漏的问题,知道的同学都知道只要在最后加上delete num语句就没问题了,不知道的同学就不知道加什么了,所以对不熟悉的同学来说,这个“最后加上delete num”的动作就会经常忘记。那有没有那么一种方法,可以让我们不必关注这个delete num的动作,它在该释放的时候自动释放呢?

对于基本类型来说,暂时是没有这种功能的,但是对于C++类而言,它的析构函数就提供了这样的功能,所以通过类来实现智能指针就可以有效的避免“忘记加上delete语句”导致内存泄漏的情况

int* num1 = new int(2);
int* num2 = new int;
num2 = num1;
delete num2;
cout << *num1 << endl;
cout << *num2 << endl;
delete num1;

再来看上面的代码,能发现其中的缺陷吗?

在这里插入图片描述

两个指针指向同一块地址,那么这时候无论是delete哪个对象,都会释放掉这块内存资源,所以第五行开始输出的就已经不再是本来所指向的资源,而导致num1变成了野指针,最后上面的代码实际对同一个资源对象执行了两次释放动作(资源对象被释放后,如果再去释放该资源,就会会导致系统崩溃)。这就导致在必须使用多个指针指向同一个对象资源的情况下,很容易因为内存的错误释放或者操作已经释放的指针而导致系统崩溃,那怎么规避这个问题?

万不可能说拿笔记住每一个指针声明及调用的地方,人工保证每一个指针的正确声明,引用及释放问题。而对类而言,可以通过对这个智能指针类进行扩展功能来保证共享的资源只会被释放一次,从而有效的解决这个问题。而智能指针shared_ptr就应运而生了

01 | 概念

智能指针shared_ptr的本质是一个用于管理动态内存分配的模板类,它的设计意图是为了解决“当多个智能指针指向同一个对象资源时,错误的内存释放导致系统崩溃”问题,它的设计理念是“采用引用计数,使得在多个智能指针指向同一个对象资源时,给每一个指向这个对象资源的智能指针的引用计数进行算数处理,保证这个共享资源的正确释放”

简单来说,shared_ptr就是一个采用引用计数技术(允许多智能指针指向同一共享对象资源时,且保证该资源仅当引用计数为0时才被释放)的一个模板类实现智能指针。

shared_ptr定义在头文件memory中,其模板定义了类似指针的对象,可以将 new 获得(直接或间接)的地址赋给这种对象,同时在类内部定义一份引用计数数据,用来记录这个对象资源被几个对象共享。引用计数规则如下:
1. 当一个shared_pte对象过期时,引用计数减一,并且判断此时引用计数是否为0;
2. 引用计数为0,表示这是最后一个使用该资源的对象,调用其析构函数使用delete来释放内存;
3. 引用计数不为了0,则说明还有其他shared_ptr对象在使用该资源,不可释放资源,否则会导致其他shared_ptr对象称为野指针。

从下面的示例图直观的看一下shared_ptr在多指针指向同一个对象资源时的内存分配情况

shared_ptr<int> p1(new int(1));
shared_ptr<int> p2 = p1;

在这里插入图片描述

图中不难看出,在最后销毁资源的过程中,销毁p2时,引用计数减一后不为0,不对对象资源进行释放,当p1销毁时,引用计数为0,此时才释放对象资源,从而保证了共享资源只被释放一次,防止了内存泄漏问题

02 | 实现

前面提到shared_ptr的本质是一个模板类,既然是类,那么一定会有构造函数以及析构函数,再加上引用计数原则,把这个模板类的基础框架搭建起来,代码如下

template<class T>
class shared_ptr
{
    public:
        shared_ptr(T* i_sPtr):ptr(i_sPtr)
        {
            count = new int(1);
        }
        ~shared_ptr()
        {
            if (--(*count) == 0)
            {
                delete count;
                delete ptr;
                count = nullptr;
                ptr = nullptr;
            }
        }
    private:
        T* ptr;
        int* count;
};

上面的框架只实现了构造函数和析构函数,但是对于以下代码操作

shared_ptr<int> p1(new int(1));
shared_ptr<int> p2(p1);
shared_ptr<int> p3 = p1;
shared_ptr<int> p4(new int(2));
p3 = p4;

还需要增加拷贝构造函数以及赋值重载函数的实现,两个函数实现代码如下:

shared_ptr(shared_ptr<T>& i_sPtr):sPtr(i_sPtr.ptr), count(i_sPtr.count)
{
    (*count)++;
}

shared_ptr<T>& operator=(const shared_ptr<T>& i_sPtr)
{
    if (i_sPtr.ptr != ptr)
    {
        delete ptr;
        delete count;
        ptr = i_sPtr.ptr;
        count = i_sPtr.count;
        (*count)++;
    }
    return *this;
}

既然是智能指针,那么就一定可以使用运算符"*"和“->”,所以还需要重载这两个操作符,实现代码如下:

T& operator*()
{
    return *ptr;
}
T& operator->()
{
    return ptr;
}

最后还要加上shared_ptr的常用函数之一use_count() —— 返回当前的引用计数值,至此,智能指针shared_ptr的基本功能实现完成了,整合起来就是

template<class T>
class shared_ptr
{
    public:
        shared_ptr(T* i_sPtr):ptr(i_sPtr)
        {
            count = new int(1);
        }
        ~shared_ptr()
        {
            if (--(*count) == 0)
            {
                delete count;
                delete ptr;
                count = nullptr;
                ptr = nullptr;
            }
        }
        shared_ptr(shared_ptr<T>& i_sPtr):sPtr(i_sPtr.ptr), count(i_sPtr.count)
        {
            (*count)++;
        }
        shared_ptr<T>& operator=(const shared_ptr<T>& i_sPtr)
        {
            if (i_sPtr.ptr != ptr)
            {
                delete ptr;
                delete count;
                ptr = i_sPtr.ptr;
                count = i_sPtr.count;
                (*count)++;
            }
            return *this;
        }
        T& operator*()
        {
            return *ptr;
        }
        T& operator->()
        {
            return ptr;
        }
        int& use_count()
        {
            return *count;
        }

    private:
        T* ptr;
        int* count;
};

综上所述,C++的模板类使得可以通过构造函数将shared_ptr的类对象初始化为一个拟常规指针(类似于常规指针,但是特性比常规指针要多)
那么以上实现的智能指针 s h a r e d − p t r 就完美了吗? \color{pink}{那么以上实现的智能指针shared-ptr就完美了吗?} 那么以上实现的智能指针sharedptr就完美了吗?

03 | 问题

shared_ptr并非万灵丹

  • 对象类型格式问题 \color{pink}{对象类型格式问题} 对象类型格式问题

      shared_ptr<int> p1(new int[3]);
    

    仔细回想下C++中关于new和delete操作符的描述——对于指针和动态数组,C++中new和delete需要使用相应的方式,指针:new && delete动态数组数组:new[] && delete[]

    而在自己实现的的shared_ptr类代码中,申请内存空间和释放内存资源时使用的是 new 和 delete,而上面这行代码因为对象类型格式不匹配导致的后果在目前来说是不确定的,这就是当前实现的shared_ptr问题之一。

    想要上面这行代码执行正常,有两种方法:

    1. 可以重新定义一个shared_ptr1,将对应的操作符格式修改即可

    2. 自定义删除器,在构造时传递一个函数指针,用于释放对象的内存;

    下面是一个实现shared_ptr自定义删除器的示例代码:

    #include <iostream>
    #include <memory>
    #include <cstring>
    
    using namespace std;
    void custom_deleter(char* p)
    {
    	cout << "custom deleter called" << endl;
    	delete[] p;
    }
    
    int main()
    {
    	shared_ptr<char> sp(new char[10], &custom_deleter);
    	strcpy(sp.get(), "example");
    	cout << sp.get() << endl;
    	return 0;
    }
    

    在这个例子中,我们定义了一个名为custom_deleter的自定义删除器函数,在函数中输出“custom deleter called”作为演示目的。然后,我们创建一个带有指向10字节字符缓冲区的shared_ptr,并将自定义删除器函数的地址传递给它。在程序的其余部分,我们使用strcpy函数向缓冲区复制字符串“example”,并打印它们。

    当程序退出作用域时,shared_ptr对象将在结束生命周期时调用自定义删除器。在这个例子中,我们的自定义删除器函数将打印一条消息,并释放存储在动态内存中的字符缓冲区。

  • 线程安全问题 \color{pink}{线程安全问题} 线程安全问题

    shared_ptr的引用计数本身是安全且无锁的,但对象的读写不是,它有两个数据成员,读写操作不能原子化。

    • 一个 shared_ptr 对象实体可被多个线程同时读取

    • 两个 shared_ptr 对象实体可以被两个线程同时写入,“析构”算写操作。如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁

    针对线程安全问题,对上述实现代码进行修改后,如下

      template<class T>
      class shared_ptr
      {
      	public:
          	shared_ptr(T* i_sPtr):ptr(i_sPtr)
          	{
              	count = new int(1);
              	mtx = new mutex;
          	}
          	void deletePtr()
          	{
              	bool flg = false;
              	mtx->lock();
              	if (--(*count) == 0)
              	{
                  	flg = true;
                  	delete count;
                  	delete ptr;
                  	count = nullptr;
                  	ptr = nullptr;
              	}
              	mtx->unlock();
              	if (flg)
              	{
                  	delete mtx;
                  	mtx = nullptr;
              	}
          	}
          	~shared_ptr()
          	{
              	deletePtr();
          	}
          	shared_ptr(shared_ptr<T>& i_sPtr):sPtr(i_sPtr.ptr), count(i_sPtr.count), mtx(i_sPtr.mtx)
          	{
              	mtx->lock();
              	(*count)++;
              	mtx->unlock();
          	}
          	shared_ptr<T>& operator=(const shared_ptr<T>& i_sPtr)
          	{
              	if (i_sPtr.ptr != ptr)
              	{
                  	deletePtr();
                  	ptr = i_sPtr.ptr;
                  	count = i_sPtr.ptr;
                  	mtx = i_sPtr.mtx;
                  	mtx->lock();
                  	(*count)++;
                  	mtx->unlock();
              	}
              	return *this;
          	}
          	T& operator*()
          	{
              	return *ptr;
          	}
          	T& operator->()
          	{
              	return ptr;
          	}
          	int& use_count()
          	{
              	return *count;
          	}
    
      	private:
          	T* ptr;
          	int* count;
          	mutex* mtx;
      };
    
  • 循环引用问题 \color{pink}{循环引用问题} 循环引用问题

    在使用shared_ptr时,当两个或多个对象互相持有对方的引用,导致它们的引用计数永远不会降为零,从而导致内存泄漏的情况。

    看下面这一个例子

      #include <iostream>
      #include <memory>
    
      using namespace std;
      class A;
      class B;
    
      class A
      {
      	public:
          	A(){cout << "A done" << endl;}
          	~A(){cout << "A kill" << endl;}
          	void test(shared_ptr<B> i_ptrb){ptr = i_ptrb;}
      	private:
          	shared_ptr<B> ptr;
      };
    
      class B
      {
      	public:
          	B(){cout << "B done" << endl;}
          	~B(){cout << "B kill" << endl;}
          	void test(shared_ptr<A> i_ptra){ptr = i_ptra;}
      	private:
          	shared_ptr<A> ptr;
      };
    
      int main()
      {
      	shared_ptr<A> a(new A);
      	shared_ptr<B> b(new B);
      	cout << "countA = " << a.use_count() << ", countB = " << b.use_count() << endl;
      	a->test(b);
      	b->test(a);
      	cout << "countA = " << a.use_count() << ", countB = " << b.use_count() << endl;
      	return 0;
      }
    

    在这里插入图片描述

    从输出的结果上可以看出异常 —— 两个类对象的构造函数都正常执行了,但是在程序退出的时候,并没有执行对应的析构函数,为什么?可以看到输出结果中程序推出前,两个对象的引用计数不是0,而是2,那么程序退出时引用计数一定不会等于0,就一定不会释放资源,从而导致内存泄漏问题,通过图形化演示一遍上述代码的关键执行过程,如下

    在这里插入图片描述

    1. 对象初始化时,两个对象实例的引用计数都至为1;

    2. 调用test()函数时,都分别引用了对方,所以引用计数都加一为2;

    3. 程序结束后,分别调用了边自身的析构函数,引用计数减一为1;

    此时引用计数为1,不为0,析构函数并不会去释放内存资源。

    那么怎么解决循环引用的问题呢?

    C++库中存在weak_ptr类型智能指针,weak_ptr对象可以指向shared_ptr对象,且不会增加shared_ptr对象中的引用计数

    通过使用标准库中的weak_ptr智能指针可以很好的解决循环引用问题,方法如下

    将其中一个类的对象成员修改为weak_ptr类型智能指针,那么为什么不是两个都用呢?

    通过代码调试来看下为什么只修改其中一个为weak_ptr类型智能指针就能解决循环引用问题

    class A
    {
    	public:
        	A(){cout << "A done" << endl;}
        	~A(){cout << "A kill" << endl;}
        	void test(shared_ptr<B> i_ptrb){ptr = i_ptrb;}
    	private:
        	shared_ptr<B> ptr; //weak_ptr<B> ptr
    };
    
    class B
    {
    	public:
        	B(){cout << "B done" << endl;}
        	~B(){cout << "B kill" << endl;}
        	void test(shared_ptr<A> i_ptra){ptr = i_ptra;}
    	private:
        	weak_ptr<A> ptr;
    };
    

    在这里插入A描述

    仅修改类B结果图

    在这里插入图片描述

    修改类B和类A结果图

    从上面两个执行结果可以看出,无论修改一个类还是修改两个类,都能正常释放内存资源了,所以只需要修改一个类即可,那么为什么修改一个类对象成员就可以了呢?

    结果上可以看到执行完a→test(b)的时候,a的引用计数并没有改变,所以在程序执行结束的时候,a正常执行了析构函数,并释放了对应的内存资源,也就不会再引用b对象的资源,所以a正常销毁之后,b的计数也正常减一变为1,此时再执行自身的析构函数就可以正常释放内存资源了

04 | 总结

当需要多个指针共享同一个对象并且确保对象的内存被正确释放时,shared_ptr是一种方便可靠的内存管理工具。可以通过shared_ptr创建一个指向动态内存对象的智能指针,并且它支持自定义删除器,自动引用计数以及弱指针。

shared_ptr具有以下特点:

  • 可以安全地指向动态分配的对象,避免内存泄漏和悬挂指针的问题。

  • 引用计数机制确保了多个指向同一个对象的shared_ptr能够准确地共享所指向对象的访问权。

  • 智能指针本身在销毁时自动释放内存,不需要程序员显式调用delete函数。

  • 支持自定义删除器,可以在需要特殊操作(如文件关闭、数据库连接断开等)时回收动态分配的对象。

总之,shared_ptr在C++中是一种强大且灵活的工具,可以使代码更加简洁、安全和易于维护。但需要注意的是过度使用shared_ptr可能会导致引用计数的开销变得非常大,从而影响程序的运行效率。