C++:智能指针(RAII思想)

news2024/11/19 8:31:18

目录

1、什么是智能指针?

2、为什么需要智能指针

3、RAII思想及智能指针的原理

4、智能指针的发展

4.1 auto_ptr

4.2 unique_ptr

4.3 share_ptr

5、share_ptr的模拟实现

6、循环引用问题

7、share_ptr中的自定义删除器


1、什么是智能指针?

        智能指针是一种用于管理动态分配的内存的 C++ 类。它们提供了对堆内存的自动分配和释放,以防止内存泄漏和悬挂指针的情况。常见的智能指针包括 std::unique_ptr、std::shared_ptr 和 std::weak_ptr。它们提供不同的所有权和生命周期管理模型,以满足不同的需求。

std::unique_ptr: 代表独占所有权的智能指针。它允许一个对象拥有对动态分配的内存的唯一所有权,当其超出作用域时,会自动释放所管理的内存。它不能被复制或赋值,因为复制会导致所有权转移,从而破坏唯一性。

std::shared_ptr: 代表共享所有权的智能指针。多个 shared_ptr 对象可以共享同一块动态分配的内存。内部维护一个引用计数,当所有 shared_ptr 对象都释放了对内存的引用时,才会释放内存。这种指针适用于需要多个所有者的情况。

std::weak_ptr: 代表弱引用的智能指针。它不会增加引用计数,也不会影响内存的释放。通常与 shared_ptr 搭配使用,用于避免循环引用导致的内存泄漏问题。可以通过 lock() 方法获取一个 shared_ptr,如果所指向的对象还存在的话。

2、为什么需要智能指针

我们来看一下这段代码:

#include<iostream>
using namespace std;
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	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;
}

1、在 Func() 函数中,如果 new int 抛出异常,导致内存分配失败,那么 p1 指针会成为悬空指针,没有办法释放动态分配的内存,因为没有对应的 delete 调用。这会导致内存泄漏。

2、同样地,如果 p2new int 抛出异常,会导致 p2 成为悬空指针,同样会造成内存泄漏。

3、如果在 div() 函数中,当用户输入的 b 为 0 时,会抛出一个 invalid_argument 异常。而在 Func() 中调用 div(),如果这个异常被抛出,那么 new int 分配的内存将无法被释放,同样会导致内存泄漏。

智能指针可以很好地解决这些问题。通过使用智能指针,我们可以确保在发生异常或函数退出时,动态分配的内存会得到正确的释放,从而避免内存泄漏。因为智能指针的析构函数会在对象超出作用域时自动调用 delete 操作,确保资源的正确释放。因此,将 p1p2 替换为 std::unique_ptr<int>std::shared_ptr<int> 将会更安全和可靠。

3、RAII思想及智能指针的原理

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。

这样做有两大好处:

无需显式释放资源 通过 RAII,资源的释放被嵌入到对象的析构函数中,因此不需要在代码中显式地释放资源。这样可以避免忘记释放资源而导致的内存泄漏或资源泄漏问题。

资源的自动管理 对象的生命周期由语言的自动对象生存期规则控制,因此当对象超出作用域时,资源会自动释放,从而确保资源在不再需要时被正确释放,避免资源泄漏和错误使用资源的问题。

智能指针设计:

#include<iostream>
using namespace std;
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{

	}
	~SmartPtr()
	{
		if (_ptr)
		{
			delete _ptr;
		}
	}
private:
	T* _ptr;
};

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);
	cout << div() << endl;
}

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

这段代码定义了一个简单的模板类 SmartPtr,用于管理动态分配的内存。然后,在 Func 函数中创建了两个 SmartPtr<int> 对象,分别管理两个动态分配的整型变量,当对象超出作用域资源就会自动释放,然后调用了 div 函数进行除法运算。如果除数为零,则抛出 invalid_argument 异常。

仅仅释放资源还不能称为智能指针,所以需要将* 、->重载下,才可让其像指针一样去使用。

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;
};

4、智能指针的发展

4.1 auto_ptr

// std::auto_ptr<int> sp1(new int);
// std::auto_ptr<int> sp2(sp1); // 管理权转移     这个时候sp1已经失效了

auto_ptr 采用了独占所有权的模型,意味着当一个 auto_ptr 被赋值给另一个 auto_ptr 后,原始 auto_ptr 将失去对资源的所有权。这导致在代码中进行所有权转移时很容易出现问题,例如可能会导致资源多次释放或悬空指针。auto_ptr 存在诸多弊端,因此不建议在现代 C++ 中使用它。

自 C++ 11 起,此类模板已弃用。unique_ptr 是一个具有类似功能的新工具,但具有更高的安全性

4.2 unique_ptr

 既然auto_ptr的拷贝存在问题,那么unique_ptr直接禁用了赋值拷贝。

