【C++】-- 智能指针

目录

智能指针意义

智能指针的使用及原理

RAII

智能指针的原理

std::auto_ptr

std::auto_ptr的模拟实现 

std::unique_ptr

std::unique_ptr模拟实现

std::shared_ptr

std::shared_ptr的模拟实现

循环引用问题


智能指针意义

#问:为什么需要智能指针?

#include <iostream>
#include <stdexcept>

int div()
{
    int a, b;
    std::cin >> a >> b;
    if (b == 0)
        throw std::invalid_argument("除0错误");
    return a / b;
}

void Func()
{
    // 1、如果p1这里new 可能会抛异常。
    // 2、如果p2这里new 可能会抛异常。
    // 3、如果div调用也可能会抛异常。
    int *p1 = new int;
    int *p2 = new int;
    std::cout << div() << std::endl;
    delete p1;
    delete p2;
}

int main()
{
    try
    {
        Func();
    }
    catch (std::exception &e)
    {
        std::cout << e.what() << std::endl;
    }
    return 0;
}

        在一异常中,因为异常会导致程序的执行流乱跳,所以很多个会出现异常的代码,放在一起就很容易其中一个抛异常,而导致其余的未执行 / 需要释放的空间未释放。如上:p2出问题,需要释放p1,div出问题,需要释放p1、p2,智能指针就是用来解决这个问题。

智能指针的使用及原理

RAII

        RAII(Resource Acquisition Is Initialization - 获取资源即初始化)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。资源:需要手动释放的。(RAII:请求到志愿就初始化)

初始化指的是:
        调用一个其他类的构造函数,利用其他类的生命周期来进行管理。

        在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上:把管理一份资源的责任托管给了一个对象

        就是在获取到资源的时候,交给一个对象管理,于是在该对象的生命周期里这个资源始终有效。而这个对象无论如何是异常还是正常结束,都会调用这个对象的析构函数,于是利用这个对象的析构函数释放资源。

这种做法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

智能指针的原理

最基础的智能指针:

// 使用RAII思想设计的SmartPtr类
template <class T>
class SmartPtr
{
public:
    SmartPtr(T *ptr = nullptr) // 将资源给智能指针
        : _ptr(ptr) // 智能指针将资源保存
    {}
    
    ~SmartPtr()
    {
        if (_ptr)
            delete _ptr;
    }

private:
    T *_ptr;
};
        上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过 "->" 去访问所指空间中的内容,因此:智能指针模板类中还得需要将 "*" "->" 重载下,才可让其像指针一样去使用
#include <iostream>
#include <stdexcept>

// 1、利用RAII思想设计delete资源的类
// 2、像指针一样的行为
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr) // 将资源给智能指针
		:_ptr(ptr) // 智能指针将资源保存
	{}

	~SmartPtr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

private:
	T* _ptr;
};

int div()
{
    int a, b;
    std::cin >> a >> b;
    if (b == 0)
        throw std::invalid_argument("除0错误");
    return a / b;
}
void Func()
{
    // sp1、sp2出作用域会调用析构函数,抛异常,栈帧会正常结束
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);

	*sp1 = 0;
	*sp2 = 2;

	std::cout << div() << std::endl;
}

int main()
{
	try
	{
		Func();
	}
	catch (std::exception& e)
	{
		std::cout << e.what() << std::endl;
	}

	return 0;
}

总结一下智能指针的原理:
  1. RAII特性。
  2. 重载operator*和opertaor->,具有像指针一样的行为。

Note:

        智能指针看起来很完美,但是又一个致命的问题:智能指针的拷贝问题。默认的拷贝构造只会进行浅拷贝,就会导致一个地址被析构两次。主要原因就是:智能指针管理资源的释放。

解决方案:

问:深拷贝?

        不能,违背了智能指针的功能需求,需要的就是浅拷贝,智能指针不知道该空间有多大,只是对与指针的保存。

(问题先保留看看C++库中的解决方式)

std::auto_ptr

http://www.cplusplus.com/reference/memory/auto_ptr/

        C++98版本的库中就提供了auto_ptr的智能指针。
        auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份bit::auto_ptr来了解它的原理。
#include <iostream>
#include <memory>

class A
{
public:
	~A()
	{
		std::cout << "~A()" << std::endl;
	}

	int _a1 = 0;
	int _a2 = 0;
};

int main()
{
	std::auto_ptr<A> ap1(new A);
	ap1->_a1++;
	ap1->_a2++;

	std::auto_ptr<A> ap2(ap1);
	//ap1->_a1++;
	//ap1->_a2++;

	return 0;
}
        正是这个原因,所以:ap1就空了。

