代码编织梦想

前言:
在我们之前学习异常的时候,讲到过异常安全的问题,会有内存泄露的问题。

  • 内存泄露这个问题对程序员的要求很高,申请的空间就必须要手动释放,不像Java这种语言自带垃圾回收器(gc)。
  • 就算是我们手动释放了空间,也有可能存在内存泄露的问题(异常安全),抛异常时会乱跳,有可能就会导致即使手动释放了,也没会内存泄露。
  • 上节在异常种我们可以通过拦截异常手动释放掉,但是防不胜防并不是所有的都能拦截到,于是C++就引入了智能指针。

内存泄漏是指针丢了还是内存丢了?

答:所有的内存泄露都是指针丢了。。
1.内存还在,进程正常结束,内存也会释放
2.僵尸进程有内存泄露,比较可怕。
3.服务器都是长期运行的。

1 RAII思想:

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

  • 在对象构造时获取资源
  • 在对象析构的时候释放资源。

借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

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

> 注意:RAII只是智能指针的一种思想,切不可说RAII就是智能指针

1.1 拦截异常解决不了的内存泄漏:

在上节中我们讲到了在抛异常时会出现内存泄漏的问题,即使是手动释放掉内存也会出现内存泄漏,原因时抛异常时直接跳到了捕获的地方,所以会泄露。

  • 对于上述问题我们也给出了对应的解决办法,那就是拦截异常的方式
  • 为了避免内存泄漏,我们先将异常拦截下来,先释放掉再将捕获的异常抛出
void func()
{
	int* p1 = new int[10]; //这里亦可能会抛异常
	int* p2 = new int[10]; //这里亦可能会抛异常 -- 这里抛异常,p1就没释放掉
	int* p3 = new int[10]; //这里亦可能会抛异常
	int* p4 = new int[10]; //这里亦可能会抛异常

	try
	{
		div();
	}
	catch (...)
	{
		delete[] p1;
		delete[] p2;
		delete[] p3;
		delete[] p4;

		throw;
	}

	delete[] p1;
	delete[] p2;
	delete[] p3;
	delete[] p4;
}

假设我们每个new出来的空间非常大,我们也不确定到底是哪个new失败了
所以,C++就提供了智能指针

  • 利用对象生命周期的特性:出了作用域之后自动调用对象的析构函数,通过析构函数来释放空间。
  • 无论如何都会正常释放资源,抛异常也好,中间抛异常也好,或者是正常结束,出了作用域就调用对象析构函数。

2. 智能指针的原理

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

  • RAII特性
  • 重载operator*和opertaor->,具有像指针一样的行为
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;
};

3 直接拷贝带来的问题:

智能指针衍生的问题~

智能指针管理资源的方式我们不难理解,但是智能指针的拷贝却是个令人头疼的问题

  1. 我们知道我们只是将指针封装了一层
  2. 如果是简单的只拷贝的话,会出两个指针指向同一块资源
  3. 在释放的时候会发生同一块空间释放多次的问题

智能指针最大的问题:在于拷贝构造问题**(拿一个已经有的,来可瓯北另外一个)**

3.1 auto_ptr:

在这里插入图片描述
核心思想:

  • 管理权转移,被拷贝的对象悬空。
namespace Joker
{
	template<class T>
	class auto_ptr
	{
	public:
		//RAII思想
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

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

		//sp2(sp1) -- 拷贝构造(简直就是神人)
		auto_ptr(auto_ptr<T>& sp)
			:_ptr(sp._ptr)
		{
			//将sp1置空,交给sp2管了,sp1不管了
			sp._ptr = nullptr;
		}
      //ap1=ap2
      auto_ptr<T>& operator=(auto_ptr<T>& ap)
      {
             if(this!=&ap)
             {
                 if(_ptr)
                 {
                   delete _ptr;
                 }
                 _ptr=ap._ptr;
                 ap._ptr=nullptr;
             }
      }

		//像指针一样
		T& operator*()
		{
			return *_ptr;
		}

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

		T* get()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

思路:

  • auto_ptr支持拷贝,但是方式很挫
  • 拷贝之后直接将原来的指针给置空了
  • 这要是不知情的人使用了原来指针,直接就造成非法访问

3.2 unique_ptr:

我们再来看C++11给的解决办法:
在这里插入图片描述
核心思想:

  • 不让拷贝 / 防拷贝 — 拷贝编译就报错。
namespace Joker
{
	//不能拷贝用unique_ptr
	template<class T>
	class unique_ptr
	{
	public:
		//RAII思想
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

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

		//像指针一样
		T& operator*()
		{
			return *_ptr;
		}

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

		T* get()
		{
			return _ptr;
		}

		//C++98的方式:
		//private:
		//sp2(sp1)
		//1、只声明,不实现(不声明会默认生成一个)
		//2、声明成私有
		//不过还是有问题,在类里面可以调用
		//unique_ptr(const unique_ptr<T>& sp);

