深入理解shared_ptr与weak_ptr之手写

lxf2023-05-23 00:59:21

前言

智能指针是一种可以自动管理内存的指针,它可以在不需要手动释放内存的情况下,确保对象被正确地销毁,防止内存泄漏和悬空指针的出现。上一篇文章主要分析了C++11中的三个智能指针:shared_ptr、weak_ptr和unique_ptr的用法和一些陷阱,其中在平时项目中用的最多的就是shared_ptr,此文就深入剖析一下shared_ptr的背后实现原理。并尝试手写出简易的shared_ptr和weak_ptr。

一、 shared_ptr的思想

shared_ptr的关键特性就是共享所有权,多个指针可以指向相同的对象。当最后一个指向该对象的指针被销毁或者释放对对象的所有权的时候,对象就会自动被释放。

这种特性是通过引用计数来实现的。 shared_ptr是一个模板类,类里面有一个成员专门用来记录有多少个shared_ptr共享一个对象。

shared_ptr简单用法:

深入理解shared_ptr与weak_ptr之手写

shared_ptr和make_shared都会包含在memory头文件中。

二、shared_ptr的拷贝和赋值

当进行拷贝和赋值操作的时候,每个shared_ptr都会记录有多少个其他的shared_ptr指向相同的对象:

auto p = make_shared<int>(42);
auto q(p);

我们可以认为每个shared_ptr都有一个引用计数,就是一个关联的计数器。

无论何时当我们拷贝一个shared_ptr,计数器都会递增。 比如,用一个shared_ptr初始化另一个shared_ptr,或者将其作为函数参数传递给一个函数,或者作为函数的返回值,他所关联的引用计数就会递增。

当我们把shared_ptr赋予一个新的shared_ptr或者shared_ptr被销毁,或者shared_ptr离开其作用域的时候,引用计数就会递减。

当一个shared_ptr的引用计数减为0的时候,他就会释放自己所管理的对象。

auto p1 = make_shared<int>(4);
auto p2 = make_shared<int>(3);
p1 = p2;

注意,在上面的例子中,我们创建了两个智能指针,然后给p1赋值,令p1指向另一个地址,也就是让他去管理另一个对象。 这里我们就要递增p2指向的对象的引用计数,递减p1指向的对象的引用计数。如果p1原来的对象已经没有引用者了,那就释放。

三、使用shared_ptr的陷阱

下面看一个例子:


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

class Company;//前置声明,公司类
//创建worker类
class Worker {
public:
	Worker() {
		cout << "执行worker类的构造方法" << endl;
	}
	~BOY() {
		cout << "执行worker类的析构方法" << endl;
	}

	void GetCompany(shared_ptr<Company>& company_ptr) {   //BOY获取GRIL方法
		company = company_ptr;
	}

private:
	shared_ptr<Company> company;     //worker类里的shared_ptr指针
};

//创建Company类
class Company {
public:
	Company() {
		cout << "执行company类的构造方法" << endl;
	}

	~Company() {
		cout << "执行company类的析构方法" << endl;
	}

	void GetWorker(shared_ptr<Worker>& worker_ptr) {     //GRIL类获取BOY方法
		worker = worker_ptr;
	}
private:
	shared_ptr<Worker> worker;      //Company类里的shared_ptr指针
};

int main(void) {

    {
        shared_ptr<Company> ptr1(new Company);      //分配一个boy类对象
        shared_ptr<Worker> ptr2(new Worker);   //分配一个gril类对象
        ptr1->GetWorker(ptr2);
        ptr2->GetCompany(ptr1);
    }	


	system("pause");

	return 0;
}

在这个例子中,有一个公司类,有一个员工类,然后他们的类里面的智能指针互相引用。

在动态创建完公司类和员工类的对象之后,它们各自的智能指针的引用计数为一,又因公司类与员工类类里各有一个指向对方的shared_ptr指针,执行完它们各自的GetCompany和GetWorker函数之后,他们的智能指针就会互相引用

当生命周期到了,都等着对方释放,但是又都不能释放,此时引用计数为一不为零,所以任何一个动态内存都无法释放,其中一个无法释放,就导致另一个无法释放,就造成了类似于死锁的情况。两个都不能释放。就造成了内存泄漏。

