当前位置: 首页 > news >正文

C++ 智能指针底层逻辑揭秘:优化内存管理的核心技术解读

目录

0.为什么需要智能指针?

1.智能指针的使用及原理

RAII:

智能指针的原理:

2.智能指针有哪些?

std::auto_ptr

std::unique_ptr

std::shared_ptr

std::weak_ptr


0.为什么需要智能指针?

        想要回答这个问题,首先要来看一个没有智能指针存在的场景:

int div()
{
     int a, b;
     cin >> a >> b;
     if (b == 0)
     throw invalid_argument("除0错误");
     return a / b;
}
void Func()
{
     int* p1 = new int;
     int* p2 = new int;
     cout << div() << endl;
     delete p1;
     delete p2;
}
int main()
{
     try
     {
         Func();
     }
     catch (exception& e)
     {
         cout << e.what() << endl;
     }
     return 0;
}

思考:如果p1这里new 抛异常会如何?如果p2这里new 抛异常会如何?如果div调用又会抛异常会如何?毫无疑问,如果new开辟空间时抛出异常亦或是div调用时候抛出异常都会导致p1p2就没有及时释放,造成内存泄露!但如果我们依次在抛出异常前手动释放掉资源又会显得繁琐,这时就需要智能指针

1.智能指针的使用及原理

        先来介绍一下智能指针运用到了什么原理:

RAII:

        RAII(Resource Acquisition Is Initialization)是一种 利用对象生命周期来控制程序资源 (如内存、文件句柄、网络连接、互斥量等等)的简单技术。
         在对象构造时获取资源 ,接着控制对资源的访问使之在对象的生命周期内始终保持有效, 最后在对象析构的时候释放资源 借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:不需要显式地释放资源采用这种方式,对象所需的资源在其生命期内始终保持有效。
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
    SmartPtr(T* ptr = nullptr)
       : _ptr(ptr)
   {}
    ~SmartPtr()
   {
        if(_ptr)
            delete _ptr;
   }
    
private:
    T* _ptr;
};

        上面就是根据RAII思想大致设计出的智能指针,指针内部存储的是对象的地址,这样就相当于把资源的地址交给一个类对象管理,当这个类销毁时调用析构函数就不需要手动释放了!

智能指针的原理:

        上面其实与真正的智能指针的框架大差不差,但是因为还要具备指针的行为,因此需要重载一下operator*以及operator->两个操作符函数:

template<class T>
class SmartPtr {
public:
     SmartPtr(T* ptr = nullptr)
         : _ptr(ptr)
     {}
     ~SmartPtr()
     {
         if(_ptr)
             delete _ptr;
     }
    T& operator*() {return *_ptr;}
    T* operator->() {return _ptr;}
private:
    T* _ptr;
};

        这样就有了一个完整智能指针的雏形了~
 

2.智能指针有哪些?

std::auto_ptr

        C++98版本的库中就提供了auto_ptr的智能指针,让我们先来看一下关于auto_ptr的一些介绍:

        但其实auto_ptr是许多程序员不喜欢使用的智能指针,因此存在指针悬空的问题:

void test()
{
	//c++98中提供auto_ptr
	auto_ptr<A> ap1(new A(1));
	auto_ptr<A> ap2(new A(2));
	auto_ptr<A> ap3(ap1);
	//这里用sp1去构造sp3就会出现指针悬空的问题
	(ap1->a)++;//sp1被置为空后再去访问就会报错
	(ap2->a)++;

}

        在上面的场景中就出现了指针悬空的问题,这里进行了管理权转移,ap1将管理权交给了ap3,那么ap3构造的时候会将ap1置为nullptr,如果此时再去对ap1进行访问就属于非法的!

下面来简单实现一下auto_ptr,以便我们明了具体结构:

思路:

---1--- 其实大致框架已经在前面给出了,完成了operator->以及operator*函数的重写,这里需要完成的是拷贝构造函数

---2--- 在拷贝构造函数中,需要把地址传给成员变量,将参数的地址置为nullptr

template<class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr)
		:_ptr(ptr)
	{ }

	auto_ptr(auto_ptr<T>& ptr)
		:_ptr(ptr._ptr)
	{
		ptr._ptr = nullptr;
	}

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

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

         如果不想要这种危险的情况发生,可以使用std::unique_ptr:

std::unique_ptr

        顾名思义,unique则代表的是唯一,也就是说此智能指针不能拷贝构造一个新的同类型对象,先让我们看看介绍:

        那么如果你试图用一个unique_ptr对象去拷贝构造一个新对象,这种行为在编译时就会报错:

void test()
{
    unique_ptr<A> up1(new A(1));
    unique_ptr<A> up2(new A(2));
    //unique_ptr直接进行了反拷贝操作,禁止了这种指针拷贝行为
    unique_ptr<A> up3(up1);
}

 为什么会有删除函数这样的报错,是因为unique_ptr的底层实现了反拷贝:

        我们来模拟实现一下unique_ptr:

思路:

---1--- 大致框架还是相同,不同的是这样要如何实现反拷贝?

---2--- 如果你使用C++98那么反拷贝应该将拷贝构造函数以及赋值函数只声明不实现

---3--- 如果你使用C++11那么只需将这两个函数设置为=delete即可,这也是为什么报错显示函数已经被删除

template<class T>
class unique_ptr
{
public:
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{
	}


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

	T* operator->()
	{
		return _ptr;
	}
private:
	//unique这里用到了反拷贝,c++98中也可以只声明不实现,将声明放到private中
	unique_ptr(unique_ptr<T>& ptr) = delete;

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

	T* _ptr;
};

         那么如果我就是要实现可以拷贝构造和赋值构造的智能指针呢?登场的是std::shared_ptr:

std::shared_ptr

        顾名思义,shared就是可以共享,那么就代表此类型的智能指针可以实现多个指针指向同一块内存,先来看看介绍:

        那么如果你使用shared_ptr去构造和赋值,都是可以通过编译的:

void test()
{
	bit::shared_ptr<A> sp1(new A(1));
	bit::shared_ptr<A> sp2(new A(2));
	bit::shared_ptr<A> sp3(sp1);//这里支持拷贝,是因为实现了引用计数
	sp2 = sp3;
}

         话不多说,来看看底层实现:

思路:

---1--- 框架没有变化,但是如果要实现shared_ptr的拷贝构造和赋值构造就要一点难度了:

---2--- 为什么shared_ptr可以支持多个对象指向同一内存?这里运用了引用计数,每个对象销毁时计数-1,直到减为0时,这块内存发生析构释放,这里最适合来进行技术的是int*类型的对象,为什么不是static或者是int,static只会实例化一份,无法实现多个对象的引用计数,int又会导致每个对象的引用计数都是拷贝,无法从根本修改计数

---3--- 默认构造:先new int,初始化为1即可

---4--- 拷贝构造:将指针赋值,将计数+1,并把地址赋值一下

---5--- 赋值构造:不仅仅是计数的增加以及指针的赋值,还要考虑原指针指向的内存是否应该释放

template<class T>
class shared_ptr
{
public:

	shared_ptr(T* ptr=nullptr)
		:_ptr(ptr)
		, _pcount(new int(1))
	{}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
	{
		++(*(sp._pcount));
		_pcount = sp._pcount;
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		//在改变被赋值对象的指针之前,先要考虑是否要释放被赋值对象管理的内存资源
		//也就是说改变指针之前要对原理管理资源的pcount--,如果减到0就需要释放

		//首先要考虑指向通过一块内存的两个指针相互赋值,如果不同名只是麻烦一点,如果同名那么就会造成释放后再去访问的问题
		if (sp._ptr == _ptr)
		{
			return *this;
		}

		//对于被赋值的要记得减去引用计数来决定是否释放内存,防止造成内存泄漏
		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
		}
		
		_ptr = sp._ptr;
		++*(sp._pcount);
		_pcount = sp._pcount;
		return *this;
	}

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

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

	T* get()const//返回指针
	{
		return _ptr;
	}

	size_t use_count()const//返回引用计数的个数
	{
		return *_pcount;
	}

	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			_del(_ptr);
			delete _pcount;
		}
	}
private:
	T* _ptr;
	//shared_ptr可以支持多个指针指向同一块空间,怎么实现?
	//引用计数,如何实现?使用static int不可行,因为多个对象只能有一个static,
	//但是每一块资源都需要一个计数
	int* _pcount;

};

        但是,shared_ptr也会产生相应的问题,那就是引用循环:

        引用循环(Reference Cycle),也被称为循环引用,它会引发内存泄漏等问题。

        定义:引用循环指两个或多个对象之间相互持有对方的引用,形成一个闭环引用结构,导致这些对象无法被正常释放,造成内存资源的浪费。例如,对象 A 持有对象 B 的引用,而对象 B 又持有对象 A 的引用,这样就形成了引用循环。

        假设存在两个类AB ,它们的定义如下:

#include <memory>
class B;
class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() {
        std::cout << "A destroyed" << std::endl;
    }
};
class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() {
        std::cout << "B destroyed" << std::endl;
    }
};