		//C++11的方式:防拷贝
		unique_ptr(const unique_ptr<T>& sp) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;

	private:
		T* _ptr;
	};
}

思想:

- unique直接不给拷贝,防止拷贝,但是功能不全
在这里插入图片描述
在这里插入图片描述

3.3 shared_ptr:

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr:
在这里插入图片描述
核心思想:

  • 核心原理就是引用计数,记录几个对象管理这块资源,析构的时候 - -(减减)计数,最后一个析构的对象释放资源。

> 思路:

  • 用到引用计数的方式,拷贝就计数++,析构就计数- -
  • 最后一个析构的对象释放资源

这时,对计数这个变量(count)就有要求了,要求共同管理同一个对象的时候要做到对同一个count ++ 或 - -

  • 直接定义成成员变量(不行,对象内存空间独立)
  • 定义一个static 的成员变量,管理多个资源的时候,就会出问题。(静态成员变量为所有类共享,地址都是一样的)
  • 在堆上开辟一段空间,引用计数放在堆上(int* count)—构造时,给count在堆上申请空间。
private:
   T* _ptr;
   int* _pcount;

shared_ptr的代码实现:

namespace joker
{
     template<class T>
      class shared_ptr
     {
     public:
      //RAII思想
          shared_ptr(T* ptr = nullptr)
          :_ptr(ptr)
          , _pRefCount(new int(1))
          , _pmtx(new mutex)
           {}
           
         //拷贝构造
         shared_ptr(const shared_ptr<T>& sp)
         : _ptr(sp._ptr)
         , _pRefCount(sp._pRefCount)
         , _pmtx(sp._pmtx)
         {
            AddRef();
         }


      //引用计数释放资源专用函数
       void Release()
      {
        _pmtx->lock();
        bool flag = false;
        if (--(*_pRefCount) == 0 && _ptr) %考虑 _ptr为nullptr时,不要delete了。
       {
          cout << "delete:" << _ptr << endl;
          delete _ptr;
          delete _pRefCount;
          flag = true;
      }
      _pmtx->unlock();
     if (flag == true)
      {
        delete _pmtx;
      }
    }

    void AddRef()
    {
      _pmtx->lock();
      ++(*_pRefCount);
      _pmtx->unlock();
    }
    
   //赋值重载
    shared_ptr<T>& operator=(const shared_ptr<T>& sp)
   {
      //if (this != &sp)
     if (_ptr != sp._ptr)
    {
      Release();
      _ptr = sp._ptr;
      _pRefCount = sp._pRefCount;
      _pmtx = sp._pmtx;
      AddRef();
      }
      return *this;
    }
    int use_count()
   {
     return *_pRefCount;
   }

//析构函数
    ~shared_ptr()
   {
     Release();
   }

// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pRefCount;
mutex* _pmtx; %多线程使用shared_ptr,会出现线程安全问题
};

注意:shared_ptr会存在多线程安全问题,所以要使用互斥锁。

3.4 循环引用的问题:

我们先来看一段代码:

struct ListNode
{
	Joker::shared_ptr<ListNode> _prev = nullptr;
	Joker::shared_ptr<ListNode> _next = nullptr;

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

int main()
{
	Joker::shared_ptr<ListNode> p1(new ListNode);
	Joker::shared_ptr<ListNode> p2(new ListNode);

	p1->_next = p2;
	p2->_prev = p1;

	return 0;
}

为了更好理解上面的问题,看下图:

在这里插入图片描述

3.5 weak_ptr:

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

在这里插入图片描述

  • 其他的智能指针的构造函数可以传一个指针;
  • weak_ptr构造函数不支持接收指针,不管理资源
  • 它接收一个shared_ptr,可以通过shared_ptr来构造weak_ptr
  • 可以指向一块空间,但是不参与空间的管理

构造函数:
在这里插入图片描述

  • 可以说 weak_ptr是shared_ptr的小弟 — 不是传统的智能指针
  • 专门 用来辅助解决shared_ptr循环引用的问题
// 简化版本的weak_ptr实现
template<class T>
class weak_ptr
{
public:
    weak_ptr()
    :_ptr(nullptr)
    {}
    
     weak_ptr(const shared_ptr<T>& sp)
    :_ptr(sp.get())
    {}
    
     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;
 };

解决方案:使用weak_ptr

struct ListNode
{
	Joker::weak_ptr<ListNode> _prev = nullptr;
	Joker::weak_ptr<ListNode> _next = nullptr;

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
% _next 和_prev是weak_ptr时,不参与资源释放管理,可以访问和修改到资源,但是不增加计数,不存在循环引用问题。

int main()
{
	Joker::shared_ptr<ListNode> p1(new ListNode);
	Joker::shared_ptr<ListNode> p2(new ListNode);

	p1->_next = p2;
	p2->_prev = p1;

	return 0;
}

3.6 定制删除器:

通过给智能指针unique_ptr和shared_ptr传递一个可调用对象,来定制析构的具体行为

在这里插入图片描述
构造函数
在这里插入图片描述

注意:
传递给智能指针的定义删除器可调用对象,可以是仿函数,lambda表达式,函数指针等

尾声

看到这里,相信大家对这个C++有了解了。
如果你感觉这篇博客对你有帮助,不要忘了一键三连哦

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_42976474/article/details/139027805

c++智能指针 示例讲解-爱代码爱编程

