• C++智能指针(1)


    C++11前的智能指针

    int div()
    {
        double a, b;
        cin >> a >> b;
        if(b == 0)
            throw invalid_argument("除0错误");
    
    		return a / b;
    }
    
    void f1()
    {
    	int* p = new int;
    	cout << div() << endl;
    	delete p;
    	cout << p << endl;
    }
    
    int main()
    {
    	try
    	{
    		f1();
    	}
    	catch(exception& e)
    	{
    		cout << e.what() << endl;
    	}
    
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    如果发生了除0错误,在f1中没有到delete p就抛异常了,这时候只能在f1中也抛异常,这样才能让p释放。

    void f1()
    {
    	int* p = new int;
    	try
    	{
    		cout << div() << endl;
    	}
    	catch(...)
    	{
    		delete p;
    		cout << p << endl;
    		throw;
    	}
    	delete p;
    	cout << p << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    但这样处理不太好看,C++中可以用智能指针来处理。

    RAII: 是一种利用对象生命周期来控制程序资源。构造函数获取资源,析构函数释放资源。
    一个基本的智能指针包含两点:1、RAII。2、重载operator* 和operator-> 用起来像指针一样。

    实现一个基本的智能指针:

    template<class T>
    class SmartPtr
    {
    public:
      // 1、RAII
    	// 2、重载operator* 和 operator->  用起来像指针一样
    	SmartPtr(T* ptr)
    		:_ptr(ptr)
    	{}
    
    	~SmartPtr()
    	{
    		delete _ptr;
    		cout << _ptr << endl;
    	}
    
    	T& operator*()
    	{
    		return *_ptr;
    	}
    
    	T* operator->()
    	{
    		return _ptr;
    	}
    
    private:
    	T* _ptr;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在这里插入图片描述注意:这里的(*spp).first就是spp->first,而它原本是spp->->first,中间还有一个pair,但这样可读性太差,编译器优化成只有一个箭头就可以。

    在这里插入图片描述
    这里拷贝了一个智能指针,或传参时拷贝了。多个智能指针对象管理一个资源,析构时会析构多次,而这个智能指针没有写拷贝构造函数,默认构造函数会完成内置类型(T* 指针)的浅拷贝,程序崩溃。

    C++98 : 拷贝时,管理权转移(auto_ptr)
    在这里插入图片描述永远只有一个对象管理资源。

    赋值运算符重载也是如此,将管理权转移,把自己置空。
    在这里插入图片描述但是auto_ptr的管理权转移会导致被拷贝的对象悬空,如果不小心访问了sp1就访问了空指针,是一种不好的设计。

    boost库(第三方库):scoped_ptr,shared_ptr,weak_ptr
    C++11吸收了boost库的精华:unique_ptr,shared_ptr,weak_ptr

    unique_ptr

    设计思路:有些场景下面,智能指针仅仅用于管理资源,不需要拷贝。

    防拷贝、赋值:

    unique_ptr(const unique_ptr<T>& up) = delete;
    unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
    
    • 1
    • 2

    如果使用它拷贝就直接报错。

    shared_ptr

    在这里插入图片描述
    如果count是在栈上,那么每个对象都有一个count,少一个智能指针对象就要把每个对象的count都减1才能一致,增加对象时也一样,不是只有一个count。我们要的是每个对象都使用同一个计数。

    如果使用静态变量,当有多个资源对象时,每个资源对象应该有独立的计数,但静态count是所有智能指针对象共享了,也不行。
    在这里插入图片描述所以我们给一个在堆上的计数变量int* count;
    如果使用了拷贝构造那么智能指针对象使用的也是同一个count资源
    在这里插入图片描述在这里插入图片描述
    析构:
    在这里插入图片描述设计思路:多个智能指针对象管理一块资源,这块资源对应一个引用计数,析构时减减计数,计数等于0时表示是最后一个管理对象,就释放资源。

    赋值重载:
    要注意判断是不是自己给自己赋值,如果是以下的情况,没有判断是不是自己给自己赋值,计数减为0将释放sp6,count将会被置成随机值。
    在这里插入图片描述

    //sp1 = sp4
    shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
    	//if(*this != sp): 对象没有默认重载!=符号
    	//if(this != &sp):如果其中的指针_ptr指向同一块资源,但不是同一个对象,那--又++就白操作了,也不好
    	if(_ptr != sp._ptr)
    	{
    		if(--(*pCount) == 0)
    		{
    			delete _pCount;
    			delete _ptr;
    		}
    
    		_ptr = sp._ptr;
    		_pCount = sp._pCount;
    		++(*pCount);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    现代写法:sp1 = sp4,先用sp4传值拷贝构造一个sp,计数++,sp的计数换给了sp1,sp1原来的计数换给了sp,sp是个局部对象,函数结束调用析构函数释放,减少了一个计数。
    在这里插入图片描述

    线程安全问题

    template<class T>
    class shared_ptr
    {
    public:
    	shared_ptr(T* ptr)
    		:_ptr(ptr)
    		, _pCount(new int(1))
    	{}
    
    	// sp2(sp1)
    	shared_ptr(shared_ptr<T>& sp)
    		:_ptr(sp._ptr)
    		, _pCount(sp._pCount)
    	{
    		++(*_pCount);
    	}
    	
    	// sp1 = sp4 现代写法
    	shared_ptr<T>& operator=(shared_ptr<T> sp)
    	{
    		swap(_ptr, sp._ptr);
    		swap(_pCount, sp._pCount);
    
    		return *this;
    	}
    
    	~shared_ptr()
    	{
    		if (--(*_pCount) == 0 && _ptr)
    		{
    			delete _ptr;
    			delete _pCount;
    			cout << _ptr << endl;
    		}
    	}
    
    	T& operator*()
    	{
    		return *_ptr;
    	}
    
    	T* operator->()
    	{
    		return _ptr;
    	}
    	
    	T* get()
    	{
    		return _ptr;
    	}
    
    	int use_count()
    	{
    		return *_pCount;
    	}
    
    private:
    	T* _ptr;
    	int* _pCount;
    }; 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    引用计数是智能指针的内部实现,但是资源的线程安全不是智能指针能管的。智能指针对象拷贝析构过程中引用计数的线程安全需要保障。以上代码就会引发线程安全问题。

    struct Date
    {
    	int _year = 1;
    	int _month = 1;
    	int _day = 1;
    };
    
    void SharePtrFunc(my::shared_ptr<Date>& sp, size_t n)
    {
    	for(size_t i = 0; i < n; i++)
    	{
    		//这里智能指针拷贝会++计数,析构会--计数,这里是线程安全的。
    		my::shared_ptr<Date> copy(sp);
    
    		//这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但是最终看到的结果,不一定是加了2n
    		copy->_year++;
    		copy->_month++;
    		copy->_day++;
    	}
    }
    
    void test_shared_ptr_thread_salf()
    {
    	my::shared_ptr<Date> p(new Date);
    	cout << p.get() << endl;
    	cout << p.use_count() << endl;
    
    	const size_t n = 10000;
    	thread t1(SharePtrFunc, p, n);
    	thread t2(SharePtrFunc, p, n);
    
    	t1.join();
    	t2.join();
    
    	cout << p.get() << endl;
    	cout << p.use_count() << endl;
    
    	cout << p->_year << endl;
    	cout << p->_month << endl;
    	cout << p->_day << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    我们可以专门抽象出来控制pCount

    void add_ref()
    {
    	_pMtx->lock();
    	++(*_pCount);
    	_pMtx->unlock();
    }
    
    void release_ref()
    {
    	bool flag = false;
    	_pMtx->lock();
    	if (--(*_pCount) == 0 && _ptr)
    	{
    		D del;
    		del(_ptr); // 使用删除器释放即可
    
    		//delete _ptr;
    		delete _pCount;
    		flag = true;
    		cout << "释放资源:" << _ptr << endl;
    	}
    	_pMtx->unlock();
    
    	if (flag == true)
    	{
    		delete _pMtx;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    完整代码

    template<class T, class D = DefaultDel<T>>
    class shared_ptr
    {
    	template<class T>
    	friend class weak_ptr;
    public:
    	explicit shared_ptr(T* ptr = nullptr)
    		:_ptr(ptr)
    		, _pCount(new int(1))
    		, _pMtx(new mutex)
    	{}
    
    	void add_ref()
    	{
    		_pMtx->lock();
    		++(*_pCount);
    		_pMtx->unlock();
    	}
    
    	void release_ref()
    	{
    		bool flag = false;
    		_pMtx->lock();
    		if (--(*_pCount) == 0 && _ptr)
    		{
    			D del;
    			del(_ptr); // 使用删除器释放即可
    
    			//delete _ptr;
    			delete _pCount;
    			flag = true;
    			cout << "释放资源:" << _ptr << endl;
    		}
    		_pMtx->unlock();
    
    		if (flag == true)
    		{
    			delete _pMtx;
    		}
    	}
    
    	// sp2(sp1)
    	shared_ptr(shared_ptr<T, D>& sp)
    		:_ptr(sp._ptr)
    		, _pCount(sp._pCount)
    		, _pMtx(sp._pMtx)
    	{
    		add_ref();
    	}
    
    	// sp1 = sp4
    	shared_ptr<T, D>& operator=(shared_ptr<T, D> sp)
    	{
    		swap(_ptr, sp._ptr);
    		swap(_pCount, sp._pCount);
    
    		return *this;
    	}
    
    	~shared_ptr()
    	{
    		release_ref();
    	}
    
    	T& operator*()
    	{
    		return *_ptr;
    	}
    
    	T* operator->()
    	{
    		return _ptr;
    	}
    
    	T* get()
    	{
    		return _ptr;
    	}
    
    	int use_count()
    	{
    		return *_pCount;
    	}
    
    private:
    	T* _ptr;
    	int* _pCount;
    	mutex* _pMtx;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89

    循环引用

    在这里插入图片描述
    n2要销毁,它的引用计数要到0,而它被n1的_next管理,n1的_next要销毁就要让n1销毁,而n1被n2的_prev管理,n1销毁就要让n2的_prev销毁,而n2的_prev要销毁就要让n2销毁,这样两个节点都无法销毁,形成循环引用。

    weak_ptr

    它可以用一个shared_ptr去构造它。

    在会产生循环引用的位置,把shared_ptr换成weak_ptr。

    weak_ptr不是一个RAII智能指针,它不参与资源的管理,它是专门用来解决循环引用的问题的。可以把一个shared_ptr用来初始化一个weak_ptr,但是weak_ptr不增加引用计数,不参与管理,但是也像指针一样访问修改资源。

    struct ListNode
    {
    	 int _data;
    	 weak_ptr<ListNode> _prev;
    	 weak_ptr<ListNode> _next;
    	 ~ListNode(){ cout << "~ListNode()" << endl; }
    };
    
    int main()
    {
     shared_ptr<ListNode> node1(new ListNode);
     shared_ptr<ListNode> node2(new ListNode);
     
     cout << node1.use_count() << endl;
     cout << node2.use_count() << endl;
     
     node1->_next = node2; //把node2(shared_ptr)赋值给node1->_next(weak_ptr),不会增加node2(shared_ptr)的引用计数
     node2->_prev = node1;
     
     cout << node1.use_count() << endl;
     cout << node2.use_count() << endl;
     return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了。
    原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。

    简单模拟实现:

    template<class T>
    class weak_ptr
    {
    public:
    	weak_ptr()
    		:_ptr(nullptr)
    	{}
    
    	weak_ptr(shared_ptr<T>& sp)
    		:_ptr(sp._ptr)
    		, _pCount(sp._pCount)
    	{}
    
    	weak_ptr<T>& operator=(shared_ptr<T>& sp)
    	{
    		_ptr = sp._ptr;
    		_pCount = sp._pCount;
    
    		return *this;
    	}
    private:
    	T* _ptr;
    	int* _pCount;
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    定制删除器

    前面说的指针都是new一个出来的,但如果指针是其他方式生成的呢?

    std::shared_ptr<pair<int, int>> sp1(new pair<int, int>[10]);
    std::shared_ptr<string> sp2(new string[10]);
    std::shared_ptr<string> sp3((string*)malloc(sizeof(string)));
    
    • 1
    • 2
    • 3

    此时就要用到删除器进行删除,如

    template<class T>
    struct DeleteArray
    {
    	void operator()(T* ptr)
    	{
    		delete[] ptr;
    	}
    };
    
    void test_deletor()
    {
    	std::shared_ptr<string> sp(new string[10], DeleteArray<string>());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    定制删除器最重要的是在析构函数时调用

    我们模拟实现删除器的传递位置跟std的不太一样
    std的框架设计底层用一个类专门管理资源技术和释放,所以它可以在构造函数传参,把删除器类型传递给专门管理资源引用计数的这个类。
    我们是一体化的,只能shared_ptr实例化给删除器,析构函数才能拿到删除器。

    template<class T>
    struct DefaultDel
    {
    	void operator()(T* ptr)
    	{
    		delete ptr;
    	}
    };
    
    template<class T, class D = DefaultDel<T>>
    class shared_ptr{...}void test_deletor()
    {
    	bit::shared_ptr<string> sp1(new string);
    
    	bit::shared_ptr<string, DeleteArray<string>> sp2(new string[10]);
    
    	auto ffree = [](string* ptr){free(ptr); };
    	bit::shared_ptr<string, decltype(ffree)> sp4((string*)malloc(sizeof(string)));
    
    	auto ffclose = [](FILE* ptr){fclose(ptr); };
    	bit::shared_ptr<FILE, decltype(ffclose)> sp5(fopen("test.cpp", "r"));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    总结:

    1. 不要使用auto_ptr,他是早期的设计缺陷。
    2. 不拷贝的场景建议使用unique_ptr。
    3. 如果要拷贝或者定制删除器,建议使用shared_ptr。不过要注意循环引用的问题。

    智能指针的一些常见问题:

    1. 什么是RAII
    2. 了解智能指针的发展历史
    3. 模拟实现一个简洁的智能指针 – unique_ptr
    4. 什么是循环引用?如何解决?解决的原理是什么

    相关知识:

    内存泄漏,资源泄漏:一个内存或资源,不使用了还没释放,就会导致资源泄漏/内存泄漏。

    内存泄漏危害:1、进程僵尸了,资源无法释放。2、长期运行的服务器一直泄漏。

    如何避免内存泄漏:
    1、事前预防:养成良好的编码规范;RAII思想或智能指针来管理资源。
    2、事后查错:内存泄漏工具。Linux:valgrind

  • 相关阅读:
    Flask实现简单的首页登录注销逻辑
    不安装运行时运行.NET程序
    快速复现 实现 facenet-retinaface-pytorch 人脸识别 windows上 使用cpu实现
    x86 与 x64 架构下函数参数传递的区别【汇编语言】
    前端JS必用工具【js-tool-big-box】学习,打开全屏和关闭全屏
    C. Joyboard
    康拓123发卡软件支持PN532读卡器
    校招期间 准备面试算法岗位 该怎么做?
    pytorch深度学习实战lesson32
    CentOS7 设置 nacos 开机启动
  • 原文地址:https://blog.csdn.net/mmz123_/article/details/124029863