void test() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_ptr = a;
}

        在test函数结束时,ab所指向的对象本应被释放,但由于它们相互引用,引用计数都不会降为 0,这里值得细说一下:a_ptr由b管理,当b析构时a_ptr发生析构,但是b_ptr又管理b,当b_ptr发生析构,b才会析构,那么b_ptr由谁管理呢?a,当a发生析构b_ptr才会析构,但是a又被a_ptr管理着,这就构成了死循环,最后引用计数由2->1,会导致内存泄漏。

        如何解决?weak_ptr登场:不过在这之前先要补充定制删除器:如果让智能指针管理开辟的数组,该如何释放呢?shared_ptr提供了这样的接口:

这里的D可以传入函数指针,仿函数亦或是lambda表达式

void test_sp4()
{
	//如果new了一个数组,又该如何释放呢?
	//了解定制删除器:new A[10]
	//template <class U, class D> shared_ptr (U* p, D del);
	//template <class D> shared_ptr(nullptr_t p, D del);
	//库中用了模板D来控制,其实是仿函数,同时也可以接收lameda表达式
	std::shared_ptr<A> sp1(new A[10], [](const A* ptr) { delete[] ptr; });
	std::shared_ptr<A> sp2((A*)malloc(sizeof(A)*10), Destroy<A>());
	std::shared_ptr<FILE> sp3(fopen("SmartPtr.hpp", "r"), [](FILE* ptr) {return fclose(ptr); });

//ps:这里的A是作者自定义的类
}

 这是怎么做到的呢?其实是用到了U实例化后的包装器来接收传入的函数指针,仿函数亦或是lambda表达式,并使用模板来处理,这样在析构函数的时候使用包装器传入要释放资源的指针即可:(了解一下,理解即可)

template<class T>
class shared_ptr
{
public:

	template<class D>
	shared_ptr(T* ptr,D del)
		:_ptr(ptr)
		,_pcount(new int(1))
		,_del(del)
	{}

	function<void(T*)> _del;//使用包装器来解决模板参数无法涉及析构函数的问题
};

std::weak_ptr

        weak_ptr可以理解为是专门用于处理循环引用的问题的指针,但请注意并没有采用RAII思想来搭建,所以并不属于智能指针:

可以看到,weak_ptr并不支持传参构造,通常用shared_ptr去构造weak_ptr,那么如果这样使用weak_ptr就可以正常析构:

#include <memory>
class B;
class A {
public:
    std::weak_ptr<B> b_ptr;//这里改成了weak_ptr
    ~A() {
        std::cout << "A destroyed" << std::endl;
    }
};
class B {
public:
    std::weak_ptr<A> a_ptr;//这里改成了weak_ptr
    ~B() {
        std::cout << "B destroyed" << std::endl;
    }
};

void test() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_ptr = a;
}

原理是什么呢?其实weak_ptr并没有处理shared_ptr中的引用计数部分,只负责了指针的拷贝,这才使得循环引用得以解决,请注意weak_ptr不属于RAII智能指针,可以参与资源的访问,但是不参与资源的释放!!!

模拟实现一下:

思路:

---1--- 其实很简单,只用实现shared_ptr构造和默认构造即可,直接给出:

template<class T>
class weak_ptr
{
public:
	weak_ptr()//不支持传参构造
		:_ptr(nullptr)
	{
	}

	weak_ptr(const shared_ptr<T>& sp)//支持sharedptr构造
		:_ptr(sp._ptr)
	{ }


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

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

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

相关文章:

  • Java 中常见的数据结构
  • 3、组件:魔法傀儡的诞生——React 19 组件化开发全解析
  • 【Python爬虫】详细入门指南
  • UNet深度学习实战遥感航拍图像语义分割
  • Java雪花算法
  • RabbitMQ的应用
  • mysql和mongodb
  • React 之 Redux 第三十二节 Redux 常用API及HOOKS,以及Redux Toolkit核心API使用详解
  • 62. 评论日记
  • java 实现文件编码检测的多种方式
  • Podman技术深度解剖:架构、原理与核心特性解析
  • cocos Spine资源及加载
  • JavaScript Map 对象深度解剖
  • HarmonyOS 第2章 Ability的开发,鸿蒙HarmonyOS 应用开发入门
  • 开源FMC 4路千兆网模块
  • Git 基本使用
  • 塑料瓶识别分割数据集labelme格式976张1类别
  • CASAIM与中国中车达成深度合作,助力异形大部件尺寸精准分析
  • TCPIP详解 卷1协议 四 地址解析协议
  • gcc/g++使用
  • 张涌任西安市委常委,已卸任西安市副市长职务
  • 夜读丨读《汉书》一得
  • 4月份全国企业销售收入同比增长4.3%
  • 甘肃发布外卖食品安全违法行为典型案例:一商家用鸭肉冒充牛肉被罚
  • 视频丨美国两名男童持枪与警察对峙,一人还试图扣动扳机
  • 走进“双遗之城”,领略文武风采:沧州何以成文旅新贵