我们先来看一下

  1. 移动赋值运算符 (1): 接受一个右值引用参数,用于将资源所有权从一个 unique_ptr 转移到另一个。这种赋值运算符用于实现资源的移动语义,通常在转移所有权时使用。

  2. 赋值为 null 指针 (2): 接受一个 nullptr_t 参数,用于将 unique_ptr 赋值为 null 指针,即释放当前持有的资源,并将 unique_ptr 设置为 null。

  3. 类型转换赋值运算符 (3): 接受一个右值引用参数,用于将资源所有权从一个 unique_ptr 转移到另一个,但允许指定不同的模板参数类型和删除器类型。这种赋值运算符用于允许更灵活的类型转换和删除器设置。

  4. 拷贝赋值运算符 (4): 这个赋值运算符是被删除的,意味着不能直接对 unique_ptr 进行拷贝赋值操作。这是因为 unique_ptr 是独占所有权的智能指针,不允许多个指针共享同一个资源,因此拷贝赋值在语义上是不合适的,所以被明确删除。

unique_ptr不允许多个指针共享一个资源,虽然还有使用场景,但明显场景受限制。这个时候有了更加优化的share_ptr.

4.3 share_ptr

share_ptr允许多个指针共享一个资源,采用引用计数的思想,只有当计数为一时才会析构,避免了同一块空间多次释放的问题。由于share_ptr比较完善,所以实际应用场景也会更多一些。

5、share_ptr的模拟实现

shared_ptr 的基本思想:共享同一个指针,但维护一个引用计数来追踪有多少个 shared_ptr 指向相同的资源。每次智能指针被复制时,引用计数增加;每次智能指针被销毁时,引用计数减少。如果引用计数减少到 0,智能指针会释放所持有的资源。

template <class T>
class share_ptr
{
public:
	share_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		,_count(new int(1))
	{

	}
	share_ptr(const share_ptr<T>& sp)
	{
		_ptr = sp._ptr;
		_count = sp._count;

		//拷贝时引用计数++
		++(*_count);
	}
	share_ptr<T>& operator=(const share_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			release();//释放可能存在的资源
			_ptr = sp._ptr;
			_count = sp._count;

			//拷贝时引用计数++
			++(*_count);
		}
		return *this;
	}

	void release()
	{
		if (--(*_count) == 0)
		{
			delete _ptr;
			delete _count;
		}
	}
	~share_ptr()
	{
		release();
	}

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

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

	T* _ptr;
	int* _count;

};

int main()
{
	share_ptr<int> sp;


	return 0;
}

share_ptr还需要注意循环引用问题

6、循环引用问题

循环引用是指两个或多个对象之间相互引用,导致它们的引用计数永远无法归零,从而导致内存泄漏。这在使用智能指针时是一个常见的问题,特别是在使用 shared_ptr 时更容易出现。

#include <memory>

class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
};

class B {
public:
    std::shared_ptr<A> a_ptr;
};

int main() {
    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;

    return 0;
}

A 拥有一个指向类 B 对象的 shared_ptr,而类 B 拥有一个指向类 A 对象的 shared_ptr。当 ab 超出作用域时,它们的引用计数永远不会归零,因为彼此都持有对方的指针,而只有指针释放时所指向的对象才会释放,但是指针是对象的成员,所以循环引用谁也不会释放,从而导致内存泄漏。

解决方案:

#include <memory>

class B;

class A {
public:
    std::weak_ptr<B> b_weak_ptr;  // 将其中一个指针设计为 weak_ptr
};

class B {
public:
    std::shared_ptr<A> a_ptr;
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();

    a->b_weak_ptr = b;  // 使用 weak_ptr

    b->a_ptr = a;

    // 在使用前检查 weak_ptr
    if (auto shared_b = a->b_weak_ptr.lock()) {
        // 如果对象仍然存在,则可以安全地使用 shared_b
        std::cout << "Object B is still alive." << std::endl;
    } else {
        std::cout << "Object B is expired or deleted." << std::endl;
    }

    return 0;
}

使用weak_ptr打破循环引用:将其中一个指针设计为 weak_ptr,这样它不会增加对象的引用计数。但是需要在使用前对 weak_ptr 进行检查,以确保对象仍然存在。

7、share_ptr中的自定义删除器

如何根据不同的对象来执行特定的清理操作。

shared_ptr 允许指定一个自定义的删除器(deleter),并且提供了对应的构造函数

#include <iostream>
#include <memory>

// 自定义资源
struct MyResource {
    MyResource() { std::cout << "资源已分配。" << std::endl; }
    ~MyResource() { std::cout << "资源已释放。" << std::endl; }
    void CustomCleanup() { std::cout << "自定义清理操作。" << std::endl; }
};

