首页 > 编程语言 >【C++进阶学习】第十三弹——C++智能指针的深入解析

【C++进阶学习】第十三弹——C++智能指针的深入解析

时间:2024-08-17 19:53:35浏览次数:12  
标签:std 进阶 C++ 第十三 智能 内存 shared ptr 指针

前言:

在C++编程中,内存管理是至关重要的一个环节。传统的手动内存管理方式容易导致内存泄漏、悬挂指针等问题。为了解决这些问题,C++引入了智能指针。本文将详细讲解C++中智能指针的概念、种类、使用方法以及注意事项。

目录

一、引言

二、智能指针的原理及目的

2.1 智能指针的原理

2.1.1 RAII

2.2 智能指针的目的

三、智能指针的种类

3.1 std::auto_ptr

3.2 std::unique_ptr

3.3 std::shared_ptr

3.4 std::weak_ptr

四、智能指针的使用方法

五、注意事项

六、总结


一、引言

在正式讲解智能指针之前,我们先来了解一下为什么会诞生智能指针:

在C++中,指针是用于访问内存地址的一种特殊变量。传统的指针管理需要程序员手动分配和释放内存,这容易导致以下问题:

  1. 内存泄漏:当程序员忘记释放内存时,会导致内存泄漏,最终耗尽系统资源。
  2. 悬挂指针:当指针指向的内存被释放后,如果指针没有被设置为NULL,那么它就变成了悬挂指针,访问悬挂指针可能会导致未定义行为。
  3. 双重释放:当指针被错误地释放两次时,会引发程序崩溃。

为了解决这些问题,C++引入了智能指针,它是一种特殊的对象,能够自动管理指针指向的内存。

下面是一个内存泄漏的例子:

void MemoryLeaks()
{
	// 1.内存申请了忘记释放
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = new int;

	// 2.异常安全问题
	int* p3 = new int[10];
	Func(); // 这里Func函数如果抛异常就会导致 delete[] p3未执行,p3没被释放.
	delete[]p3;
}

二、智能指针的原理及目的

了解使用智能指针之前,我们要先来了解RAII

2.1 智能指针的原理

2.1.1 RAII

RAII是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在
对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
    · 不需要显式地释放资源。
    · 采用这种方式,对象所需的资源在其生命期内始终保持有效

智能指针是一种能够自动管理指针指向内存的类模板。它通过重载解引用运算符(*)和箭头运算符(->)来模拟指针的行为,同时内部使用某种机制(RAII的原理)来自动释放内存。

总结一下智能指针的原理: 
1. RAII特性 
2. 重载operator*和opertaor->,具有像指针一样的行为。

2.2 智能指针的目的

智能指针的主要目的是:

1、自动释放内存:当智能指针超出作用域或被销毁时,它会自动释放所管理的内存。
2、防止内存泄漏和悬挂指针:智能指针确保内存被正确释放,从而避免内存泄漏和悬挂指针。

三、智能指针的种类

C++标准库提供了三种主要的智能指针:

  1. std::unique_ptr:独占智能指针,表示指针指向的内存只能由一个智能指针拥有。
  2. std::shared_ptr:共享智能指针,表示多个智能指针可以共享同一块内存。
  3. std::weak_ptr:弱指针,用于解决共享指针可能导致的循环引用问题。

在标准库出来之前,还有一个auto_ptr,下面我们会对这几个进行逐一讲解

3.1 std::auto_ptr

auto_ptr是在C++98版本中就给出的,它的实现原理是:管理权转移,只有一个对象能够管理资源

下面是auto_ptr的简单实现:

template<class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr)
		:_ptr(ptr)
	{}
	auto_ptr(auto_ptr<T>& sp)
		:_ptr(sp._ptr)
	{
		// 管理权转移
		sp._ptr = nullptr;
	}
	auto_ptr<T>& operator=(auto_ptr<T>& ap)
	{
		// 检测是否为自己给自己赋值(这一步很重要,不然容易发生双重释放)
		if (this != &ap)
		{
			// 释放当前对象中资源
			if (_ptr)
				delete _ptr;
			// 转移ap中资源到当前对象中
			_ptr = ap._ptr;
			ap._ptr = NULL;
		}
		return *this;
	}
	~auto_ptr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}
	// 像指针一样使用
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

3.2 std::unique_ptr