总结:

        std::auto_ptr是采用的资源管理权转移。但是,是不负责任的拷贝,会导致被拷贝对象悬空。所以多年以来被挂在耻辱柱上,很多公司明确要求不能使用它。

std::auto_ptr的模拟实现 

        注意:不是交换 —— 是管理权的转移,所以如果是:

std::auto_ptr<A> ap1(new A);
std::auto_ptr<A> ap2(new A);
ap2 = ap1;

        ap2是获取ap1的资源(资源管理权的转移) ,ap2之前的资源自动调用析构释放,ap1置空(nullptr)

namespace cr
{
	template<class T>
	class auto_ptr 
	{
	public:
		auto_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{}

        // 不是交换 —— 是管理权的转移
        // 不是交换
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

        // 不是交换 —— 是管理权的转移
		// ap1 = ap2;
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			if (this != &ap)
			{
				if (_ptr)
				{
					cout << "Delete:" << _ptr << endl;
					delete _ptr;
				}

				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}

			return *this;
		}

		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "Delete:" << _ptr << endl;
				delete _ptr;
			}
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

	private:
		T* _ptr;
	};
}

std::unique_ptr

        C++11中开始提供更靠谱的unique_ptr

https://cplusplus.com/reference/memory/unique_ptr/

        unique_ptr的 实现原理不让你拷贝 ,简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原理。只适用于不需要拷贝的场景。

std::unique_ptr模拟实现

namespace cr
{
	template<class T>
	class unique_ptr
	{
	private: // 防止有人跑到类外实现写一个浅拷贝

		// 防拷贝 C++98 - 当时还是boost库中
		// 只声明不实现 
		// unique_ptr(unique_ptr<T>& ap);
		// unique_ptr<T>& operator=(unique_ptr<T>& ap);
	public:
		unique_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{}

		// 防拷贝 C++11
		unique_ptr(unique_ptr<T>& ap) = delete;
		unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;

		~unique_ptr()
		{
			if (_ptr)
			{
				cout << "Delete:" << _ptr << endl;
				delete _ptr;
			}
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

	private:
		T* _ptr;
	};
}

std::shared_ptr

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr 。

#include <memory>
#include <iostream>

class A
{
public:
	~A()
	{
		std::cout << "~A()" << std::endl;
	}

	int _a1 = 0;
	int _a2 = 0;
};

void test_shared_ptr()
{
	std::shared_ptr<A> sp1(new A);
	std::shared_ptr<A> sp2(sp1);

	sp1->_a1++;
	sp1->_a2++;

	std::cout << sp2->_a1 << ":" << sp2->_a2 << std::endl;
    
	sp2->_a1++;
	sp2->_a2++;

	std::cout << sp1->_a1 << ":" << sp1->_a2 << std::endl;
}

int main()
{
    test_shared_ptr();
    return 0;
}

        shared_ptr的 原理是通过引用计数的方式来实现多个shared_ptr对象之间共享资源 。例如: 学校老师晚上在下班之前都会通知,让最后走的学生记得把门锁下,并且打扫一下教室。

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享
  2. 对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

std::shared_ptr的模拟实现

        不能用static修饰计数器,因为这样会让所有的模拟实现的shared_ptr的不同类型的实例化,同用一个计数器。需要的是一个资源,配一个计数器,多个智能指针对象共管静态计数对象,所以资源都只有一个计数,因为静态成员属于整个类,属于类的所有对象。

        所以使用的是一个指针,这个时候这个指针指向的空间就是new出来的,使用构造函数new,这就保证了同一个资源,用一个计数器。并且这样这个指针就会指向需释放的资源,也指向了计数。

        即:每个资源需要管理的时候,会给构造函数,构造new一个计数。

namespace cr
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _pCount(new int(1))
		{}

        // 判断是否释放
		void Release()
		{
            // 减减被赋值对象的计数,如果是最后一个对象,要释放资源
			if (--(*_pCount) == 0)
			{
				cout << "Delete:" << _ptr << endl;
				delete _ptr;
				delete _pCount;
			}
		}

		~shared_ptr()
		{
			Release();
		}

