[cpp进阶]C++智能指针

news/2024/11/22 19:38:45/

文章目录

  • 为什么需要智能指针?
  • 智能指针的原理及使用
    • 智能指针的原理
    • 智能指针的使用
  • C++中的智能指针
    • C++智能指针的发展历程
    • std::auto_ptr
      • std::auto_ptr的使用
      • std::auto_ptr的模拟实现
    • std::unique_ptr
    • std::unique_ptr的使用
    • std::unique_ptr的模拟实现
    • std::shared_ptr
      • std::shared_ptr的使用
      • std::shared_ptr的模拟实现
      • std::shared_ptr的线程安全问题
    • std::weak_ptr
      • std::shared_ptr的循环引用
      • std::weak_ptr的使用
      • std::weak_ptr的模拟实现
    • 智能指针的定制删除器

为什么需要智能指针?

下面我们分析一段关于异常安全的代码:

#include <iostream>
using namespace std;double division(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "division by zero condition!";}return (double)a / (double)b;
}void func()
{// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再// 重新抛出去。int* array = new int[10];int len, time;cin >> len >> time;try{cout << division(len, time) << endl;}catch (...)  // 拦截异常,不是要处理异常,而是要正常释放资源{cout << "delete []" << array << endl;delete[] array;throw;  // 捕获到什么对象就重新抛出什么对象}cout << "delete []" << array << endl;delete[] array;
}int main()
{try{func();}catch (const char* errmsg){cout << errmsg << endl;// 记录日志}return 0;
}

由于division函数可能会抛异常,所以如果直接在main函数捕捉异常,就会导致申请出来的array资源没有得到释放,就会导致内存泄漏问题。

怎么解决这个问题呢?

  1. 在Func函数中拦截异常,将array资源释放掉,再将异常重新抛出。
  2. 申请资源后将指针交给智能指针管理。

智能指针的原理及使用

上述的异常问题,可以通过智能指针来解决。

智能指针的原理

智能指针运用了RAII的思想。RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

智能指针的使用

设计一个智能指针SmartPtr:

  • 在构造SmartPtr时,将用户传入的资源管理起来
  • 在析构SmartPtr时,将管理的资源释放
  • 智能指针SmartPtr能像指针一样进行解引用操作
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~SmartPtr(){cout << "delete: " << _ptr << endl;delete _ptr;}private:T* _ptr;
};

实验代码:

double division(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "division by zero condition!";}return (double)a / (double)b;
}void func()
{int* ptr = new int;SmartPtr<int> sp(ptr); // 使用智能指针管理ptr资源int len, time;cin >> len >> time;cout << division(len, time) << endl;
}int main()
{try{func();}catch (const char* errmsg){cout << errmsg << endl;}return 0;
}

实验结果:

在这里插入图片描述

在func函数中,申请了ptr资源并交于SmartPtr进行管理,当出现除0错误,异常由division函数抛出,main函数捕捉时,func函数的函数栈帧销毁,SmartPtr对象也被回收,SmartPtr对象销毁之前会调用析构函数释放管理的资源,这样就避免了内存泄漏的问题

但是这样的SmartPtr智能指针还有问题:

int main()
{SmartPtr<int> sp1(new int);SmartPtr<int> sp2(sp1);return 0;
}

运行结果:

在这里插入图片描述

SmartPtr智能指针不能进行拷贝构造,因为这样会释放两次资源,导致程序崩溃。


C++中的智能指针

C++智能指针的发展历程

  • C++98标准中产生了第一个智能指针auto_ptr
  • C++ boost库中给出了更实用的scoped_ptr、shared_ptr和weak_ptr
  • C++11标准中引入了unique_ptr、shared_ptr和weak_ptr,这些智能指针的实现原理都是参考boost库的,unique_ptr参考的是scoped_ptr

std::auto_ptr

std::auto_ptr的使用

C++ auto_ptr智能指针文档

std::auto_ptr的实现原理是管理权的转移。

实验代码:

#include <iostream>
#include <memory>int main()
{std::auto_ptr<int> ap1(new int(10));std::auto_ptr<int> ap2(ap1);return 0;
}

实验结果:

在这里插入图片描述
std::auto_ptr解决智能指针拷贝构造的问题是用管理权转移解决的,但是随之而来会带来另一个问题,管理权转移让ap1指针悬空了,如果用户对std::auto_ptr不熟悉,继续使用ap1进行一系列操作,而ap1已经悬空,这势必会导致程序的崩溃。

实验代码:

#include <iostream>
#include <memory>int main()
{std::auto_ptr<int> ap1(new int(10));std::auto_ptr<int> ap2(ap1);*ap1 = 20;return 0;
}

实验结果:

在这里插入图片描述


std::auto_ptr的模拟实现

下面简化模拟实现了一份auto_ptr来了解它的原理:

namespace cwx
{template<class T>class auto_ptr{public:auto_ptr(T* ptr = nullptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){// 管理权的转移ap._ptr = nullptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}

std::unique_ptr

std::unique_ptr的使用

std::auto_ptr管理权转移的方式,经过时间的证明,它是一个失败的设计。C++11中引入更加实用的std::unique_ptrstd::shared_ptr

std::unique_ptr是参考了boost库中的scoped_ptr,它的原理非常简单粗暴,它解决智能指针拷贝构造的方式是直接防拷贝。

实验代码:

#include <iostream>
#include <memory>int main()
{std::unique_ptr<int> up1(new int);std::unique_ptr<int> up2(up1);       // errorreturn 0;
}

实验结果:
在这里插入图片描述


std::unique_ptr的模拟实现

下面简化模拟实现了一份unique_ptr来了解它的原理:

namespace cwx
{template<class T>class unique_ptr{public:unique_ptr(T* ptr = nullptr):_ptr(ptr){}// 防拷贝unique_ptr(const unique_ptr<T>&) = delete;unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~unique_ptr(){if (_ptr){delete _ptr;}}private:T* _ptr;};
}

std::shared_ptr

std::shared_ptr的使用

std::unique_ptr实现了简单粗暴的防拷贝,但是难以避免要需要用到拷贝。C++11引入了std::unique_ptrstd::unique_ptr使用了引用计数的技术来实现智能指针的拷贝问题。

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

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

实验代码:

#include <iostream>
#include <memory>int main()
{std::shared_ptr<int> sp1(new int(10));std::shared_ptr<int> sp2(sp1);std::shared_ptr<int> sp3(new int(20));sp3 = sp2;return 0;
}

实验结果:

在这里插入图片描述

原理:

  • std::shared_ptr<int> sp1(new int(10));定义了智能指针sp1,管理new出来的int空间,sp1对象中的引用计数加一,refs = 1
  • std::shared_ptr<int> sp2(sp1);定义了智能指针sp2,拷贝sp1,sp1和sp2对象中的引用计数加一,refs = 2
  • std::shared_ptr<int> sp3(new int(20));定义了智能指针sp3,管理new出来的int空间,sp3对象中的引用计数加一,refs = 1
  • sp3 = sp2;,sp3对象赋值拷贝sp2,原来sp3指向的空间的引用计数减一,refs = 0,则释放值为20的空间,sp3现在指向值为10的空间,sp1、sp2和sp3的引用计数加一,refs = 3
    在这里插入图片描述

std::shared_ptr的模拟实现

namespace cwx
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _pRefCount(new int(1)){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pRefCount(sp._pRefCount){++(*_pRefCount);}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){if (--(*_pRefCount) == 0 && _ptr){delete _ptr;delete _pRefCount;}_ptr = sp._ptr;_pRefCount = sp._pRefCount;++(*_pRefCount);}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~shared_ptr(){if (--(*_pRefCount) == 0 && _ptr){delete _ptr;delete _pRefCount;}}private:T* _ptr;int* _pRefCount;};
}

std::shared_ptr的线程安全问题

模拟实现的shared_ptr还存在线程安全的问题,由于管理同一个资源的多个对象的引用计数是共享的。

在多线程环境下可能会同时对同一个引用计数进行自增或自减操作,而自增和自减操作都不是原子操作,因此需要通过加锁来对引用计数进行保护,否
则就会导致线程安全问题。

加锁后代码:

  • 将进行引用计数自增自减的代码封装成函数,便于加锁
  • 引入mutex,对自增自减操作进行加锁
namespace cwx
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _pRefCount(new int(1)), _pmtx(new mutex){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pRefCount(sp._pRefCount), _pmtx(sp._pmtx){AddRef();}void AddRef(){_pmtx->lock();++(*_pRefCount);_pmtx->unlock();}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){Release();_ptr = sp._ptr;_pRefCount = sp._pRefCount;AddRef();}return *this;}void Release(){_pmtx->lock();bool flag = false;if (--(*_pRefCount) == 0 && _ptr){delete _ptr;delete _pRefCount;flag = true;}_pmtx->unlock();if (flag){delete _pmtx;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get() const{return _ptr;}~shared_ptr(){Release();}private:T* _ptr;int* _pRefCount;mutex* _pmtx;};
}

std::weak_ptr

std::shared_ptr的循环引用

实验代码:

struct ListNode
{int _val;cwx::shared_ptr<ListNode> _prev;cwx::shared_ptr<ListNode> _next;~ListNode() { cout << "~ListNode()" << endl; }
};int main()
{cwx::shared_ptr<ListNode> node1(new ListNode);cwx::shared_ptr<ListNode> node2(new ListNode);node1->_next = node2;node2->_prev = node1;return 0;
}

实验结果:

在这里插入图片描述

实验原理

程序创建了node1和node2两个链表结点,用shared_ptr智能指针管理,node1和node2的引用计数当前分别为1,node1的_next指向node2,node2的_prev指向node1,此时node1和node2的引用计数分别为2。

在这里插入图片描述

程序运行结束后,node1和node2调用析构函数,引用计数减一,此时引用计数为1,然而程序已经结束,node1和node2开辟的空间并没有被释放,这种现象叫做循环引用。


std::weak_ptr的使用

在上述的实验场景中,循环引用导致资源没有被释放的问题,需要使用std::weak_ptr来解决。

std::weak_ptr的原理是可以获取并访问指向的资源,但是不参与引用计数。

实验代码:

struct ListNode
{int _val;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;
}

实验结果:

在这里插入图片描述


std::weak_ptr的模拟实现

下面简化模拟实现了一份weak_ptr来了解它的原理:

namespace cwx
{template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* ptr;};
}

