初识C++ · 智能指针

news2024/11/28 8:30:17

目录

前言:

1 智能指针的发展历史

2 unique_ptr和shared_ptr的基本使用

3 shared_ptr的模拟实现

4 有关定制删除器


前言:

智能指针的引入,我们得先从异常开始说起,异常面临的一个窘境是new了多个对象,抛异常了会导致先new的对象没有析构,从而导致内存泄漏的问题,解决方法是使用RAII

RAII是一种技术,英文的全称是:Resource Acquisition Is Initialization,是利用对象的声明周期来解决内存泄漏的一种技术。

就像这样:

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


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

private:
	T* _ptr;
};
double Division(int a, int b)
{
	if (b == 0)
	{
		throw string("除数为0");
	}
	else
	{
		return (double)a / (double)b;
	}
}
void Func()
{
	Smart_ptr<int> p1 = new int[1000];
	Smart_ptr<int> p2 = new int[1000];

	int a = 0, b = 0;
	cin >> a >> b;
	Division(a, b);

}
int main()
{
	try
	{
		Func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "捕获到未知异常" << endl;
	}
	return 0;
}

之前的忧虑来源于第二个抛异常之后前面的对象不会析构的问题,那么现在用一个智能指针对象来接受指针,不仅起到了管理空间的作用,还可以调用原本的行为,比如* -> ,这里还没有重载。

上文有些许未介绍的,比如exception,是C++异常体系里面的基类,所以捕捉到了异常我们就可以用exception来捕捉,打印是用的what函数,就和前面提到的示例代码是一样的。

现在就对智能指针有了一个简单的理解了,现在我们就深入了解一下。


1 智能指针的发展历史

提到智能指针,我们首先应该想到的是C++98里面的auto_ptr,早在C++98就引入了智能指针,但是使用确实让人一言难尽,它的本质是转移控制权,所以会导致一个悬空,像move左值一样,导致原来的自定义对象变为空了,这就不是将亡值的施舍了,是活生生的夺舍:

就像文档里面提到的,推荐使用unique_ptr而不是auto_ptr,因为是指针,所以需要重载函数使得该类有指针的对应行为,比如* -> ,这里我们先使用,所在的头文件是memory:

int main()
{
	auto_ptr<string> s1(new string("aaaa"));
	auto_ptr<string> s2(s1);
	return 0;
}

当我们通过监视窗口可以看到:

执行了拷贝构造之后,s1就悬空了,相应的,s2获得了控制权,但是,属实有点鸡肋。

比如之后还要访问s1,那就报错了,反正呢,很多公司是禁止使用auto_ptr的。

那么呢,在03年的时候,引入了一个库boost,这个库是由C++委员会的部分工作人员搞的,在C++11里面引入的很多新特性都是在boost里面借鉴的,其中就涉及到了智能指针,在boost里面有三种智能指针,分别是scoped_ptr,shared_ptr,weak_ptr,其中也有引入数组,但是C++11并没有采纳。

在C++11引入智能指针的时候就借鉴了boost的智能指针,但是有一个指针改了一个名,scoped_ptr改成了unique_str,为什么改呢,咱也不知道,学就完事儿了。

所以今天的重点就是unique_str和shared_ptr和weak_ptr,其中shared_ptr是人人都要会的。


2 unique_ptr和shared_ptr的基本使用

对于使用方面都是很简单,咱们先看一个文档:

模板参数有一个D = 什么什么的,这时定制删除器,本文后面会介绍,现在先不急。

基本的函数使用就是:

这就是所有的成员函数了,get函数是获取原生指针,release是显式的释放指针,但不是显式的析构这个类,同样的,既然是指针,就应该具备指针的行为,比如* ->等,有了stl的基本,这些我们应该是看一下文档就应该知道怎么使用的,这里再看看构造函数:

注意的是unique_ptr是不支持拷贝构造的,重载的第9个函数,拷贝函数被delete修饰了,所以不支持。其中支持auto_ptr 右值构造什么的,咱们先不管,主要了解前面几个就可以了。

class A
{
public:
	A(int a1 = 1, int a2 = 1)
		:_a1(a1)
		,_a2(a2)
	{}

