IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++智能指针简介 -> 正文阅读

[C++知识库]C++智能指针简介

写在前面:

在很多的场景下我们都需要进行动态的申请内存,但是动态内存的使用很容易出现问题,因为确保在正确的时间释放内存是极其困难的。有时候我们会忘记了释放内存,这样的情况会产生内存泄漏的问题,这种问题也比较难以发现,因为现象不会立马出现,而多次释放一块内存就会很容易导致崩溃问题。
而C++为了更容易、更安全的使用动态内存,为我们提供了智能指针的概念,通过指针指针来管理对象,其行为比较像指针,主要负责自动释放所指向的对象。

智能指针头文件 <memory>

一、智能指针的原理

说到智能指针就不得不提一个叫 RALL(Resource Acquisition Initlalization) 直译过来就是资源获取初始化

它是 C++ 之父 Bjarne Stroustrup 提出的设计理念,其核心是把资源和对象的生命周期绑定对象创建获取资源,对象销毁释放资源。在 RAII 的指导下,C++ 把底层的资源管理问题提升到了对象生命周期管理的更高层次。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源

借此,我们实际上把管理一份资源的责任托管给了一个对象(听到没,把你托管给你的对象,然后你的对象一直管你管到你被析构了为止)。

这种做法有两大好处:
1.不需要显式地释放资源。
2.采用这种方式,对象所需的资源在其生命期内始终保持有效。

示例 :RALL 思想

1.1 动态申请的内存忘记释放

    for (int i = 0; i <= 10000000; ++i)  
    {  
        int *ptr = new int[3];  
        ptr[0] = 1;  
        ptr[1] = 2;  
        ptr[2] = 3;  
        //delete ptr;     //未释放资源
    } 

这样造成的后果就是内存泄露(危险动作切勿模仿)
我们都知道类的构造函数与析构函数在对象生成时自动调用其构造函数,在对象的生命周期结束时自动调用其析构函数,那么如果我使用一个这样的机制来管理我动态申请的对象,那么这样就再也不怕忘记释放资源了。
示例:

#include"SmartPtr.h"
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};

void Test1()
{
		int *ptr = new int[3];
		SmartPtr<int> sp(ptr);
		ptr[0] = 1;
		ptr[1] = 2;
		ptr[2] = 3;
		//delete ptr;     //未释放资源
}
int main()
{
	Test1();
	cout << "hahhah" << endl;
	system("pause");
	return 0;
}

在这里插入图片描述
这里可以看到,我们定义的sp与ptr地址是相同的,那么两个指针所指向的内容也是相同的,通过智能指针 sp 的管理实现资源的自动释放,当然手动释放了 ptr 指针也没有问题,因为两者生命周期一致,ptr 被释放了 sp 自然也被释放了,不存在多次非法释放内存问题。

这样的一个SmartPtr 类的确可以体现智能指针的思想,但是这里的 SmartPtr还不具有一个指针的行为,我们都知道指针是支持解引用或者 -> 的方式来访问所指向的空间的内容,因此再次基础之上再对 * 和 -> 进行重载即可,这便是智能指针的大概的实现原理的实现理念。

二、auto_ptr、unique_ptr 、scoped_ptr 、shared_ptr 、weak_ptr 简介

2.1 auto_ptr

auto_ptr 在C++ 98 版本的库中就提供了该智能指针
auto_ptr的实现原理:管理权转移的思想。
该指针的大致是实现思路为:

#include <memory>
class Date
{
public:
 Date() { cout << "Date()" << endl;}
 ~Date(){ cout << "~Date()" << endl;}
 int _year;
 int _month;
 int _day;
};
int main()
{
 auto_ptr<Date> ap(new Date);
 auto_ptr<Date> copy(ap);
 // auto_ptr的问题:当对象拷贝或者赋值后,前面的对象就悬空了
 // 所以auto_ptr 的缺陷非常严重、现在很多时候都不建议使用这种智能指针
 ap->_year = 2018;
 return 0;
}

在这里插入图片描述
在这里插入图片描述