std::unique_ptr是独占智能指针,它确保了指针指向的内存只能由一个智能指针拥有。当std::unique_ptr被销毁或赋值给另一个std::unique_ptr时,它所指向的内存会被自动释放。

下面我们来看一下库中它的声明方式:

#include <memory>

int main() {
    std::unique_ptr<int> ptr(new int(10));
    // 使用ptr
    // ...
    return 0;
}

我们来简单的模拟一下它的实现(简单粗暴,防止拷贝,这样就能确保管理权不会被转移):

template<class T>
class unique_ptr
{
public:
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}
	~unique_ptr()
	{
	if (_ptr)
	{
	cout << "delete:" << _ptr << endl;
	delete _ptr;
	}
	}
		// 像指针一样使用
		T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	//将它的赋值和拷贝都禁止,就可以保证管理权无法转移
    //delete关键字:声明函数时用到这个可以使这个函数无意义
	unique_ptr(const unique_ptr<T>& sp) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
	T* _ptr;
};

3.3 std::shared_ptr

std::shared_ptr是共享智能指针,它允许多个智能指针共享同一块内存。std::shared_ptr内部使用引用计数来管理内存,当引用计数为0时,内存会被自动释放。

下面我们来看一下库中它的声明方式:

#include <memory>

int main() {
    std::shared_ptr<int> ptr1(new int(10));
    std::shared_ptr<int> ptr2 = ptr1;
    // 使用ptr1和ptr2
    // ...
    return 0;
}

我们来简单的模拟一下它的实现:

	//引用计数
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr=nullptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{}
		~shared_ptr()
		{
			if(--(*_pcount)==0)
			{
				cout << "~shared_ptr()" << endl;
				delete _ptr;
				delete _pcount;
			}
		}
		int use_count()
		{
			return *_pcount;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
		{
			++(*_pcount);
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if(_ptr!=sp._ptr)
			{
				if (--(*_pcount) == 0)
				{
					delete _ptr;
					delete _pcount;
				}
				_ptr = sp._ptr;
				(*sp._pcount)++;
				_pcount = sp._pcount;
				return *this;
			}
		}
	private:
		T* _ptr;
		int* _pcount;
	};

这里其实是涉及到线程锁的一些知识,这些内容比较靠后,等我们后面学到之后再回来讲

3.4 std::weak_ptr

std::weak_ptr是弱指针,它用于解决共享指针可能导致的循环引用问题。弱指针不会增加引用计数,因此不会阻止内存的释放。

那么什么是共享指针的循环引用呢?我们下面详细讲解一下

先看一下下面这个共享指针(就是shared_ptr)的例子

(这个样例是建立在我们上面的模拟实现上的)

//循环引用
struct ListNode
{
	int val;
	zda::shared_ptr<ListNode> prev;
	zda::shared_ptr<ListNode> next;

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
void test_shared_ptr3()
{
	zda::shared_ptr<ListNode> n1 = new ListNode;
	zda::shared_ptr<ListNode> n2 = new ListNode;

	//循环引用(下面这两个同时放开的时候会发生循环引用引发崩溃)
	//n1->next = n2;
	n2->prev = n1;
}

所以说shared_ptr在有些情况下会有循环引用的问题存在,比如链表,而weak_ptr就是专门来解决shared_ptr这个问题的,所以weak_ptr的模拟实现上与shared_ptr十分相似,可以直接借用

	//weak_ptr
	//不支持RAII
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
		{}
		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.~shared_ptr;
			return *this;
		}
		//像指针一样
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};

四、智能指针的使用方法

使用智能指针时,需要注意以下几点:

  1. 初始化:智能指针必须通过new操作符或构造函数进行初始化。
  2. 赋值:智能指针之间可以相互赋值,但std::unique_ptr不能赋值给std::shared_ptr
  3. 解引用:使用解引用运算符(*)和箭头运算符(->)来访问智能指针指向的内存。
  4. 重置:使用reset方法来重置智能指针,释放当前指向的内存,并可以重新指向新的内存。
  5. 比较:智能指针之间可以使用比较运算符进行比较。

五、注意事项

  1. 避免循环引用:在使用共享智能指针时,要注意避免循环引用,否则可能导致内存无法释放。
  2. 不要使用原始指针:尽量避免使用原始指针来管理内存,使用智能指针可以简化代码并提高安全性。
  3. 了解智能指针的行为:在使用智能指针之前,要了解它们的行为,以避免潜在的问题。

六、总结