智能指针的定制删除器

模拟实现的智能指针默认都用delete释放资源,但是如果是申请一个数组资源,fopen打开一个文件,delete就会不匹配或者程序直接就崩溃了。

C++文档中,智能指针的接口定义了一个模板D,就是定制删除器。定制删除器本质是一个可调用对象,比如函数指针、仿函数或者lambda表达式。

在这里插入图片描述

下面给unique_ptr实现一个简化的定制删除器:

namespace cwx
{template<class T, class D = default_delete<T>>class unique_ptr{public:unique_ptr(T* ptr = nullptr):_ptr(ptr){}// 防拷贝unique_ptr(const unique_ptr<T>&) = delete;unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~unique_ptr(){if (_ptr){D del;del(_ptr);}}private:T* _ptr;};
}template<class T>
struct DelArray
{void operator()(const T* ptr){delete[] ptr;}
};template<class T>
struct DelFile
{void operator()(FILE* file_name){cout << "fclose: " << file_name << endl;fclose(file_name);}
};int main()
{unique_ptr<int, DelArray<int>> up1(new int[10]);unique_ptr<FILE, DelFile<FILE>> up2(fopen("test.txt", "w"));return 0;
}


http://www.ppmy.cn/news/9641.html

相关文章

Rust入门(七):编写测试

Rust 中的测试函数是用来验证代码是否是按照你期望的方式运行的一类函数&#xff1a; 函数测试 Rust 中的测试就是一个带有 test 属性注解的函数&#xff0c;当使用 cargo test 命令运行测试时&#xff0c;Rust 会构建一个测试执行程序用来调用标记了 test 属性的函数&#x…

git---常用命令集合

适用平台:gitee github gerrit gitlab 提交代码和查看相关信息 git log git status git add . 增加所有修改,需要添加指定文件可以选择添加文件即可 git commit -m "xxx" git push git reset --hard commitid 保持与服务器更新到commitid git pull git diff HEAD^ …

JavaScript篇.day10-面向对象,对象,构造函数,this关键字,原型

目录面向对象对象构造函数this关键字原型面向对象面向过程: 在开发过程中,关注过程的开发方式. 在开发时关注每一个细节,步骤和顺序.面向对象: 在开发过程中,只需要找一个对象来完成事情的开发思想对象: 在生活中,万物皆对象 封装: 将完成步骤封装在对象内部属性: 对象的特征核…

JavaWeb项目 -- 博客系统

JavaWeb项目 -- 博客系统前言&#xff1a;页面展示一、创建 Maven 项目二、设计数据库三、封装数据库的操作3.1 创建 DBUtil 类3.2 创建 Blog 类3.3 创建 User 类3.4 创建类 BlogDao3.5 创建类 UserDao四、导入准备好的前端代码五、实现博客列表界面5.1 约定好前后端交互接口5.…

简单又好用的财务分析工具有哪些?

什么样的财务分析工具才能算是简单又好用&#xff1f;是能够快速完成组合多变的财务指标运算分析&#xff1b;能够充分发挥企业经营健康晴雨表作用&#xff0c;反映企业财务健康状态&#xff1b;还是能够支持多维度动态分析、自助分析&#xff1b;或者是轻松合并账套&#xff0…

Java集合类ArrayList应用 | 如何在字符串s1中删除有在字符串s2出现的字符?

目录 一、题干 二、题解 1. 思路 ArrayList实现 2. 代码 ArrayList实现 StringBuilder实现-1 StringBuilder实现-2 三、总结 一、题干 面试的编程题&#xff1a; s1: "welcome to Zhejiang" s2: "come" 要求输出从字符串s1删除s2中存在的字符之后…

【Java寒假打卡】Java基础-BigDecimal

【Java寒假打卡】Java基础-BigDecimal构造方法四则运算BigDecimal的特殊方法基本数据类型包装类自动装箱与自动拆箱Integer的类型转换将数字字符串进行拆分成整数数组构造方法 package com.hfut.edu.test1;import java.math.BigDecimal;public class test3 {public static void…

Spring AOP 面向切面编程

1.AOP是什么我们之前学过 OOP 面向对象编程, 那么 AOP 是否就比 OOP 更牛逼呢? 是否也是面向过程到面向对象这种质的跨越呢? 其实不是, 它和 OOP 思想是一种互补的关系, 是对 OOP 思想的一种补充.AOP (Aspect Oriented Programming) : 面向切面编程, 它是一种思想, 它是对某一…