	~A()
	{
		cout << "~A()" << endl;
	}
//private:
	int _a1 = 1;
	int _a2 = 1;
};

int main()
{
	unique_ptr<A> sp1(new A[10]);
	unique_ptr<A> sp2(sp1);
	A* p = sp1.get();
	cout << p << endl;

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

	return 0;
}

找找错误?

第一个 不能拷贝构造,第二析构会报错,因为开辟的是一个数组,基本类型是A,应该是A[],这就和定制删除器有关系了,所以这里的正确代码是:

class A
{
public:
	A(int a1 = 1, int a2 = 1)
		:_a1(a1)
		,_a2(a2)
	{}

	~A()
	{
		cout << "~A()" << endl;
	}
//private:
	int _a1 = 1;
	int _a2 = 1;
};

int main()
{
	unique_ptr<A[]> sp1(new A[10]);
	//unique_ptr<A> sp2(sp1);
	A* p = sp1.get();
	cout << p << endl;

	//sp1->_a1++;
	//sp1->_a2++;
	sp1[1]._a1++;
	sp1[1]._a2++;

	return 0;
}

这里的执行结果就是析构了十次:

为什么new了十个空间,基本类型也要是一个数组,这里的解决方案是定制删除器,先不管。

然后就是shared_ptr的基本使用:

相比之下shared_ptr就朴实无华很多了。

成员函数这么多,这里和pair有点像的是,有一个make_shared的东西,在构造的时候会用到:

因为它支持拷贝构造,欸~所以我们可以使用make_shared构造,产生临时对象拷贝一个就行。

简单介绍一下其中的成员函数,use_count是用来计算计数的,因为在智能指针要实现的一个事就是,管理资源更加简单,比如多个对象共同管理一块资源,新加了一个对象管理资源,引入计数就++,反之--,对比auto那里不能拷贝,就是因为如果拷贝了,造成了多次析构的问题,就会报错。

那么要验证计数很简单:

int main()
{
	//shared_ptr<int> sp1(new int);
	shared_ptr<int> sp1 = make_shared<int>(1);
	cout << sp1.use_count() << endl;
	{
		shared_ptr<int> sp2(sp1);
		cout << sp2.use_count() << endl;
	}
	cout << sp1.use_count() << endl;
	return 0;
}

既然是出了局部域就会销毁,那么我们创建一个局部域即可:

简单使用了解一下可以了,咱们这里简单模拟实现一下。


3 shared_ptr的模拟实现

目前对于shared_ptr我们的简单理解有了,那么现在简单捋一下,智能指针防止多次析构的方法是使用计数的方式,那么我们就需要一个计数了,问题是这个变量我们如何创建呢?

template<class T>
class shared_ptr
{
private:
	T* ptr;
	int count;
};

这样可行吗?显然是不可行的,因为我们创建一个对象,指向空间,就会让这个类多次实例化,每个对象都有一个count,实例化一次count++,然后呢就导致每个对象都有count,count的都是一样的,我们指向一个空间,创建了n个对象,预期是计数为n,但是创建的对象是独立的,析构只会析构某个对象的count,从而count的大小是n * n,所以结果是不可预估的。

那么我们应该用static吗?每创建一个对象就++一次,看起来好像可以,但是我们如果指向的空间不是一个呢?new了两个空间,就会导致两个空间公用一个计数,更不行了。

所以解决办法是创建一个指针,每创建一个对象,指针指向的空间,即计数空间就++:

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

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


	int use_count()
	{
		return *_pcount;
	}

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

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

private:
	T* _ptr;
	int* _pcount;
};

这是一般的函数,重点在于赋值重载 和 定制删除器。

对于赋值重载来说,指向的空间修正,指向的计数空间修正,那么原来指向的空间是否需要修正呢?这就要看计数是否到0了,所以需要判断是否到0,到了就析构:

那么析构函数,简单的,new了空间删除就完事了:

void release()
{
	if (--(*_pcount) == 0)
	{
		delete _ptr;
		delete _pcount;
	}
}
~shared_ptr()
{
	release();
}

