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

2024-08-14 14:51:35 浏览数 (1)

前言:

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

一、引言

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

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

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

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

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

代码语言:javascript复制
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的简单实现:

代码语言:javascript复制
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时,它所指向的内存会被自动释放。

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

代码语言:javascript复制
#include <memory>

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

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

代码语言:javascript复制
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时,内存会被自动释放。

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

代码语言:javascript复制
#include <memory>

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

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

代码语言:javascript复制
	//引用计数
	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)的例子

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

代码语言:javascript复制
//循环引用
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十分相似,可以直接借用

代码语言:javascript复制
	//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 智能指针的知识点总结,有些涉及线程安全的问题等到后期学到之后再进行补充

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

0 人点赞