		// sp1(sp2)
		shared_ptr(const shared_ptr<T>& sp)
			: _ptr(sp._ptr)
			, _pCount(sp._pCount)
		{
			(*_pCount)++;
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
            // 防止自己给自己赋值,导致计数--,出现内存泄漏
			if (_ptr == sp._ptr)
			{
				return *this;
			}

			// 判断是否释放 -- 防止_ptr原来的数据未计数--,导致内存泄漏
			Release();

			// 共管新资源,++计数
			_ptr = sp._ptr;
			_pCount = sp._pCount;

			(*_pCount)++;

			return *this;
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

	private:
		T* _ptr;

		// 引用计数
		int* _pCount;
	};
}

shared_ptr的问题:

shared_ptr看起来很完美,但是也有问题:

  • 在多线程使用shared_ptr的时候,shared_ptr不是线程安全的。
  • shared_ptr存在一个循环引用的问题。

循环引用问题

(是一个非常特殊的情况下)

#include <iostream>
#include <memory>

class Node
{
public:
    ~Node()
    {
        std::cout << "~Node" << std::endl;
    }

    int val = 0;
    std::shared_ptr<Node> _next;
    std::shared_ptr<Node> _prev;
};

void test_shared_ptr()
{
    std::shared_ptr<Node> s1(new Node);
    std::shared_ptr<Node> s2(new Node);
    
    s1->_next = s2;
    s2->_prev = s1;
}

int main()
{
    test_shared_ptr();
    return 0;
}

        我们可以发现上面代码并没有像我们想象的一样,自动调用析构函数:

分析:

        当下图的时候,是没有问题的,也就是对于shared_ptr智能指针的使用,shared_ptr的计数器分别都++。

        但是当继续向下执行的时候,就是问题的关键所在。

        此处:shared_ptr类型的s1,经过operator ->的重载,于是变为(Node*)->_next = s2,此处Note中的_next对象是shared_ptr类型的,所以会调用s2中的智能指针的赋值。

        同理:会调用s1中的智能指针的赋值。

        于是s1与s2的计数器分别都++到了2。

 #问:通过以上为什么不会调用析构?

        因为,根据函数的创建顺序,s2后构造所以先析构,s1先构造所以后析构。

        于是s2与s1就分别调用它们的析构函数了,于是计数器分别都--到了1。但是由于:

  • _next:管着右边的节点内存块。
  • _prev:管着左边的节点内存块。

        于是,便出现:理想状态下,_next释放那右边就释放,_prev释放那左边就释放。

        但是会出现一个问题,_next与_prev分别数据不同的成员节点,对于对象中的成员是需要该对象调用析构函数才会释放其中的成员,于是对于左节点是需要计数器--到0,才能释放左节点,_next才会析构。同样的,右节点是需要计数器--到0,才会释放右节点,_prev才会析构

        这就是一个死循环,所以没有调用Node的析构函数,也就不可能看到Node类型对象的析构释放显示。

解决方式:

         这个地方想使用shared_ptr进行Node类型的对象中类似的操作是把必定错的,没有办法,所以C++也为我们提供了一个新的方式:weak_ptr(弱指针)

        weak_ptr与其他的智能指针都不一样,其并不是常规智能指针,没有RAII,不支持直接管理资源。weak_ptr主要用shared_ptr构造,用来解决shared_ptr循环引用问题

https://legacy.cplusplus.com/reference/memory/weak_ptr/?kw=weak_ptr

        主要用share_ptr来构造自己,并不支持用一个资源构造自己,所以其并不支持RAII。

  • 特点:不++share_ptr的计数器。
#include <iostream>
#include <memory>

class Node
{
public:
    ~Node()
    {
        std::cout << "~Node" << std::endl;
    }

    int val = 0;
    std::weak_ptr<Node> _next;
    std::weak_ptr<Node> _prev;
};

void test_shared_ptr()
{
    std::shared_ptr<Node> s1(new Node);
    std::shared_ptr<Node> s2(new Node);
    
    s1->_next = s2;
    s2->_prev = s1;
}

int main()
{
    test_shared_ptr();
    return 0;
}

        当 _next 与 _prev 是 weak_ptr 的时候,它们不参加资源的释放管理,但是可以访问和修改数据,且并不增加计数,所以不会存在循环引用的问题了。(这个方法可行,但是因为不计数,多以我们要识别到这个问题)

Note:

        想看到计数的对应变化,可以使用shared_ptr的成员函数 use_count

weak_ptr的模拟实现

        对于前面所模拟实现的shared_ptr同样的也具有循环引用的问题。

#include <iostream>