赋值重载判断+ 改定指向:

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
	if (--(sp._pcount) == 0)
	{
		release();
		_ptr = sp._ptr;
		_pcount = sp._pcount;
		(*_pcount)++;

	}
	return *this;
}

但是,万一有人突发奇想,想自己给自己赋值呢?再万一,有人在指向空间是同一块的基础上相互赋值呢?

所以我们不妨判断一下指向空间的地址是不是一样的:

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
	if (_ptr != sp._ptr)
	{
		release();
		_ptr = sp._ptr;
		_pcount = sp._pcount;
		(*_pcount)++;
	}
	return *this;
}

shared_ptr的基本实现就完成了。

现在引入一个线程安全的问题:
 

mutex mtx;
void Func(Ptr::shared_ptr<list<int>> sp, int n)
{
	cout << sp.use_count() << endl;
	
	for (int i = 0; i < n; i++)
	{
		Ptr::shared_ptr<list<int>> copy1(sp);
		Ptr::shared_ptr<list<int>> copy2(sp);
		Ptr::shared_ptr<list<int>> copy3(sp);

		mtx.lock();
		sp->push_back(i);	
		mtx.unlock();

	}
}

int main()
{
	Ptr::shared_ptr<list<int>> sp1(new list<int>);
	cout << sp1.use_count() << endl;

	thread t1(Func, sp1, 100);
	thread t2(Func, sp1, 200);

	t1.join();
	t2.join();

	cout << sp1->size() << endl;
	cout << sp1.use_count() << endl;

	return 0;
}

解决方法是引入atomic,原子操作:

shared_ptr(T* ptr)
	:_ptr(ptr)
	, _pcount(new std::atomic<int>(1))
{}

private:
	T* _ptr;
	std::atomic<int>* _pcount;

还有一个问题就是,如果是交叉指向,就会导致无法析构:

struct Node
{
	//std::shared_ptr<Node> _next;
	//std::shared_ptr<Node> _prev;

	std::weak_ptr<Node> _next;
	std::weak_ptr<Node> _prev;
	int _val;

	~Node()
	{
		cout << "~Node()" << endl;
	}
};
int main()
{
	std::shared_ptr<Node> p1(new Node);
	std::shared_ptr<Node> p2(new Node);

	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;

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

	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;

	cout << p1->_next.use_count() << endl;
	cout << p2->_prev.use_count() << endl;

	return 0;
}

 此时就需要弱指针,weak_ptr,只是指向空间,但是计数不++即可。


4 有关定制删除器

为什么会引入定制删除器呢?

int main()
{
	Ptr::shared_ptr<int> sp1((int*)malloc(sizeof(4)));
	Ptr::shared_ptr<FILE> sp2(fopen("test.txt", "w"));

	return 0;
}

在内存管理章节提及,内存管理的函数不能混合使用,何况是FILE呢,FILE*要使用fclose,所以我们应该定制一个删除器,但是如何传过去呢?传只能传到构造,传不到析构,所以我们不妨:

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

private:
	T* _ptr;
	std::atomic<int>* _pcount;
	std::function<void(T*)> _del = [](T* ptr) {delete ptr; };

要删除的时候,传对象就可以了,比如:

template<class T>
struct FreeFunc {
	void operator()(T* ptr)
	{
		cout << "free:" << ptr << endl;
		free(ptr);
	}
};
class A
{
public:
	A(int a1 = 1, int a2 = 1)
		:_a1(a1)
		,_a2(a2)
	{}

	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1 = 1;
	int _a2 = 1;
};


int main()
{
	Ptr::shared_ptr<A> sp1(new A[10], [](A* ptr) {delete[] ptr; });
	Ptr::shared_ptr<int> sp2((int*)malloc(4), FreeFunc<int>());
	Ptr::shared_ptr<FILE> sp3(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr); });
	Ptr::shared_ptr<A> sp4(new A);

	return 0;
}

智能指针就介绍到这里。


感谢阅读!

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

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

相关文章

centos7简介和安装步骤

