【C++】智能指针详解

article/2025/9/19 12:20:36

今天我们来讲一下c++中的智能指针。

目录

  • 1. 智能指针初识
    • 1.1 什么是智能指针
    • 1.2 智能指针发展历史
    • 1.3 为什么需要智能指针
  • 3. 智能指针原理
    • 3.1 RALL
    • 3.2 智能指针的分类
      • 3.2.1 auto_ptr
      • 3.2.2 unique_ptr
      • 3.2.3 shared_ptr
        • 3.2.3.1 shared_ptr 原理
        • 3.2.3.2 shared_ptr 的模拟实现
        • 3.2.3.3 定制删除器
      • 3.2.4 weak_ptr

1. 智能指针初识

1.1 什么是智能指针

智能指针不是指针,是一个管理指针的类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。

动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源


1.2 智能指针发展历史

  1. C++ 98 中产生了第一个智能指针auto_ptr
  2. C++boost给出了更加实用的scoped_ptr 和 shared_ptr 和 weak_ptr
  3. C++ 11 引入了unquie_ptr 和 shared_ptr 和 weak_ptrt .需要注意的是,unique_ptr对应的是boost中的scoped_ptr。并且这些智能指针的实现是参照boost中的实现的。

1.3 为什么需要智能指针

在之前的博客 【C++】异常 中我提到过一个叫做异常的重新抛出的场景:

我们再把那个例子讲一下:


void File()
{string filename;cin >> filename;FILE* fout = fopen(filename.c_str(), "r");if (fout == nullptr) {string errmsg = "打开文件失败:";errmsg += filename;errmsg += "->";errmsg += strerror(errno);Exception e(errno, errmsg);throw e;}char ch;while ((ch = fgetc(fout))!=EOF) {cout << ch;}fclose(fout);
}double Division(int a, int b)
{if (b == 0) {string errmsg = "Division by zero condition!";Exception e(100, errmsg);throw e;}else{return ((double)a / (double)b);}
}void Func()
{int* p = new int[100];int len, time;cin >> len >> time;try {cout << Division(len,time) << endl;File();}catch (...){//捕获之后,不是要处理异常,异常由最外层同一处理//这里捕获异常只是为了处理内存泄漏的问题delete[]p;throw; }delete[]p;
}int main()
{try {Func();}catch (const Exception& e) {cout << e.what() << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}

在Func函数中,我们在堆上创建了开一个指针,为了防止函数抛出异常导致最后的 析构函数不执行而产生野指针,我们使用了 异常的重新抛出策略。

但是,终究不是个好的方法,如果这类资源较多,那么我们需要大量的 异常重抛 ,而且就算程序不涉及程序处理,大量的堆上空间需要人工释放,容易造成疏漏,这一问题在工程中比较常见。

所以,这时候如果我们实用智能指针,就可以不用再操心内存是否会泄露的问题

3. 智能指针原理

3.1 RALL

RAII 是 resource acquisition is initialization 的缩写,意为“资源获取即初始化”。它是 C++ 之父 Bjarne Stroustrup 提出的设计理念,其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。在 RAII 的指导下,C++ 把底层的资源管理问题提升到了对象生命周期管理的更高层次。

借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

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

我们可以借助RALL思想来写一个简单的 智能指针:

#include<iostream>
using namespace std;template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr =nullptr):_ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;cout<<"~SmartPtr"<<endl;}
private:T* _ptr;
};int main()
{int* a = new int(1);SmartPtr<int> sp(a); //将a 指针委托给sp对象管理SmartPtr<int>sp2(new int(2)); //直接船舰匿名对象给sp2管理
}

3.2 智能指针的分类

上面的SmartPtr 还不可以被称为智能指针,因为它还不具有指针的行为与性质。

指针可以解引用,也可以通过->去访问所指向的空间中的内容,因此智能指针还需要将 *,->重载。

除此之外,如果我们使用了 拷贝或者赋值操作,就会发生浅拷贝的问题,由于二者指向同一块空间,所以在析构的时候也会析构两次,造成错误。

在这里插入图片描述
所以,为了解决以上问题,C++提供了几种设计方案实现的智能指针,我们下面来一一讲解。


C++中存在4种智能指针:auto_ptr,unquie_ptr,shared_ptr,weak_ptr,他们各有优缺点,以及对应的实用场景。

3.2.1 auto_ptr

在C++98版本的库种,提供了 auto_ptr 的智能指针:

我们使用一下std::auto_ptr:

class Date
{
public:Date():_year(0),_month(0),_day(0){}~Date(){}int _year;int _month;int _day;};int main()
{auto_ptr<Date>ap(new Date);//拷贝构造auto_ptr<Date>copy(ap);ap->_year = 2022;
}

我们发现报错了,发生了非法访问。

这就是auto_ptr 的弊病,当我们使用对象拷贝或者赋值之后,之前的那个对象就被置空(如下图)

在这里插入图片描述
在拷贝或者赋值的过程种,auto_ptr 会传递所有权,将资源全部从源指针转移给目标指针,源指针被置空。

虽然这种方法确实解决了 浅拷贝的问题,但是十分局限性也很大,这也就导致了,我们使用auto_ptr的时候要注意,不要对源指针进行访问或者操作。

由于C++98种提供的这个智能指针问题明显,所以在实际工作种哼多公司是明确规定了不能使用auto_ptr的。


那么auto_ptr具体是如何实现的呢?很简单.

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; //管理权转移}auto_ptr<T>& operator = (auto_ptr<T>& ap){if (this != *ap) {delete _ptr;_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}~SmartPtr(){if (_ptr)delete _ptr;}T& operator *(){return *_ptr;}T* operator ->(){return _ptr;}private:T* _ptr;};

3.2.2 unique_ptr

在C++11中,C++11y引入了unique_ptr.

unique_ptr的原理很简单,就是一个“得不到就毁掉”的理念,直接把拷贝和赋值禁止了。

对于用不上赋值拷贝的场景的时候,我们选择unique_ptr也是一个不错的选择。


我们可以尝试实现一下:

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

3.2.3 shared_ptr

3.2.3.1 shared_ptr 原理

C++中还提供了shared_ptr。

shared_ptr 是当前最为广泛使用的智能指针,它可以安全的提供拷贝操作。

我们可以测试使用一下:
在这里插入图片描述


那么shared_ptr的原理是什么?

我们可以对一个资源添加一个计数器,让所有管理该资源的智能共用这个计数器,倘若发生拷贝,计数器加一,倘若有析构发生, 计数器减一,当计数器等于0的时候,就把对象析构掉。

再具体一点:

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

3.2.3.2 shared_ptr 的模拟实现

我们可以实现一个简单的shared_ptr:

template<class T>class shared_ptr{public:shared_ptr(T*ptr =nullptr):_ptr(ptr),_pcount(new int(1)){}//拷贝构造shared_ptr(const T& sp)_ptr(sp._ptr),_pcount(sp._pcount){++(*_pcount);}//赋值拷贝shared_ptr<T>& operator = (shared_ptr<T>& sp){if (_ptr != sp._ptr) {if (--(*_pcount) == 0){delete _pcount;delete _ptr;}_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}T& operator *(){return *_ptr;}T* operator ->(){return _ptr;}~shared_ptr(){if (--(*_pcount) == 0 && _ptr) {delete _pcount;delete _ptr;}}private:T* _ptr;int* _pcount;};

我们把 这个 计数器 建在堆上,这样就可以保证各个对象之间保持同步同时计数正确。

-拷贝构造

在这里插入图片描述

  • 赋值拷贝

赋值拷贝需要注意两点:

  1. 在被赋值之前的对象需要将自己析构,也就是放弃当前资源的管理权,然后再去被赋值,取得新的管理权。
  2. 避免自己对自己赋值,按照1中的机制,如果自己对自己赋值,会造成无谓的操作,或者误析构资源。

在这里插入图片描述
其他写法:

		shared_ptr<T>& operator=(shared_ptr<T> sp){swap(_ptr, sp._ptr);swap(_pcount, sp._pcount);return *this;}

但是,此时我们的shared_ptr 还面临着 线程安全的问题。

这里我们需要保障的是对于 计数器的 ++ 和 – 造成的线程不安全。对于资源的线程安全问题,这不是智能指针保证的部分。

template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex){}void add_ref(){_pmtx->lock();++(*_pcount);_pmtx->unlock();}void release_ref(){bool flag = false;_pmtx->lock();if (--(*_pcount) == 0 && _ptr) {delete _pcount;delete _ptr;flag = true;cout << "释放资源:" << _ptr << endl;}_pmtx->unlock();if (flag)delete _pmtx;}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount),_pmtx(sp._pmtx){add_ref();}shared_ptr<T>& operator = (const shared_ptr<T>& sp){if (_ptr != sp._ptr) {if (--(*_pcount) == 0){delete _pcount;delete _ptr;}_ptr = sp._ptr;_pcount = sp._pcount;add_ref();}return *this;}T& operator *(){return *_ptr;}T* operator ->(){return _ptr;}T* get(){return _ptr;}int use_count(){return *_pcount;}~shared_ptr(){release_ref();}private:T* _ptr;int* _pcount;mutex* _pmtx;};