// 删除器函数对象
struct CustomDeleter {
    void operator()(MyResource* ptr) const {
        if (ptr) {
            ptr->CustomCleanup();
            delete ptr;
        }
    }
};

int main() {
    // 创建 shared_ptr,并指定删除器
    std::shared_ptr<MyResource> ptr(new MyResource(), CustomDeleter());

    // 使用 shared_ptr
    // ...

    // 当 shared_ptr 超出作用域时,将会调用删除器来释放资源
    return 0;
}

MyResource 是我们要管理的自定义资源,CustomDeleter 是我们定义的删除器函数对象。在创建 shared_ptr 时,我们将资源指针和删除器一起传递给 shared_ptr 的构造函数。shared_ptr 超出作用域时,删除器将被调用来执行特定的清理操作,例如执行自定义的清理函数 CustomCleanup() 并释放资源。

我们可以根据不同的场景使用不同的删除器

std::shared_ptr<A> sp(new A[10], [](A* p){delete[] p; });

这里创建了一个 shared_ptr,它指向一个包含 10 个 A 对象的动态数组。在这里,我们传递了一个 lambda 函数作为删除器,这个 lambda 函数负责释放数组内存,使用 delete[] 来释放动态数组的内存。

std::shared_ptr<FILE> sp(fopen("test.txt", "w"), [](FILE* p){fclose(p); });

 这里创建了一个 shared_ptr,它指向通过 fopen 打开的文件指针。同样地,我们传递了一个 lambda 函数作为删除器,这个 lambda 函数使用 fclose 来关闭文件。

这种方式允许我们在 shared_ptr 不再需要资源时执行特定的清理操作,确保资源被正确释放,避免内存泄漏或资源泄漏。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1642264.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java spring 09 Bean的销毁过程

1.Bean销毁是发送在Spring容器关闭过程中的 AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(AppConfig.class);UserService userService (UserService) context.getBean("userService");userService.test();// 容器关闭cont…

LeetCode题练习与总结:最大矩形--85

一、题目描述 给定一个仅包含 0 和 1 、大小为 rows x cols 的二维二进制矩阵&#xff0c;找出只包含 1 的最大矩形&#xff0c;并返回其面积。 示例 1&#xff1a; 输入&#xff1a;matrix [["1","0","1","0","0"],[&quo…

C++入门系列-类对象模型this指针

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” 类对象模型 如何计算类对象的大小 class A { public:void printA(){cout << _a << endl;} private:char _a; }; 算算看&#xff0c;这个类的大小是多少 我们知道…

Typora编辑markdown的技巧

参考视频的B站链接&#xff1a; 手把手教你撰写Typora笔记 在其中选择了常用的部分做标记。 一、标题 使用ctrl数字键&#xff0c;可以快捷的把一行文字变成n级标题 二、源代码模式 可以在下图所示进入 三、设置typora能够自动显示粘贴的图片 打开“偏好设置”&#xff0…

Android使用kts发布aar到JitPack仓库

Android使用kts发布aar到JitPack 之前做过sdk开发&#xff0c;需要将仓库上传到maven、JitPack或JCenter,但是JCenter已停止维护&#xff0c;本文是讲解上传到JitPack的方式,使用KTS语法&#xff0c;记录使用过程中遇到的一些坑.相信Groovy的方式是大家经常使用的&#xff0c;…

力扣每日一题106:从中序与后序遍历序列构造二叉树

题目 中等 相关标签 相关企业 给定两个整数数组 inorder 和 postorder &#xff0c;其中 inorder 是二叉树的中序遍历&#xff0c; postorder 是同一棵树的后序遍历&#xff0c;请你构造并返回这颗 二叉树 。 示例 1: 输入&#xff1a;inorder [9,3,15,20,7], postorder …

R语言学习—6—多元相关与回归分析

1、引子 xc(171,175,159,155,152,158,154,164,168,166,159,164) #身高 yc(57,64,41,38,35,44,41,51,57,49,47,46) #体重 par(marc(5,4,2,1)) #设定图距离画布边缘的距离&#xff1a;下5&#xff0c;左4&#xff0c;上2&#xff0c;右1 plot(x,y) 2、相关…

Web API之DOM