目录 centos简介 1.CentOS 7 简略简介 2.使用建议 vmware搭建centos7 1.centos镜像下载 2.虚拟机创建 2.1 新建虚拟机 2.2典型 2.3操作系统 2.4客户机操作系统选择 ​编辑 2.5虚拟机命名与位置安装 2.5磁盘容量 2.6完成 2.6编辑虚拟机 2.6.1内存大小编辑 2.6.…

React+vite+antd点击路由切换时出现闪屏

问题描述:点击左侧路由切换会出现闪屏的情况 页面的效果 解决方案 完整代码 import { lazy, Suspense, ReactNode } from "react"; import { Navigate, useLocation } from "react-router-dom"; import LayOut from ../pages/LayOut/LayOut import NotFo…

过滤了字母、数字、_、$的webshell命令执行技巧

目录 对于php5以上首先要解决的问题有 解决技巧 1.code长度小于35位 2.没有字母、数字、_ 、$ 3.怎么把文件放进服务器 4.怎么执行文件里面的内容 1.执行Linux命令 2.执行文件里面的shell命令 5.构造完整的code参数 6.我们还可以通过修改文件里面shell命令&#xff0c;…

学习记录703@计算机组成原理之原码、补码、反码、移码

数据表示基本概念 数据表示分为有符号数和无符号数&#xff0c;其中有符号数分为整数、纯小数、带小数&#xff1b;整数和纯小数小数点位置固定&#xff0c;统称为定点数&#xff1b;带小数小数点位置可以浮动&#xff0c;称为为浮点数&#xff0c;浮点数阶码就是整数部分&…

忘记iPhone锁屏密码,多次输出密码导致iPhone停用了怎么解锁?

iphone已停用怎么解锁&#xff1f;当因忘记iPhone锁屏密码&#xff0c;多次输入错误密码而被停用时&#xff0c;怎么解锁恢复对设备的访问是非常重要的。下面小编将给大家介绍几种解锁已停用iPhone的方法&#xff0c;一起来看看吧&#xff01; 一、使用恢复模式解锁 将iPhone连…

知识学习技巧:如何从 iPhone 恢复误操作删除的视频

您来这里主要是因为您想知道如何从 iPhone 恢复已删除的视频。实际上&#xff0c;如果视频是用 iPhone 拍摄的&#xff0c;您可以尝试在相册“最近删除”中找到它们。删除后它将保留 40 天。如果您清空了相册或无法从相册中找到已删除的视频&#xff0c;那么您必须尝试使用奇客…

系统管理篇

系统管理 一、网络管理1. 网络状态查看2. 网络和路由配置3. 网络故障排除4. 网络服务管理 二、软件管理1. 软件包管理器2. rpm包和rpm命令3. yum仓库4. 源代码编译安装5. 内核升级6. grub配置文件 三、进程管理1. 进程的概念与进程查看2. 进程的控制命令3. 进程的通信方式--信号…

Linux内核中的eMMC存储支持详解

一、eMMC概述 eMMC&#xff08;Embedded MultiMediaCard&#xff09;是一种嵌入式多媒体卡&#xff0c;是一种基于NAND Flash的闪存卡标准&#xff0c;由JEDEC&#xff08;电子设备工程联合委员会&#xff09;订立和发布。eMMC集成了NAND Flash、闪存控制器和eMMC协议接口&…

【npm】如何将开发的vite插件发布到npm

前言 简单说下 npm 是什么&#xff1a; npm 是一个 node 模块管理工具&#xff0c;也是全球最大的共享源。 npm 工具与 nodejs 配套发布&#xff0c;便利开发人员共享代码。npm 主要包括 npm 官方网站、CLI&#xff08;控制台命令行工具&#xff09;、和 registry&#xff08;…

系统架构设计定义

系统架构设计是软件开发和信息系统构建中至关重要的一环&#xff0c;它涉及到系统的整体结构、模块划分、接口定义以及设计原则等多个方面。以下是对系统架构设计的详细阐述&#xff1a; 一、系统架构设计的定义 系统架构设计是指对系统的整体结构、模块关系、接口定义以及交互…

IDEA2023版本创建mavenWeb项目及maven的相关配置