以上就是C++智能指针的知识点总结,有些涉及线程安全的问题等到后期学到之后再进行补充

感谢各位大佬观看,创作不易,还请各位大佬点赞支持!!!

标签:std,进阶,C++,第十三,智能,内存,shared,ptr,指针
From: https://blog.csdn.net/2301_80220607/article/details/141172119

相关文章

  • C++_类和对象(下篇)
    一、目标1.再谈构造函数2.Static成员3.友元4.内部类二、对目标的讲解1.再谈构造函数1.1构造函数体赋值在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。classDate{public: Date(intyear,intmonth......
  • C/C++ 拷贝构造函数 | 赋值构造函数 | 移动构造函数 | 移动赋值构造函数
    文章目录前言1.拷贝构造函数(CopyConstructor)2.赋值构造函数(CopyAssignmentOperator)3.移动构造函数(MoveConstructor)4.移动赋值构造函数(MoveAssignmentOperator)总结前言C++中关于一个对象的构造,就有很多讲究。其中最常用的可能就是拷贝构造函数......
  • 【c++】用c++写一个十六进制颜色随机产生器
     引入:大家在设计网页时有没有不知道用啥颜色,词汇量太少不知道有啥颜色单词?今天教大家用C++一个程序来随机生成一个16进制的颜色值 #include<iostream>#include<cstdlib>#include<ctime>intmain(){ srand(time(nullptr)); intarr[]={0,1,2,3,4,5,6,7,8,......
  • Kettle PDI小白新手/进阶/必备 大数据基础之一数据清洗(ETL)基础进阶总结 1.6万字长文
    Kettle是一个开源的数据集成工具,主要用于ETL(抽取、转换、加载)过程。它的全名是PentahoDataIntegration(PDI),而Kettle是其早期的名字,Kettle在2006年被Pentaho收购后,正式更名为PentahoDataIntegration(PDI),因此现在更常被称为PDI。PDI仍然是Pentaho产品套件中的一个重要......
  • 彼岸花开C++,模版初阶
    欢迎访问小马的博客,如果觉得小马的博客有帮助的话,记得点赞收藏加关注哦~~~  模版初阶(1)泛型编程(2)函数模版(3)类模版模版初阶(1)泛型编程如何实现一个通用的交换函数?voidSwap(int&a,int&b){inttmp=a;a=b;b=tmp;}voidSwap(double&a,do......
  • 【C++】STL 知识总复习
    文章目录1.STL使用1.1常见的容器1.1.1序列式容器1.1.2关联式容器1.1.3容器适配器1.2迭代器1.2.1输入迭代器(InputIterator)1.2.2输出迭代器(OutputIterator)1.2.3前向迭代器(ForwardIterator)1.2.4双向迭代器(BidirectionalIterator)1.2.5随机访问迭......
  • c++ (2-0) 从txt读取和保存数据
     CMakeLists.txt #设置CMake的最小版本要求cmake_minimum_required(VERSION3.10)#设置项目名称和版本project(PoseSaverVERSION1.0)#设置C++标准为C++11set(CMAKE_CXX_STANDARD11)set(CMAKE_CXX_STANDARD_REQUIREDTrue)#查找Eigen库find_packa......
  • OpenCV图像处理——轮廓的面积与弧长计算(C++/Python)
    概述轮廓面积与轮廓周长是图像分析中的两项核心统计特征,它们为理解和量化图像中的形状提供了基础。轮廓面积:这代表了轮廓所界定区域的像素数量,是衡量区域大小的直接指标。面积的计算结果以像素平方为单位,为我们提供了一个量化的尺度来比较不同物体的相对大小。轮廓周长......
  • C++多线程详解 | 线程创建 | 互斥锁 | 条件变量 | 线程池
    目录前言1.线程创建2.互斥锁3.lock_guard与std::unique_lock4.condition_variable 5.线程池前言在说线程之前,先说说进程和线程的关系,以及什么是多线程(为了方便理解就用大白话来说)进程:进程就是运行中的程序,比如说一个微信的程序,你双击它,它运行起来了就是一个进程,在还......
  • C++ 模版详解 | 函数模板 | 类模版
    前言 什么是模板?模板是一个泛型编程的概念,即不考虑类型的一种编程方式,能够实现代码重用,提高效率模板可分为函数模板、类模板 模板的声明和定义模板的声明有两种,一种就是typename,另外一种就是使用class ,一般使用一种声明格式就可以了,不建议混合使用。template<typenam......