  智能指针实际上就是通过模板技术实现的一个类 内存泄露(臭名昭著的Bug)——在软件开发和测试阶段都不容易被发现 -动态申请堆空间,用完后不归还 -C++语言中没有垃圾回收的机制 -指针无法控制所指堆空间的生命周期 当代C++软件平台中的智能指针 -指针生命周期结束时主动释放堆空间 -一片堆空间最多只能由一个指针标识 -杜绝指针运算和指针比较

C++智能指针-爱代码爱编程

智能指针对象是一个对象,可以充当函数参数也可以充当函数返回值。 智能体现在内存管理(内存的申请和释放)(在对象的析构函数中做了资源的释放) 用智能指针管理new的对象将不在需要手动delete 智能指针本质上是一个模板类,一般使用的是这个类的对象,而不是指针(弱化指针) shared_ptr get() 函数: 返回数据的指针的引用 use_

智能指针c++_随风流_的博客-爱代码爱编程

平常我们在使用指针的时候,如果使用完没有释放会造成内存泄漏,为了解决这个问题,就出现了智能指针。当然,你说自己注意点保证每次就会触发析构函数删除指针不会导致内存泄漏也可以,这也是基于RAII的思想,而智能指针同样也是从此演变而来。 我看了一篇写的非常好的文章,从底层开始讲解,于是我决定参照这篇文章并加上一些自己的想法,把智能指针的源码也摆上来一同比较。

c++11智能指针知识汇总与整理-爱代码爱编程

一、智能指针原理         智能指针是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保在离开指针所在作用域时,自动正确地销毁动态分配的对象,防止内存泄露。它一种通用实现技术是使用引用计数,每使用它一次,内部的引用计数加1,每析构一次,内部引用计数减1,减为0时,删除所指向的堆内存。         C++11提供了3种智能指针:std

【c++学习】智能指针-爱代码爱编程

🐱作者:一只大喵咪1201 🐱专栏:《C++学习》 🔥格言:你只管努力,剩下的交给时间! 智能指针 🥮智能指针🍢为什么需要智能指针🍢RAII 🥮auto_ptr🥮unique_ptr🥮sha

c++智能指针入门教程_打印 shared_ptr地址-爱代码爱编程

搬运油管视频 通过代码示例讲解智能指针的概念。 概述 智能指针是原始指针的封装,其优点是会自动分配内存,不用担心潜在的内存泄露。 c++有3中不同类型的智能指针: unique_ptr weak_ptr shared_p

【c++】智能指针详解_c++智能指针-爱代码爱编程

今天我们来讲一下c++中的智能指针。 目录 1. 智能指针初识1.1 什么是智能指针1.2 智能指针发展历史1.3 为什么需要智能指针 3. 智能指针原理3.1 RALL3.2 智能指针的分类3.2.1

c++父类指针指向子类-爱代码爱编程

有一个常见的c++题,就是父类和子类的构造函数和析构函数分别调用顺序: 父类构造函数子类构造函数子类析构函数父类析构函数 以及父类中的函数在子类中重新实现后,父类指针指向子类后,该指针调用的函数是父类中的还是子类中的(子

c++复习 -爱代码爱编程

类: 概念 C++ 中的类(class)是一种编程结构,用于创建对象。 这些对象可以拥有属性(即数据成员)和行为(即成员函数或方法)。类的概念是面向对象编程的核心之一,其主要目的是将数据和与数据相关的操作封装在一起。例如,如果你有一个“汽车”类,它可能包含颜色、品牌、型号等属性(数据成员),以及启动、停止、加速等行为(成员函数)。每当你基于这个类

【ccf-爱代码爱编程

题目描述 西西艾弗岛上埋藏着一份宝藏,小 C 根据藏宝图找到了宝藏的位置。藏有宝藏的箱子被上了锁,旁边写着一些提示: 给定 n 条指令,编号为1∼n,其中每条指令都是对一个双端队列的操作,队列中的元素均为 2×22×2 的矩阵;在某些时刻,某一条指令可能会改变;在某些时刻,密码可以由以下方式计算:对于给定的指令区间[l,r],对初始为空的队列依次执行第

c动态内存管理-爱代码爱编程

malloc calloc realloc free 原则:谁申请,谁释放,防止内存泄漏 #include<stdio.h> #include<stdlib.h> int mian() { i