namespace cr
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr): _ptr(ptr), _pCount(new int(1)){}

        // 判断是否释放
		void Release()
		{
            // 减减被赋值对象的计数,如果是最后一个对象,要释放资源
			if (--(*_pCount) == 0)
			{
				delete _ptr;
				delete _pCount;
			}
		}

		~shared_ptr(){Release();}

		// sp1(sp2)
		shared_ptr(const shared_ptr<T>& sp): _ptr(sp._ptr), _pCount(sp._pCount){(*_pCount)++;}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
            // 防止自己给自己赋值,导致计数--,出现内存泄漏
			if (_ptr == sp._ptr)
				return *this;

			// 判断是否释放 -- 防止_ptr原来的数据未计数--,导致内存泄漏
			Release();
			// 共管新资源,++计数
			_ptr = sp._ptr;
			_pCount = sp._pCount;
			(*_pCount)++;
			return *this;
		}
		T& operator*(){return *_ptr;}
		T* operator->(){return _ptr;}
	private:
		T* _ptr;
		// 引用计数
		int* _pCount;
	};
}

class Node
{
public:
    ~Node(){std::cout << "~Node" << std::endl;}

    int val = 0;
    cr::shared_ptr<Node> _next;
    cr::shared_ptr<Node> _prev;
};

void test_shared_ptr()
{
    cr::shared_ptr<Node> s1(new Node);
    cr::shared_ptr<Node> s2(new Node);
    s1->_next = s2;
    s2->_prev = s1;
}

int main()
{
    test_shared_ptr();
    return 0;
}

        于是可以通过模拟实现weak_ptr进行避免我们所模拟实现的shared_ptr,出现循环引用的问题。


#include <iostream>

namespace cr
{
    template <class T>
    class shared_ptr
    {
    public:
        shared_ptr(T *ptr = nullptr) : _ptr(ptr), _pCount(new int(1)) {}

        // 判断是否释放
        void Release()
        {
            // 减减被赋值对象的计数,如果是最后一个对象,要释放资源
            if (--(*_pCount) == 0)
            {
                delete _ptr;
                delete _pCount;
            }
        }

        ~shared_ptr() { Release(); }

        // sp1(sp2)
        shared_ptr(const shared_ptr<T> &sp) : _ptr(sp._ptr), _pCount(sp._pCount) { (*_pCount)++; }

        shared_ptr<T> &operator=(const shared_ptr<T> &sp)
        {
            // 防止自己给自己赋值,导致计数--,出现内存泄漏
            if (_ptr == sp._ptr)
                return *this;

            // 判断是否释放 -- 防止_ptr原来的数据未计数--,导致内存泄漏
            Release();
            // 共管新资源,++计数
            _ptr = sp._ptr;
            _pCount = sp._pCount;
            (*_pCount)++;
            return *this;
        }

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

        // 便于外部获取ptr,如weak_ptr
        T *get()const
        {
            return _ptr;
        }

    private:
        T *_ptr;
        // 引用计数
        int *_pCount;
    };

    // 辅助型智能指针,使命配合解决shared_ptr循环引用问题
    template <class T>
    class weak_ptr
    {
    public:
        weak_ptr()
            : _ptr(nullptr)
        {}

        weak_ptr(const shared_ptr<T> &sp)
            : _ptr(sp.get())
        {}

        weak_ptr(const weak_ptr<T> &wp)
            : _ptr(wp._ptr)
        {}

        weak_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            _ptr = sp.get();
            return *this;
        }

        T& operator*()
        {
            return *_ptr;
        }

        T* operator->()
        {
            return _ptr;
        }

    private:
        T *_ptr;
    };
}

class Node
{
public:
    ~Node() { std::cout << "~Node" << std::endl; }

    int val = 0;

    cr::weak_ptr<Node> _next;
    cr::weak_ptr<Node> _prev;
};

void test_shared_ptr()
{
    cr::shared_ptr<Node> s1(new Node);
    cr::shared_ptr<Node> s2(new Node);
    s1->_next = s2;
    s2->_prev = s1;
}

int main()
{
    test_shared_ptr();
    return 0;
}

需要掌握:

  • 为什么需要智能指针?

        主要的原因还是因为内存泄漏:忘记释放的问题,更重要还有异常安全的问题。

  • RAII?

        资源获得,资源请求机立即初始化。指的就是把资源交给一个对象,利用构造将其资源交给一个对象去管理。

  • 发展历史
  • auto_ptr / unique_ptr / shared_ptr / weak_ptr之间的区别与使用场景。
  • 模拟实现简洁版智能指针。
  • 什么是循环引用?如何解决循环引用?解决的原理是什么?
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川入

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值