shared_ptr的double free问题

深入理解shared_ptr与weak_ptr之手写

至于weak_ptr的用法和原理,在上一篇博客里已经讲过了,这里就不再赘述了。

shared_ptr的线程安全问题

  • 同一个shared_ptr被多个线程“读”是安全的。

  • 同一个shared_ptr被多个线程“写”是不安全的。

  • 共享引用计数的不同的shared_ptr被多个线程”写“ 是安全的。

对于智能指针shared_ptr的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,一个是指向的对象的指针,还有一个就是我们上面看到的引用计数管理对象,当智能指针发生拷贝的时候,标准库的实现是先拷贝智能指针,再拷贝引用计数对象(拷贝引用计数对象的时候,会使use_count加一),这两个操作并不是原子操作,隐患就出现在这里。两个线程中智能指针的引用计数同时++或--,这个操作不是原子的,假设引用计数原来是1,++了两次,可能还是2,这样引用计数就错乱了,违背了原子性。

总之,智能指针在多线程环境中不是安全的,需要加锁来保持线程安全。

四、手写shared_ptr

实现一个简单的 shared_ptr类,主要两个成员变量:

  • 指向对象的指针:用于保存要管理的对象的地址。

  • 引用计数:用于记录当前有多少个shared_ptr共享同一个对象,引用计数需要使用指针来在多个shared_ptr 对象之间共享计数,实际上比这个复杂,一般是一个类对象,内部包含多个引用计数相关的信息:

深入理解shared_ptr与weak_ptr之手写

几个关键点:

  • 构造函数要设计成explicit的,防止隐士的类类型转换,因为普通指针是不能直接赋值给智能指针的。必须使用直接初始化(用括号)的方式才行。
  • 析构函数:在析构函数中处理引用计数的递减和内存的释放。
  • 重载解引用操作符
  • 为了实现共享所有权,需要编写拷贝构造函数拷贝赋值运算符:
    • 在拷贝构造函数中,将指向对象的指针和引用计数成员变量复制到新对象,并递增引用计数。
    • 在拷贝赋值运算符中,处理自我赋值情况更新引用计数如何更新参考上面:二、shared_ptr的拷贝和赋值)。
#include <iostream>

template <typename T>
class _shared_ptr{
public:
    //use_count初始化的意思是,如果初始化shared_ptr的是一个空指针,那么开始引用计数就是0。如果是一个有效指针,那么开始引用计数就是1
    explicit _shared_ptr(T* ptr = nullptr):use_count(ptr == nullptr?nullptr:new int (1)),_ptr(ptr){}

    _shared_ptr(const _shared_ptr& other_ptr):use_count(other_ptr.use_count),_ptr(other_ptr._ptr)
    {
        if(use_count != nullptr)//防止用空指针拷贝构造
        {
            (*use_count) ++;
        }
    }
    _shared_ptr& operator=(const _shared_ptr& other_ptr)
    {
        if(&other_ptr != this)//防止自身赋值,自身赋值的话就不用管,引用计数就不用加一了
        {
            release();//先将自身计数减一
            _ptr = other_ptr._ptr;
            use_count = other_ptr.use_count;
            //(*use_count) ++;//这里要注意不能直接++,防止other_ptr是空指针
            if(use_count)
                ++ (*use_count);
        }
        return *this;
    }
    ~ _shared_ptr()
    {
        release();
    }

    T& operator*()const//重载解引用运算符
    {
        return *_ptr;//返回被管理对象的引用
    }

    T* operator->()const//重载->运算符
    {
        return _ptr;
    }

    T* get()const//和->一样,返回的是指针
    {
        return _ptr;
    }

    int get_use_count()const
    {
        return use_count != nullptr?(*use_count):0;//如果use_count是空指针的话,就说明没有引用计数,说明shared_ptr是空的,就返回0
    }
private:
    void release()
    {
        if(use_count && -- (*use_count) == 0)
        {
            delete use_count;
            delete _ptr;
        }

    }
    int* use_count;//引用计数
    T* _ptr;//指向所管理的对象
};

class Myclass 
{
private:
    
public:
    Myclass(){
        std::cout<<"这里是myclass的构造函数"<<std::endl;
    }
     
