Advertisement

【C++】智能指针相关知识详细梳理

阅读量:

0. 引言

为什么需要智能指针?

先看看下面一段程序:

复制代码
 int div()

    
 {
    
     int a, b;
    
     cin >> a >> b;
    
     if (b == 0)
    
     throw invalid_argument("除0错误");
    
     return a / b;
    
 }
    
 void Func()
    
 {
    
     // 1、如果p1这里new 抛异常会如何?
    
     // 2、如果p2这里new 抛异常会如何?
    
     // 3、如果div调用这里又会抛异常会如何?
    
     int* p1 = new int;
    
     int* p2 = new int;
    
     cout << div() << endl;
    
     delete p1;
    
     delete p2;
    
 }
    
 int main()
    
 {
    
     try
    
     {
    
     Func();
    
     }
    
     catch (exception& e)
    
     {
    
     cout << e.what() << endl;
    
     }
    
     return 0;
    
 }
    
    
    
    

在上述例子中,在p2处的new和div()中任意一处抛出异常,都会导致内存泄漏,为了防止此种情况发生,就有了智能指针。

现在可以回答开头的问题了:

智能指针的设计初衷是为了解决手动管理动态内存 时容易出现的一系列问题,例如内存泄漏、悬空指针、重复释放 等。传统的手动内存管理依赖于 newdelete,这要求程序员必须显式地分配和释放资源。然而,手动管理容易导致错误,特别是在异常、复杂的控制流或多线程环境中。 因此,引入智能指针不仅提升了代码的安全性 ,还增强了可维护性可读性

那么何为内存泄漏呢?

1. 内存泄漏

1.1 何为内存泄漏

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。 内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

1.2 内存泄漏的危害

长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现
内存泄漏会导致响应越来越慢,最终卡死。

1.3 内存泄漏的类别

堆内存泄漏(Heap leak):
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一
块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分
内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏:
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放
掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定

2. 智能指针

2.1 智能指针的用法及原理

0. RAII

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

接下来我们所说的每一种智能指针都基于RAII的原理。

1. std::auto_ptr(C++11弃用)

C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。
auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一个auto_ptr来了解它的原
理。

复制代码
 // C++98 管理权转移 auto_ptr

    
 namespace kia
    
 {
    
     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 = nullptr;
    
         }
    
         return *this;
    
     }
    
     ~auto_ptr()
    
     {
    
         if (_ptr)
    
         {
    
             cout << "delete:" << _ptr << endl;
    
             delete _ptr;
    
         }
    
     }
    
     // 像指针一样使用
    
     T& operator*()
    
     {
    
         return *_ptr;
    
     }
    
     T* operator->()
    
     {
    
         return _ptr;
    
     }
    
     private:
    
     T* _ptr;
    
     };
    
 }
    
 // 结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr
    
 //int main()
    
 //{
    
 // std::auto_ptr<int> sp1(new int);
    
 // std::auto_ptr<int> sp2(sp1); // 管理权转移
    
 //
    
 // // sp1悬空
    
 // *sp2 = 10;
    
 // cout << *sp2 << endl;
    
 // cout << *sp1 << endl;
    
 // return 0;
    
 //}
    
    
    
    

auto_ptr是 C++98 引入的智能指针之一,但它在现代 C++ 中被认为是“失败的设计”,并在 C++11 中被弃用,最终在 C++17 中被移除。它的失败主要归因于其独特的“所有权转移 ”机制,这种机制与开发者期望的行为不一致,且存在潜在的错误风险。

除此之外,由于 auto_ptr 的所有权转移机制,它不能用于标准模板库(STL)容器中。STL 容器需要元素支持拷贝赋值 操作,而 auto_ptr 的行为不符合这些要求。

2. std::unique_ptr

C++11开始提供:

用法和普通指针完全类似,下面模拟实现一个unique_ptr展示原理和使用:

复制代码
 // unique_ptr/scoped_ptr

    
 // 核心机制: RAII + 防拷贝
    
 namespace kia
    
 {
    
 	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;
    
 		}
    
     //封住拷贝
    
 		unique_ptr(const unique_ptr<T>& sp) = delete;
    
 		unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
    
 	private:
    
 		T* _ptr;
    
 	};
    
 }
    
 //int main()
    
 //{
    
 // /*bit::unique_ptr<int> sp1(new int);
    
 // bit::unique_ptr<int> sp2(sp1);*/
    
 //
    
 // std::unique_ptr<int> sp1(new int);
    
 // //std::unique_ptr<int> sp2(sp1);
    
 //
    
 // return 0;
    
 //}
    
    
    
    

3. std::shared_ptr

C++11开始提供:

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

模拟实现:

复制代码
 // 引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源

    
 #pragma once
    
  
    
 #include <iostream>
    
 // 引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源
    
 namespace kia
    
 {
    
 	template<class T>
    
 	class shared_ptr
    
 	{
    
 	public:
    
 		//构造
    
 		shared_ptr(T* ptr = nullptr)
    
 			:_ptr(ptr)
    
 			, _prefcount(new int(1))
    
 		{}
    
  
    
 		//拷贝构造
    
 		shared_ptr(const shared_ptr& sp)
    
 			:_ptr(sp._ptr)
    
 			, _prefcount(sp._prefcount)
    
 		{
    
 			//引用计数加一
    
 			++(*_prefcount);
    
 		}
    
  
    
 		//析构
    
 		~shared_ptr()
    
 		{
    
 			--(*_prefcount);
    
 			//如果引用计数为0,则可以释放资源
    
 			if (*_prefcount == 0)
    
 			{
    
 				delete _ptr;
    
 				std::cout << "delete _ptr" << std::endl;
    
 				delete _prefcount;
    
 				std::cout << "delete _prefcount" << std::endl;
    
 			}
    
 		}
    
  
    
 		//=运算符重载
    
 		shared_ptr& operator=(const shared_ptr& sp)
    
 		{
    
 			//防止自己给自己赋值
    
 			if (_ptr != sp._ptr)
    
 			{
    
 				//释放掉原有资源
    
 				Release();
    
 				_ptr = sp._ptr;
    
 				_prefcount = sp._prefcount;
    
 				//引用计数++
    
 				++(*_prefcount);
    
 			}
    
 			return *this;
    
 		}
    
  
    
  
    
 		//解引用重载
    
 		T& operator*()
    
 		{
    
 			return *_ptr;
    
 		}
    
  
    
 		//->运算符重载
    
 		T* operator->()
    
 		{
    
 			return _ptr;
    
 		}
    
  
    
 	private:
    
 		//清空资源
    
 		void Release()
    
 		{
    
 			--(*_prefcount);
    
 			//如果引用计数为0,则可以释放资源
    
 			if (*_prefcount == 0)
    
 			{
    
 				delete _ptr;
    
 				std::cout << "delete _ptr" << std::endl;
    
 				delete _prefcount;
    
 				std::cout << "delete _prefcount" << std::endl;
    
 			}
    
 		}
    
  
    
 		T* _ptr;
    
 		int* _prefcount;
    
 	};
    
 }
    
  
    
 /*class C
    
 {
    
 public:
    
 	int _a = 0;
    
 	int _b = 1;
    
 };
    
   97. int main()
    
 {
    
 	kia::shared_ptr<int> sp1 = new int(10);
    
 	kia::shared_ptr<int> sp3(sp1);
    
 	kia::shared_ptr<int> sp2 (new int(12));
    
 	kia::shared_ptr<int> sp4;
    
 	sp4 = sp1;
    
 	kia::shared_ptr<int> sp5;
    
 	kia::shared_ptr<C> sp6 = new C;
    
 	cout << *sp3 << endl;
    
 	cout << *sp2 << endl;
    
 	cout << sp6->_a << " " << sp6->_b << endl;
    
 	return 0;
    
 }
    
 */
    
    
    
    

实现中省略了线程安全的部分,实际的shared_ptr引用计数的加减是加锁保护的。但是指向资源不是线程安全的。另外,指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了。

3.1 std::shared_ptr的循环引用

复制代码
 struct ListNode

    
 {
    
     int _data;
    
     shared_ptr<ListNode> _prev;
    
     shared_ptr<ListNode> _next;
    
     ~ListNode(){ cout << "~ListNode()" << endl; }
    
 };
    
 int main()
    
 {
    
     shared_ptr<ListNode> node1(new ListNode);
    
     shared_ptr<ListNode> node2(new ListNode);
    
     cout << node1.use_count() << endl;
    
     cout << node2.use_count() << endl;
    
     node1->_next = node2;
    
     node2->_prev = node1;
    
     cout << node1.use_count() << endl;
    
     cout << node2.use_count() << endl;
    
     return 0;
    
 }
    
    
    
    

以上代码中的循环引用:

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属于node1的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev
属于node2成员,所以这就叫循环引用,谁也不会释放。

解决方案:weak_ptr

4. weak_ptr

std::weak_ptr 是 C++11 引入的一种智能指针,用于解决std::shared_ptr 环引用的问题。它是一种不控制对象生命周期的弱引用指针,主要与std::shared_ptr 搭配使用。

weak_ptr 用于提供对资源的非所有权访问即它不增加资源的引用计数,不参与资源的生命周期管理。 它常用于避免 shared_ptr 循环引用 问题。例如,两个对象 A 和 B 相互持有对方的 shared_ptr ,会导致引用计数永远无法归零,造成内存泄漏。通过将其中一方的**shared_ptr **改为 weak_ptr ,就可以打破这种循环引用。

可以利用weak_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);
    
     cout << node1.use_count() << endl;
    
     cout << node2.use_count() << endl;
    
     node1->_next = node2;
    
     node2->_prev = node1;
    
     cout << node1.use_count() << endl;
    
     cout << node2.use_count() << endl;
    
     return 0;
    
 }
    
    
    
    

3. C++11和boost中智能指针的关系

1. C++ 98 中产生了第一个智能指针auto_ptr。
2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr。
3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost
的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

全部评论 (0)

还没有任何评论哟~