3.2.3.3 定制删除器

不管是我们自己实现的shared_ptr还是库中的shared_ptr,我们在析构的时候默认都是 delete _ptr,如果我们托管的类型是 new T[] ,或者 malloc出来的话,就导致类型不是匹配的,无法析构。

为此,shared_ptr提供了 定制删除器,我们可以在构造的时候作为参数传入。如果我们不传参,就默认使用delete
在这里插入图片描述

这里举两个例子

template<class T>struct DeleteArray{void operator()(T* ptr){delete[]ptr;}};void test_deletor(){DeleteArray<string>da; //使用仿函数定制std::shared_ptr<string>s2(new string[10], da);std::shared_ptr<string>s3((string*)malloc(sizeof(string)),[](string* ptr) {free(ptr); }); //使用lamdba 定制}

如果我们也想自己是想一下呢?

当然是可以的,但是由于我们的实现比库中的简单很多(库中使用多个类),所以我们难以通过传参的方式来定制删除器,我们增加一个模板参数,通过向模板传参来达到相同的目的。

std的框架设计底层用一个类专门管理资源计数,所以它们可以在构造函数传参,把删除器类型传递给专门管理资源的这个类。而我们是一体化的。

template<class T>struct DefaultDel{void operator ()(T* ptr){delete ptr;}};template<class T,class D=DefaultDel<T>> //增加模板参数class shared_ptr{public:explicit shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex){}void add_ref(){_pmtx->lock();++(*_pcount);_pmtx->unlock();}void release_ref(){bool flag = false;_pmtx->lock();if (--(*_pcount) == 0 && _ptr) {//定制化删除D del;del(_ptr);delete _pcount;flag = true;cout << "释放资源:" << _ptr << endl;}_pmtx->unlock();if (flag)delete _pmtx;}shared_ptr(const shared_ptr<T,D>& sp):_ptr(sp._ptr),_pcount(sp._pcount),_pmtx(sp._pmtx){add_ref();}shared_ptr<T,D>& operator = (const shared_ptr<T,D>& sp){if (_ptr != sp._ptr) {if (--(*_pcount) == 0){delete _pcount;delete _ptr;}_ptr = sp._ptr;_pcount = sp._pcount;add_ref();}return *this;}T& operator *(){return *_ptr;}T* operator ->(){return _ptr;}T* get(){return _ptr;}int use_count(){return *_pcount;}~shared_ptr(){release_ref();}private:T* _ptr;int* _pcount;mutex* _pmtx;};

此时我们要这样使用:

template<class T>struct DeleteArray{void operator()(T* ptr){delete[]ptr;}};void test_deletor(){//使用lamdba 定制DeleteArray<string>da; std::shared_ptr<string,DeleteArray<string>>s2(new string[10);//使用lamdba 定制auto ffree =  [](string* ptr) {free(ptr); };std::shared_ptr<string,decltype(ffree)>s3((string*)malloc(sizeof(string)));auto fclose =  [](FILE* ptr) {fclose(ptr); };std::shared_ptr<string,decltype(ffcolse)>s3(fopen("test.cpp","r"));}

3.2.4 weak_ptr

虽然 shared_ptr 确实已经是一个不错的设计了,但是没有“十全十美”的东西,在一些特别的场景之下shared_ptr 也无能为力:

  • shared_ptr 的循环引用
    我们看下面的场景,我们运行发现,两个节点n1.n2 都没有析构。
    在这里插入图片描述

为什么会发生这种情况呢?
在这里插入图片描述

在出了作用域之后,首先把 n1,n2 两个对象析构,此时两边计数器均减为1,那么左边节点资源什么时候析构呢, 当n2->prev析构,也就是当右边节点资源析构,那么右边节点资源什么时候析构呢,当n1->_next析构,也就是当左边节点资源析构…我们发现,此时形成了一个类似于“死锁”的情况。


此时我们就要使用 weak_ptr 来解决 循环引用。

weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,是为了解决循环引用而生的,为什么这么说呢,我们可以看看它的构造函数:

我们只能使用 wek_ptr或者 shared_ptr 去初始化它。
在这里插入图片描述

我们在会产生循环引用的位置,把shared_ptr换成weak_ptr。 weak_ptr 不是一个RALL智能指针,它不参与资源的管理,他是专门用来解决引用计数的,我们可以使用一个shared_ptr 来初始化一个weak_ptr,但是weak_ptr 不增加引用计数,不参与管理,但是也像指针一样访问修改资源。

在这里插入图片描述

我们可以自己实现一个weak_ptr:

template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(shared_ptr<T>& sp):_ptr(sp.get()),_pcount(sp.use_count()){}weak_ptr(weak_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){}weak_ptr& operator = (shared_ptr<T>& sp){_ptr = sp.get();_pcount = sp.use_count();return *this;}weak_ptr& operator = (weak_ptr<T>& sp){_ptr = sp._ptr;_pcount = sp._pcount;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count(){return *_pcount;}private:T* _ptr;int* _pcount;};


http://chatgpt.dhexx.cn/article/CQQAW9Ay.shtml

相关文章

智能指针详解

目录 前言 1、为什么需要智能指针&#xff1f; 2、智能指针的原理 3、智能指针的分类 3.1 auto_ptr 3.2 unique_ptr 3.3 shared_ptr 前言 C11中引入了智能指针的特性&#xff0c;本文将详细介绍智能指针的使用。 1、为什么需要智能指针&#xff1f; 我们来看一段代码&…

【c++复习笔记】——智能指针详细解析(智能指针的使用,原理分析)

&#x1f482; 个人主页:努力学习的少年&#x1f91f; 版权: 本文由【努力学习的少年】原创、在CSDN首发、需要转载请联系博主&#x1f4ac; 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦 目录 一. 智能指针的基本概念 二. 智能指针的定义和使用 三. a…

C++ 智能指针 - 全部用法详解

为什么要学习智能指针&#xff1f; 咳咳&#xff0c;这个问题不是问大家的&#xff0c;是询问我自己的&#xff01; 我依稀记得刚离校出来找实习工作那会儿(2020年7月)&#xff0c;去面试一份工作&#xff0c;面试官问了我许多问题&#xff0c;其中有一个问题就是问&#xff1a…

AndroidStudio编译时 CXX1405 错误解决

AndroidStudio 编译带C的native库项目时报错&#xff0c; [CXX1405] error when building with cmake using D:\WorkSpace\AndroidXXXX\app\src\main\cpp\CMakeLists.txt: Build command failed.解决方法&#xff1a; cmd 到 Android SDK的cmake路径下&#xff0c;执行 cmake …

硬件速攻-AT24CXX存储器

AT24C02是什么&#xff1f; AT24CXX是存储芯片&#xff0c;驱动方式为IIC协议 实物图&#xff1f; 引脚介绍&#xff1f; A0 地址设置角 可连接高电平或低电平 A1 地址设置角 可连接高电平或低电平 A2 地址设置角 可连接高电平或低电平 1010是设备前四位固定地址 &#xf…

【dbus-cxx】libsigc++ 和 dbus-cxx 在 Ubuntu 中的编译和配置

文章目录 参考资料配置环境cmakelibsigcdbus-cxx 例程server.cppclient.cppMakefile运行结果 此帖作为自己学习记录使用&#xff0c;尚未使用交叉编译&#xff0c;仅在本地使用 参考资料 需先了解DBUS基础知识&#xff0c;建议看DBUS官方文档 DBUS-CXX也是建议看官方文档&…

正点原子IIC例程讲解笔记(三)——24cxx.c中函数理解

目录 一、24C02 简介 二、在 AT24CXX 指定地址写入一个数据&#xff1a; 三、在ATC24XX指定地址读出一个数据 四、检查AT24CXX是否正常&#xff1a;u8 AT24CXX_Check(void) 五、在 AT24CXX 里面的指定地址开始写入长度为 Len 的数据 六、在 AT24CXX 里面的指定地址开始读出…

windows用VS2019下编译log4cxx日志库

一、下载相关库文件 获取log4cxx源码包&#xff1a;http://logging.apache.org/log4cxx/index.html 获取依赖库apr和apr-util源码包:http://archive.apache.org/dist/apr/apr-1.2.11-win32-src.zip http://archive.apache.org/dist/apr/apr-util-1.2.10-win32-src.zip 编译apr…

log4cxx编译

本人进行过win7 64位操作系统和win10家庭版的log4cxx编译&#xff0c;使用的是vs2015&#xff0c;下面是详情。 1.sed下载 sed-4.2.1-bin.zip、sed-4.2.1-dep.zip下载地址&#xff1a;http://gnuwin32.sourceforge.net/packages/sed.htm 下载后&#xff0c;将sed的两个压缩包解…

【RT-Thread Master】at24cxx软件包使用笔记

硬件介绍 RT-Thread版本&#xff1a;V4.1.0软件包名称&#xff1a;at24cxxMCU型号&#xff1a;AT32F407VET7EEPROM型号&#xff1a;AT24C16 使用说明 1、使用menuconfig将软件包添加进入工程&#xff0c;路径如下所示。 2、把IIC总线打开&#xff0c;这里使用软件IIC&#…

linux下编译和安装log4cxx,ubuntu下log4cxx安装使用

需要安装log4cxx&#xff0c;安装的过程中可是充满了坎坷。。。最大的问题是在make log4cxx时&#xff0c;总是报undefined XML什么什么的错误&#xff0c;查了一下也没解决了&#xff0c;然后把apr-utils删了重新装了一下就好了。。 log4cxx现在是apache的一个项目&#xff0c…

linux下编译和安装log4cxx,RedHat如何安装log4cxx日志库

log4cxx日志库是一种动态库&#xff0c;用于记录c的日志&#xff0c;那么RedHat系统下要如何安装log4cxx日志库呢&#xff1f;下面小编就给大家介绍下RedHat安装log4cxx日志库的步骤&#xff0c;感兴趣的朋友不妨来了解下吧。 首先&#xff0c;我得到信息&#xff0c;安装这个库…

AT24Cxx读写全面理解

AT24Cxx - 电可擦可写E2PROM 芯片介绍 基础介绍\引脚介绍 AT24Cxx系列EEPROM是由美国Mcrochip公司出品&#xff0c;1-512K位的支持I2C总线数据传送协议的串行CMOS E2PROM&#xff0c;可用电擦除&#xff0c;可编程自定时写周期&#xff08;包括自动擦除时间不超过10ms&#…

mongodb-cxx-driver使用

mongocxx driver 是构建在 MongoDB C driver 之上的 1.首先需要安装mongo-c-driver wget https://github.com/mongodb/mongo-c-driver/releases/download/ 1.23.1/mongo-c-driver-1.23.1.tar.gz tar xzf mongo-c-driver-1.23.1.tar.gz cd mongo-c-driver-1.23.1 mkdir cmak…

老胡的周刊(第095期)

老胡的信息周刊[1]&#xff0c;记录这周我看到的有价值的信息&#xff0c;主要针对计算机领域&#xff0c;内容主题极大程度被我个人喜好主导。这个项目核心目的在于记录让自己有印象的信息做一个留存以及共享。 &#x1f3af; 项目 tabby[2] 自托管的 AI 编码助手&#xff0c;…

程序员养生指北

吴小胖第八次推送 阅读时间预计3分钟~ 熬夜篇 互联网人熬夜是不能避免的&#xff0c;原因却各不相同。 不加班的时候&#xff0c;总会对自己说&#xff0c;今天一定早睡&#xff0c;然鹅... 午休篇 熬夜的程序员总想依靠午休补觉&#xff0c;然鹅... 更不幸的是&#xff0c;互联…

老杨说运维 | 中国IT运维市场的现状与趋势

文章内容来源《第一新声》 对擎创科技CEO杨辰(老杨)的专访 前言&#xff1a; 中国目前正面临百年未有之大变局&#xff0c;在这个变局中&#xff0c;不稳定性和不确定性正在增强。疫情持续反复、国际形势变化多端&#xff0c;导致国内多个行业出现发展增速下降、产供销节奏打…

老杨说运维 | 非常重要,事关转型

《荀子》有云&#xff1a;“水能载舟&#xff0c;亦能覆舟。”在公司日常运营过程中&#xff0c;数据指标就像是水&#xff0c;孕育着生命&#xff0c;承载着万物。科学的数据指标能指引公司在正确的道路上不断前进&#xff0c;使平淡无常的业务焕发新生&#xff0c;而不合理的…

学习springcloud的一些心得体会——老卫的天气预报系统

1&#xff1a;建立天气预报springboot系统 首先先建立一个天气预报的springboot系统&#xff0c;具体流程如下&#xff1a; &#xff08;1&#xff09;从cityList.xml中获取城市信息&#xff0c; &#xff08;2&#xff09;然后根据下面的链接获取各个城市的天气预报信息&am…

学习springboot项目的一些心得-----老卫的博客系统

去年年底接触了springboot框架&#xff0c;这两天复习了一遍&#xff0c;主要是跟着老卫博客系统这个课程学习的。 springboot介绍&#xff1a;总的感觉springboot就是基于spring开发的一套框架&#xff0c;好处就是不用配置复杂的依赖包&#xff0c;xml的一些文件&#xff0c…