c++:智能指针

文章目录

  • 前言
  • 一、内存泄漏
    • [1.1 内存泄漏的定义](#1.1 内存泄漏的定义)
    • [1.2 内存泄漏的常见原因](#1.2 内存泄漏的常见原因)
    • 1.3内存泄漏的危害
  • 二、智能指针的用法和模拟实现
    • [2.1 RAII](#2.1 RAII)
      • [2.1.1 RAII的工作原理](#2.1.1 RAII的工作原理)
      • [2.1.2 RAII的优点](#2.1.2 RAII的优点)
    • [2.2 智能指针的原理和设计思路](#2.2 智能指针的原理和设计思路)
    • [2.3 智能指针的种类和特点](#2.3 智能指针的种类和特点)
      • [2.3.1 std::auto_ptr](#2.3.1 std::auto_ptr)
      • [2.3.2 std::unique_ptr](#2.3.2 std::unique_ptr)
      • [2.3.3 std::shared_ptr](#2.3.3 std::shared_ptr)
      • [2.3.4 std::weak_ptr](#2.3.4 std::weak_ptr)
  • [三. 智能指针使用时注意事项](#三. 智能指针使用时注意事项)
  • 总结

前言

智能指针(smart pointer)是一种用来防止内存泄漏的编程技术,它利用对象管理资源的方式(又名RAII------Resource Acquisition Is Initialization),即利用对象析构函数在生命周期结束时自行调用的特性,让编译器自行释放动态开辟的空间。


一、内存泄漏

1.1 内存泄漏的定义

内存泄漏通常发生在使用了new或malloc等函数动态分配内存后,忘记了对应的delete或free调用,或者是因为逻辑错误导致这些调用未能执行。这种情况下,程序占用的内存会逐渐增加,最终可能导致系统资源耗尽,程序崩溃或运行缓慢。

1.2 内存泄漏的常见原因

1.忘记释放内存:程序员在堆上动态分配内存后,忘记或未能正确地释放它。

2.异常处理不当:当程序发生异常时,执行流程可能会发生改变,导致某些正常流程中的内存释放操作无法执行

cpp 复制代码
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;
}

3.拷贝构造函数和赋值运算符未正确处理动态内存:当类对象包含动态分配的内存时,拷贝构造函数和赋值运算符需要特别处理,以确保在对象复制或赋值时不会导致内存泄漏。

4.容器使用不当:C++标准库提供了许多容器类(如std::vector、std::string等),它们内部自动管理内存。然而,如果在使用这些容器时不当(如将一个容器作为另一个容器的元素,并在某个时刻改变了容器的容量),也可能导致内存泄漏。
5.循环引用:两个或多个对象相互引用,形成一个循环,导致内存无法被释放。(往下会介绍此种泄露的原因和解决办法)

1.3内存泄漏的危害

内存泄漏会导致程序占用的内存逐渐增加,最终可能导致系统资源耗尽,程序崩溃或运行缓慢。此外,内存泄漏还可能引发其他严重问题,如内存碎片、系统不稳定等

智能指针的出现就是解决这种危害的一种方法。

二、智能指针的用法和模拟实现

2.1 RAII

实际上RAII就是利用一个类来进行动态开辟的资源的管理,这个类是一个模板类,我们只需要在实例化这个类的时候传需要被管理的资源的类型,如此就可以很好的解决内存泄漏问题

RAII(Resource Acquisition Is Initialization)是一种管理资源的技术,它利用C++对象的生命周期来自动管理资源。RAII的核心思想是,将资源的获取(如动态内存分配、文件打开、互斥锁锁定等)与对象的构造绑定在一起,而将资源的释放(如内存释放、文件关闭、互斥锁解锁等)与对象的析构绑定在一起。这样,当对象超出其作用域或被显式销毁时,资源会自动被释放。

2.1.1 RAII的工作原理

1.资源获取与对象构造:

当一个对象被构造时,它会自动获取所需的资源。这通常是通过调用构造函数中的代码来实现的。

构造函数负责确保资源被正确分配和初始化。

2.资源使用与对象生存:

在对象的生命周期内,资源可以被安全地使用。

对象的成员函数可以访问和操作这些资源。

3.资源释放与对象析构:

当对象被销毁时(例如,超出其作用域、被显式删除或作为另一个对象的成员而被销毁),它的析构函数会被自动调用。

析构函数负责释放资源,确保不会发生内存泄漏或其他资源泄漏。

2.1.2 RAII的优点

1.自动管理资源:RAII通过对象的生命周期来自动管理资源,减少了手动管理资源的复杂性和出错的可能性。
2.异常安全性:由于资源的释放是在析构函数中进行的,而析构函数在异常传播过程中总是会被调用,因此RAII提供了异常安全性。即使程序在资源使用期间抛出异常,资源也会被正确释放。

3.简化代码:使用RAII可以简化资源管理代码,使代码更加清晰和易于维护

2.2 智能指针的原理和设计思路

智能指针是一种能够自动管理指针指向内存的类模板。内部使用(RAII的原理)来自动释放内存。

|-------------------------------------------|
| 智能指针中会重载operator*和opertaor->,具有像指针一样的行为 |

2.3 智能指针的种类和特点

智能指针主要分为两类:不带引用计数的智能指针和带引用计数的智能指针 。d都包含在c++的标准库中的 memory 下面。

不带引用计数的智能指针:
auto_ptr :C++98标准库提供的智能指针,但由于其管理权会转移的特性,容易导致程序崩溃,因此不推荐使用。
scoped_ptr :C++11标准库提供的智能指针,防止拷贝,确保资源在同一作用域内被管理,离开作用域时自动释放资源。但C++17中已被弃用。
unique_ptr:C++11标准库提供的智能指针,不允许拷贝,但允许移动,确保资源的唯一所有权,离开作用域时自动释放资源。
带引用计数的智能指针:
shared_ptr :多个智能指针可以管理同一个资源,每个资源匹配一个引用计数。当智能指针指向该资源时,引用计数加1;当该指针不再使用该资源时,引用计数减1。当引用计数为0时,释放资源。shared_ptr是线程安全的,可直接用于多线程环境。但需要注意循环引用的问题。
weak_ptr :为了解决shared_ptr的循环引用问题而设计的。weak_ptr指向已被shared_ptr管理的资源时,不会增加其引用计数。在weak_ptr解除指向资源时,也不会减少引用计数。因此,weak_ptr不能单独管理资源,必须与shared_ptr配合使用。

2.3.1 std::auto_ptr

auto_ptr是在C++98版本中就给出的,它的实现原理是:管理权转移,只有一个对象能够管理资源(意味着转移后的那个指针会被悬空 )

我们先来看看其模拟代码实现以及使用

cpp 复制代码
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)
	{
		// 检测是否为自己给自己赋值(这一步很重要,不然容易发生双重释放)
		//一旦使用delete或delete[]释放了内存,就不应该再次尝试释放同一块内存。
		//这会导致未定义行为,可能包括程序崩溃。
		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;
};
int main()
{
  auto_ptr<int> s1(new int);
  auto_ptr<int> s2=s1;//此时s1为空指针,s2获取到了s1申请的资源
  //这份资源会随着程序结束时随着对象的生命周期结束而调用析构函数自动的析构
}

2.3.2 std::unique_ptr

unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原理

cpp 复制代码
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;
};

2.3.3 std::shared_ptr

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

cpp 复制代码
	//引用计数
	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;
	};

声明方式

cpp 复制代码
#include <memory>
 
int main() {
   shared_ptr<int> ptr1(new int(10));
    shared_ptr<int> ptr2 = ptr1;
    // 使用ptr1和ptr2
    // ...
    return 0;
}

2.3.4 std::weak_ptr

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

循环引用发生的场景如下:

结合share_ptr的引用计数我们可以总结出如下规律:循环引用会导致内存泄漏,因为相互引用的对象无法被正确地销毁。即使程序不再需要这些对象,由于它们的引用计数始终大于零

std::weak_ptr是一种不控制所指向对象生命周期的智能指针,它指向一个由std::shared_ptr管理的对象,但不会增加对象的引用计数。因此,将循环引用中的一个std::shared_ptr替换为std::weak_ptr可以打破循环引用,从而避免内存泄漏。

cpp 复制代码
	//weak_ptr
	//不支持RAII
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
		{}
		//相比于share_ptr没有增删引用计数的数目
		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.ptr;
			return *this;
		}
		//像指针一样
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};

三. 智能指针使用时注意事项

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

总结

以上知识点具体的使用还需结合实际情况,仍是道阻且长。

相关推荐
锅包肉的九珍1 分钟前
Scala的Array数组
开发语言·后端·scala
心仪悦悦4 分钟前
Scala的Array(2)
开发语言·后端·scala
yqcoder27 分钟前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript
baivfhpwxf202337 分钟前
C# 5000 转16进制 字节(激光器串口通讯生成指定格式命令)
开发语言·c#
许嵩6640 分钟前
IC脚本之perl
开发语言·perl
长亭外的少年1 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
直裾1 小时前
Scala全文单词统计
开发语言·c#·scala
心仪悦悦1 小时前
Scala中的集合复习(1)
开发语言·后端·scala
JIAY_WX1 小时前
kotlin
开发语言·kotlin