在使用idea创建maven项目之前&#xff0c;首先要确保本地已经下载并配置好maven的环境变量&#xff0c;可以参考我主页的maven下载及环境变量配置篇。 接下来首先介绍我们需要对maven安装目录文件进行的修改介绍。 maven功能配置 我们需要需改 maven/conf/settings.xml 配置…

数字孪生水利“天空地水工”一体化监测感知行动方案的框架及目标

监测感知数据是构建数字孪生水利体系的前提和基础。&#xff0c;推进物理流域监测系统科学建设和高频乃至在线运行&#xff0c;提升水利对象全要素和治理管理全过程智能监测感知能力&#xff0c;为数字孪生水利的高保真建设运行提供有力算据保障&#xff0c;支撑构建具有“四预…

博客系统测试用例报告

一、项目背景 ⼀款基于SpringBoot实现的简易⽹站&#xff0c;功能有⽤⼾注册及登录&#xff0c;博客列表展⽰&#xff0c;博客详情查看&#xff0c;博客编写和⽂章评论 点赞功能采⽤的技术有&#xff1a;HTML、CSS、Mybatis、Token、Ajax等技术。 二、项目功能 1.注册 进入…

仓颉编程入门 -- 泛型概述 , 如何定义泛型函数

泛型概述 , 如何定义泛型函数 1 . 泛型的定义 在仓颉编程语言中&#xff0c;泛型指的是参数化类型&#xff0c;参数化类型是一个在声明时未知并且需要在使用时指定的类型。类型声明与函数声明可以是泛型的。最为常见的例子就是 Array、Set 等容器类型。以数组类型为例&#xf…

inscode的会员计划的python环境问题【版本3.9.16】无法升级python

购买了inscode的会员计划后,部署python项目 遇到python环境无法升级的问题 inscode的会员计划的环境是3.9.16,但是项目用的例子需要3.10以上的版本,最终本人也无法完全解决,虽然手动安装了python3.10,一切都可以实现,但是最后环境自动恢复到3.9版本,导致自己手动配置的全…

速成cesium而忽略前端基础,你可能会遇到这些坑!

cesium作为一个功能强大的开源三维地球框架&#xff0c;做出来的效果受到众多企业的青睐。然而&#xff0c;许多小伙伴们都 希望能够快速掌握cesium&#xff0c;从而忽略了前端基础知识的重要性。这种急功近利的学习方式往往会导致一些不良后果&#xff1a; 1. 前端基础太薄弱…

【leetcode】找出与数组相加的整数II( 过程反思 思路详解 )

题目分析&#xff1a; 题目要咱们除掉nums1中的两个数&#xff0c;从另一个角度理解就是找到nums1中与nums2元素匹配的nums1.size() - 2个数 由题目可知&#xff0c;二者元素具有相差 x 的一一对应关系&#xff0c;如此&#xff0c;不难想到应首先通过对二者排序来构建大致的匹…

用Python制作开心消消乐游戏|附源码

制作一个完整的“开心消消乐”风格的游戏在Python中是一个相对复杂的项目&#xff0c;因为它涉及到图形界面、游戏逻辑、动画效果以及用户交互等多个方面。不过&#xff0c;我可以为你提供一个简化的版本和概念框架&#xff0c;帮助你理解如何开始这个项目&#xff0c;并提供一…

仪表板展示|DataEase看中国:2024巴黎奥运会中国体育代表团战绩报告

背景介绍 北京时间2024年8月12日凌晨&#xff0c;巴黎奥运会闭幕。在本届奥运会中&#xff0c;我们不仅见证了许多新世界纪录的诞生&#xff0c;更看到了中国体育的强大实力与无限潜力。中国运动健儿们卓越的表现和顽强的拼搏精神&#xff0c;不但让国人为之自豪&#xff0c;也…

普元EOS学习笔记-高低开结合方式实现图书的增删改查

1 前言 在《普元EOS学习笔记-低开实现图书的增删改查》的文章中&#xff0c;我描述了在精简应用中&#xff0c;基于低开实现图书分类的增删改查的操作&#xff0c;是为了简单展示EOS低开的能力。 低开天生会有一些问题&#xff0c;这个咱就不啰嗦了。而且&#xff0c;那篇文章…