这是因为auto_ptr 管理权转移的思想,auot_ptr 中的 赋值操作大致思路为:

 AutoPtr<T>& operator=(AutoPtr<T>& ap)
 {
 // 检测是否为自己给自己赋值
 	if(this != &ap)
 	{
 		// 释放当前对象中资源
 		if(_ptr)
 		delete _ptr;
 	
 		// 转移ap中资源到当前对象中
 		_ptr = ap._ptr;
 		ap._ptr = NULL;
 }
 	return *this;
 }

可以看到当我们再去访问已经被释放的指针指向的空间时就会出现崩溃的问题。

2.2 unique_ptr

因为前车之鉴,C++ 11 开始提供更靠谱的 unique_ptr
俗话说在哪里跌打就在哪里站起来,unique_ptr 直接非常的暴力 杜绝拷贝和赋值,可以说 unique_ptr 就是 auto_ptr 的 2.0 修复版本
在这里插入图片描述
具体的实现原理大概为:

1.将该接口只声明、不实现。并将其声明为私有 这是C98 版本的实现方法
2.C++ 11 是直接将其删除

2.3 scoped_ptr

scoped_ptr是一个类似于auto_ptr的智能指针,它包装了new操作符在堆上分配的动态对象,能够保证动态创建的对象在任何时候都可以被正确的删除。

scoped_ptr同时把拷贝构造函数和赋值操作都声明为私有的,禁止对智能指针的复制操作,保证了被它管理的指针不能被转让所有权。
所以scoped_ptr 和 unique_ptr 其实实现原理是一样的。

2.4 shared_ptr

上面的直接干掉了指针的赋值和拷贝的操作肯定是不满足一些场景需求的,因此C++ 提供了更靠谱且支持拷贝的 shared_ptr

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

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

大致实现思路:

void Release()
 {
 	bool deleteflag = false;
	 // 引用计数减1,如果减到0,则释放资源
	 _pMutex.lock();//保证线程安全
	 if (--(*_pRefCount) == 0)
	 {
		 delete _ptr;
		 delete _pRefCount;
 		deleteflag = true;
	 }
	 _pMutex.unlock();
 
 	if(deleteflag == true)
 	{
    	delete _pMutex;
    }
 }
private:
 		int* _pRefCount; // 引用计数
 		T* _ptr; // 指向管理资源的指针 
 		mutex* _pMutex; // 互斥锁

shared_ptr 通过引用计数的方法来确保资源的合理释放、同时还考虑到了线程安全的问题,使多个线程同时访问也可以保证线程安全。

但是shared_ptr 还是存在一个循环引用的问题
循环引用:

  1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  4. 也就是说_next析构了,node2就释放了。
  5. 也就是说_prev析构了,node1就释放了。
  6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2
    成员,所以这就叫循环引用,谁也不会释放。

就像这样:

int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	node1->_next = node2;
	node2->_prev = node1;
	//最后谁也不会释放 还是会造成内存泄露问题
	return 0;

这样的问题如何解决呢?
接下来就要引入 weak_ptr 的概念了

2.5 weak_ptr

weak_ptr 使一种不控制所指向对象生存生存期的智能指针,它所指向由一个shared_ptr 管理的对象。将一个weak_ptr 绑定到一个shared_ptr,但是其不会改变 shared_ptr 的引用计数,一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被销毁

即使有weak_ptr 指向对象,该对象还是会被释放。
因此该智能指针的创建需要一个shared_ptr 来初始化它。
示例:

auto p = make_shared<int>(0);
weak_ptr<int> wp(p);

而shared_ptr 循环引用的缺陷也可以被解决

struct ListNode
{
	 int _data;
	 weak_ptr<ListNode> _prev;
	 weak_ptr<ListNode> _next;
	 ~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
 	shared_ptr<ListNode> node1(new ListNode);
	 shared_ptr<ListNode> node2(new ListNode);
	 node1->_next = node2;
	 node2->_prev = node1;
	 return 0;
}
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-09-06 10:59:24  更:2021-09-06 10:59:42 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/27 21:22:34-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计