DOM 一.认识DOM二.获取元素三.事件基础四.操作元素(1).改变元素内容(2).修改元素属性(str、herf、id、alt、title&#xff09;(3).修改表单属性(4).修改样式属性操作(5).小结 五.一些思想(1).排他思想(2).自定义属性的操作 六.节点操作1.认识2.节点层级关系3.创建和添加、删除、…

PR2019软件下载教程

打开下载网址&#xff1a;rjctx.com 选择Premiere&#xff1a; 选择PR2019&#xff0c;并点击&#xff1a; 拉到最后&#xff0c;选择百度网盘下载&#xff1a; 下载到本地。 二&#xff0c;软件安装 解压缩后&#xff0c;双击set_up 选择位置后&#xff0c;进行安装&…

直播素材安卓情侣飞行棋v2.22 仿dofm 支持自定义模式—可用直播素材

一个情侣间增进友谊的小游戏非常好玩&#xff0c;适合男孩女孩之间增进感情&#xff01;快和你暗恋的女孩一块玩吧&#xff0c;极速升温 永久免费&#xff01;解锁激活码内容全部畅玩&#xff01;全网最强超级给力&#xff01;真人说书音频 网盘自动获取 链接&#xff1a;http…

Monorepo(单体仓库)与MultiRepo(多仓库): Monorepo 单体仓库开发策略与实践指南

&#x1f31f; 引言 在软件开发的浩瀚宇宙里&#xff0c;选择合适的代码管理方式是构建高效开发环境的关键一步。今天&#xff0c;我们将深入探讨两大策略——Monorepo&#xff08;单体仓库&#xff09;与MultiRepo&#xff08;多仓库&#xff09;&#xff0c;并通过使用现代化…

ThreeJS:两种场景雾

雾 ThreeJS提供了Fog类&#xff0c;用于创建线性雾的效果&#xff1b;提供了FogExp2类&#xff0c;用于实现指数雾的效果。 雾效果常用于模拟真实世界中视觉深度递减的效果&#xff0c;也可以用于创建某些艺术效果。即&#xff1a;当物体距离观察者越远&#xff0c;雾就越密&am…

Windows系统下安装Mosquitto的步骤(6)

接前一篇文章&#xff1a;Windows系统下安装Mosquitto的步骤&#xff08;5&#xff09; 本文内容参考&#xff1a; Windows下搭建MQTT服务器_mqtt服务器软件-CSDN博客 特此致谢&#xff01; 在前一篇文章中&#xff0c;笔者通过MQTTX实现了通过图形界面环境收发MQTT消息。但是…

SPARC VScode EIDE GDB 使用配置

前言 搞了多年的SPARC 最近接触了VSCODE插件感觉好用。想想看不是能方便调试和编译SPARC&#xff0c;决定使用开源的SPARC仿真环境和编译器来试试。感觉的却不错&#xff0c;借此献给使用SPARC的朋友们。安装 1.找微软官方的下载VSCODE. 2.电机左边的方块形状的图标&#xff0…

由于找不到msvcr110.dll,无法继续执行代码的解决方法

在日常使用计算机的过程中&#xff0c;可能会遇到系统提示缺少msvcr110.dll文件的情况&#xff0c;这一问题往往导致某些应用程序无法正常运行。幸运的是&#xff0c;有多种方法可以有效应对这一困境&#xff0c;帮助您的计算机恢复顺畅运作。以下是解决计算机丢失msvcr110.dll…

【游戏行业】2024年电子游戏分类,国内游戏产业报告,发展趋势

文章目录 一、电子游戏分类1、传统游戏分类2、混合手游分类3、二次元、开放设计、调查问卷 二、游戏产业报告1、游戏产业数据2、游戏公司名单&#xff08;独角兽&#xff09;3、营收与利润&#xff08;对比互联网、国企&#xff09; 三、发展趋势1、游戏行业上下游2、游戏行业趋…

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-12-蜂鸣器

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…

PX4二次开发快速入门(三):自定义串口驱动

文章目录 前言 前言 软件&#xff1a;PX4 1.14.0稳定版 硬件&#xff1a;纳雷NRA12&#xff0c;pixhawk4 仿照原生固件tfmini的驱动进行编写 源码地址&#xff1a; https://gitee.com/Mbot_admin/px4-1.14.0-csdn 修改 src/drivers/distance_sensor/CMakeLists.txt 添加 add…

01-MySQL 基础篇笔记

一、MySQL 概述 1.1 数据库相关概念 数据库&#xff1a;&#xff08;DB&#xff1a;DataBase&#xff09; 存储数据的仓库&#xff0c;数据是有组织的进行存储 数据库管理系统&#xff1a;&#xff08;DBMS&#xff1a;DataBase Management System&#xff09; 操作和管理数…

IoTDB 入门教程 基础篇⑨——TsFile导入导出工具

文章目录 一、前文二、准备2.1 准备导出服务器2.2 准备导入服务器 三、导出3.1 导出命令3.2 执行命令3.3 tsfile文件 四、导入4.1 上传tsfile文件4.2 导入命令4.3 执行命令 五、查询六、参考 一、前文 IoTDB入门教程——导读 数据库备份与迁移是数据库运维中的核心任务&#xf…