    ~ Myclass(){
        std::cout<<"这里是myclass的析构函数"<<std::endl;
    }
    void DoSomething(){
        std::cout<<"do some things"<<std::endl;
    }
};

int main()
{
    _shared_ptr<Myclass>p1(new Myclass());
    {
        _shared_ptr<Myclass>p2(new Myclass());\
        std::cout<<"p2的use_count: "<<p2.get_use_count()<<std::endl;

    }
    std::cout<<".................................."<<std::endl;
    _shared_ptr<Myclass>p3(new Myclass());
    std::cout<<"p1的use_count: "<<p1.get_use_count()<<std::endl;
    std::cout<<"p3的use_count: "<<p3.get_use_count()<<std::endl;

    p1 = p3;
    std::cout<<".................................."<<std::endl;
    std::cout<<"p1的use_count: "<<p1.get_use_count()<<std::endl;
    std::cout<<"p3的use_count: "<<p3.get_use_count()<<std::endl;
    return 0;
}

结果如下:

深入理解shared_ptr与weak_ptr之手写

五、手写weak_ptr

关于weak_ptr的原理在上一篇博客里已经讲了,它是一种弱引用,将shared_ptr赋值给他的时候,引用计数不会增加。

weak_ptr的代码要结合上面我自己写的shared_ptr的代码一起看,因为weak_ptr的类中用到了我自己写的shared_ptr。

话不多说,上代码:


template<typename T>
class _weak_ptr{
friend class _shared_ptr<T>;
public:
    _weak_ptr():use_count(new int(0)),ptr(nullptr){}

    _weak_ptr(const _weak_ptr& wp):use_count(wp?wp.use_count:new int(0)),ptr(wp.ptr){}

    _weak_ptr(const _shared_ptr<T>& sp):use_count(sp.use_count),ptr(sp._ptr){}

    _weak_ptr& operator=(const _weak_ptr& wp)
    {
        if(&wp != this)
        {
            ptr = wp.ptr;
            use_count = wp.use_count;
        }
        return *this;
    }

    _weak_ptr& operator=(const _shared_ptr<T>& sp)
    {
        ptr = sp._ptr;
        use_count = sp.use_count;
        return *this;
    }
    T* operator->()
    {
        return ptr;
    }
    T& operator*()
    {
        return *ptr;
    }
    int get_use_count()
    {
        //return *use_count;
        return use_count == nullptr?0:(*use_count);
    }

    bool expire()//根据use——count是否为零来判断
    {
        return use_count == nullptr || *(use_count) == 0;
    }

    _shared_ptr<T>& lock()
    {
        if(expire())//如果expire为true,就返回一个空的shared——ptr指针
            return _shared_ptr<T>();
        return _shared_ptr<T>(*this);
    }

    void reset()//将weak_ptr置为空
    {
        ptr = nullptr;
        use_count = nullptr;
    }
private:
    int* use_count;
    T* ptr;
};

int main()
{
    _shared_ptr<Myclass>p1(new Myclass());
    {
        _shared_ptr<Myclass>p2(new Myclass());\
        std::cout<<"p2的use_count: "<<p2.get_use_count()<<std::endl;

    }
    std::cout<<".................................."<<std::endl;
    _shared_ptr<Myclass>p3(new Myclass());
    std::cout<<"p1的use_count: "<<p1.get_use_count()<<std::endl;
    _weak_ptr<Myclass>wp2;
    wp2 = p1;
    std::cout<<"wp2的use_count是: "<<wp2.get_use_count()<<std::endl;
    std::cout<<"p3的use_count: "<<p3.get_use_count()<<std::endl;

    p1 = p3;
    std::cout<<".................................."<<std::endl;
    std::cout<<"p1的use_count: "<<p1.get_use_count()<<std::endl;
    std::cout<<"p3的use_count: "<<p3.get_use_count()<<std::endl;

    _weak_ptr<Myclass>wp1;
    wp1 = p1;
    std::cout<<"wp1的use_count是: "<<wp1.get_use_count()<<std::endl;
    return 0;
}

结果:

深入理解shared_ptr与weak_ptr之手写

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!