C++智能指针的原理、分类、使用

news/2024/5/17 12:04:24/文章来源:https://blog.csdn.net/qq_38967414/article/details/130623161

1. 智能指针介绍

为解决裸指针可能导致的内存泄漏问题。如:

        a)忘记释放内存;

        b)程序提前退出导致资源释放代码未执行到。

就出现了智能指针,能够做到资源的自动释放


2. 智能指针的原理和简单实现

2.1 智能指针的原理

将裸指针封装为一个智能指针类,需要使用该裸指针时,就创建该类的对象;利用栈区对象出作用域会自动析构的特性,保证资源的自动释放。

2.2 智能指针的简单实现

代码示例:

template<typename T>
class MySmartPtr {
public:MySmartPtr(T* ptr = nullptr):mptr(ptr) { // 创建该对象时,裸指针会传给对象}~MySmartPtr() {  // 对象出作用域会自动析构,因此会释放裸指针指向的资源delete mptr;}// *运算符重载T& operator*() {  // 提供智能指针的解引用操作,即返回它包装的裸指针的解引用return *mptr; }// ->运算符重载T* operator->() { // 即返回裸指针return mptr;}
private:T* mptr;
};class Obj {
public:void func() {cout << "Obj::func" << endl;}
};void test01() {/*创建一个int型的裸指针,使用MySmartPtr将其封装为智能指针对象ptr,ptr对象除了作用域就会自动调用析构函数。智能指针就是利用栈上对象出作用域自动析构这一特性。*/MySmartPtr<int> ptr0(new int);*ptr0 = 10;MySmartPtr<Obj> ptr1(new Obj);ptr1->func();(ptr1.operator->())->func(); // 等价于上面/*  中间异常退出,智能指针也会自动释放资源。if (xxx) {throw "....";}if (yyy) {return -1;}*/
}

3. 智能指针分类

3.1 问题引入

 接着使用上述自己实现的智能指针进行拷贝构造:

void test02() {MySmartPtr<int> p1(new int); // p1指向一块int型内存空间MySmartPtr<int> p2(p1);      // p2指向p1指向的内存空间*p1 = 10;   // 内存空间的值为10*p2 = 20;   // 内存空间的值被改为20
}

但运行时出错:

原因在于p1和p2指向同一块int型堆区内存空间,p2析构将该int型空间释放,p1再析构时释放同一块内存,则出错。

那可否使用如下深拷贝解决该问题?

MySmartPtr(cosnt MySmartPtr<T>& src) {mptr = new T(*src.mptr);
}

不可以。因为按照裸指针的使用方式,用户本意是想将p1和p2都指向该int型堆区内存,使用指针p1、p2都可改变该内存空间的值,显然深拷贝不符合此场景。


3.2 两类智能指针

不带引用计数的智能指针:只能有一个指针管理资源。

        auto_ptr;

        scoped_ptr;

        unique_ptr;.

带引用计数的智能指针:可以有多个指针同时管理资源。

        shared_ptr;强智能指针。

        weak_ptr: 弱智能指针。这是特例,不能控制资源的生命周期,不能控制资源的自动释放!


3.3 不带引用计数的智能指针

只能有一个指针管理资源。

3.3.1 auto_ptr (不推荐使用)

void test03() {auto_ptr<int> ptr1(new int);auto_ptr<int> ptr2(ptr1);*ptr2 = 20;// cout << *ptr2 << endl; // 可访问*ptr2cout << *ptr1 << endl; //访问*ptr1却报错
}

如上代码,访问*ptr1为何报错?

因为调用auto_ptr的拷贝构造将ptr1的值赋值给ptr2后,底层会将ptr1指向nullptr;即将同一个指针拷贝构造多次时,只让最后一次拷贝的指针管理资源,前面的指针全指向nullptr

不推荐将auto_ptr存入容器。

3.3.2 scoped_ptr (使用较少)

scoped_ptr已将拷贝构造函数赋值运算符重载delete了。

scoped_ptr(const scoped_ptr<T>&) = delete; // 删除拷贝构造
scoped_ptr<T>& operator=(const scoped_ptr<T>&) = delete;  // 删除赋值重载

3.3.3 unique_ptr (推荐使用)

unique_ptr也已将拷贝构造函数赋值运算符重载delete

unique_ptr(const unique_ptr<T>&) = delete; // 删除拷贝构造
unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;  // 删除赋值重载

unique_ptr提供了右值引用参数的拷贝构造函数赋值运算符重载,如下:

void test04() {unique_ptr<int> ptr1(new int);// unique_ptr<int> ptr2(ptr1);  和scoped_ptr一样无法通过编译unique_ptr<int> ptr2(std::move(ptr1)); // 但可使用move得到ptr1的右值类型// *ptr1  也无法访问
}

3.4 带引用计数的智能指针

可以有多个指针同时管理资源。

原理:给智能指针添加其指向资源的引用计数属性,若引用计数 > 0,则不会释放资源,若引用计数 = 0就释放资源。

具体来说:额外创建资源引用计数类,在智能指针类中加入该资源引用计数类的指针作为其中的一个属性;当使用裸指针创建智能指针对象时,创建智能指针中的资源引用计数对象,并将其中的引用计数属性初始化为1,当后面对该智能指针对象进行拷贝(使用其他智能指针指向该资源时)或时,需要在其他智能指针对象类中将被拷贝的智能指针对象中的资源引用计数类的指针获取过来,然后将引用计数+1;当用该智能指针给其他智能指针进行赋值时,因为其他智能指针被赋值后,它们就不指向原先的资源了,原先资源的引用计数就-1,直至引用计数为0时delete掉资源;当智能指针对象析构时,会使用其中的资源引用计数指针将共享的引用计数-1,直至引用计数为0时delete掉资源。

shared_ptr:强智能指针;可改变资源的引用计数

weak_ptr:弱智能指针;不可改变资源的引用计数

带引用计数的智能指针的简单实现:

/*资源的引用计数类*/
template<typename T>
class RefCnt {
public:RefCnt(T* ptr=nullptr):mptr(ptr) {if (mptr != nullptr) {mcount = 1; // 刚创建指针指针时,引用计数初始化为1}}void addRef() {  // 增加引用计数mcount++;}int delRef() {   // 减少引用计数mcount--;return mcount;}
private:T* mptr;  // 资源地址int mcount; // 资源的引用计数
};/*智能指针类*/
template<typename T>
class MySmartPtr {
public:MySmartPtr(T* ptr = nullptr) :mptr(ptr) { // 创建该对象时,裸指针会传给对象mpRefCnt = new RefCnt<T>(mptr);}~MySmartPtr() {  // 对象出作用域会自动析构,因此会释放裸指针指向的资源if (0 == mpRefCnt->delRef()) {delete mptr;mptr = nullptr;}}// *运算符重载T& operator*() {  // 提供智能指针的解引用操作,即返回它包装的裸指针的解引用return *mptr;}// ->运算符重载T* operator->() { // 即返回裸指针return mptr;}// 拷贝构造MySmartPtr(const MySmartPtr<T>& src):mptr(src.mptr),mpRefCnt(src.mpRefCnt) {if (mptr != nullptr) {mpRefCnt->addRef();}}// 赋值重载MySmartPtr<T>& operator=(const MySmartPtr<T>& src) {if (this == &src) // 防止自赋值return *this;/*若本指针改为指向src管理的资源,则本指针原先指向的资源的引用计数-1,若原资源的引用计数为0,就释放资源*/if (0 == mpRefCnt->delRef()) {  delete mptr;}mptr = src.mptr;mpRefCnt = src.mpRefCnt;mpRefCnt->addRef();return *this;}
private:T* mptr;  // 指向资源的指针RefCnt<T>* mpRefCnt; // 资源的引用计数
};

3.4.1 shared_ptr

强智能指针。可改变资源的引用计数。

(1)强智能指针的交叉引用问题

class B;class A {
public:A() {cout << "A()" << endl;}~A() {cout << "~A()" << endl;}shared_ptr<B> _ptrb;
};class B {
public:B() {cout << "B()" << endl;}~B() {cout << "~B()" << endl;}shared_ptr<A> _ptra;
};void test06() {shared_ptr<A> pa(new A());shared_ptr<B> pb(new B());pa->_ptrb = pb;pb->_ptra = pa;/*打印pa、pb指向资源的引用计数*/cout << pa.use_count() << endl;cout << pb.use_count() << endl;
}

输出结果:

 

可见pa、pb指向的资源的引用计数都为2,因此出了作用域导致pa、pb指向的资源都无法释放,如下图所示:

解决: 

建议定义对象时使用强智能指针,引用对象时使用弱智能指针,防止出现交叉引用的问题。

什么是定义对象?什么是引用对象?

定义对象:

        使用new创建对象,并创建一个新的智能指针管理它。

引用对象:

        使用一个已存在的智能指针来创建一个新的智能指针。

        定义对象和引用对象的示例如下:

shared_ptr<int> p1(new int());              // 定义智能指针对象p1
shared_ptr<int> p2 = make_shared<int>(10);  // 定义智能指针对象p2shared_ptr<int> p3 = p1;  // 引用智能指针p1,并使用p3来共享它
weak_ptr<int> p4 = p2;    // 引用智能指针p2,并使用p4来观察它

如上述代码,因为在test06函数中使用pa对象的_ptrb引用pb对象,使用pb对象的_ptra引用pa对象,因此需要将A类、B类中的_ptrb_ptra的类型改为弱智能指针weak_ptr即可,这样就不会改变资源的引用计数,能够正确释放资源。

3.4.2 weak_ptr 

弱智能指针。不可改变资源的引用计数。不能创建对象,也不能访问资源(因为weak_ptr未提供operator->operator*重载运算符),即不能通过弱智能指针调用函数、不能将其解引用。只能从一个已有的shared_ptr或weak_ptr获得资源的弱引用。

弱智能指针weak_ptr若想用访问资源,则需要使用lock方法将其提升为一个强智能指针,提升失败则返回nullptr。(提升的情形常使用于多线程环境,避免无效的访问,提升程序安全性)

注意:弱智能指针weak_ptr只能观察资源的状态,但不能管理资源的生命周期,不会改变资源的引用计数,不能控制资源的释放。

weak_ptr示例:

void test07() {shared_ptr<Boy> boy_sptr(new Boy());weak_ptr<Boy> boy_wptr(boy_sptr);// boy_wptr->study(); 错误!无法使用弱智能指针访问资源cout << boy_sptr.use_count() << endl; // 引用计数为1,因为弱智能指针不改变引用计数shared_ptr<int> i_sptr(new int(99));weak_ptr<int> i_wptr(i_sptr);// cout << *i_wptr << endl; 错误!无法使用弱智能指针访问资源cout << i_sptr.use_count() << endl; // 引用计数为1,因为弱智能指针不改变引用计数/*弱智能指针提升为强智能指针*/shared_ptr<Boy> boy_sptr1 = boy_wptr.lock();if (boy_sptr1 != nullptr) {cout << boy_sptr1.use_count() << endl; // 提升成功,引用计数为2boy_sptr1->study(); // 可以调用}shared_ptr<int> i_sptr1 = i_wptr.lock();if (i_sptr1 != nullptr) {cout << i_sptr1.use_count() << endl; // 提升成功,引用计数为2cout << *i_sptr1 << endl; // 可以输出}  
}

4. 智能指针与多线程访问共享资源的安全问题

有如下两种启动线程的方式:

方式1:主线程调用test08函数,在test08函数中启动子线程执行线程函数,如下:

void handler() {cout << "Hello" << endl;
}void func() {thread t1(handler);
}int main(int argc, char** argv) {func();this_thread::sleep_for(chrono::seconds(1));system("pause");return 0;
}

运行报错:

 方式2:主线程中直接创建子线程来执行线程函数,如下:

void handler() {cout << "Hello" << endl;
}int main(int argc, char** argv) {thread t1(handler);this_thread::sleep_for(chrono::seconds(1));system("pause");return 0;
}

运行结果:无报错

 

上面两种方式旨在通过子线程调用函数输出Hello,但为什么方式1就报错了?很简单,不再赘述。


回归本节标题的正题,有如下程序:

class C {
public:C() {cout << "C()" << endl;}~C() {cout << "~C()" << endl;}void funcC() {cout << "C::funcC()" << endl;}
private:};/*子线程执行函数*/
void threadHandler(C* c) {this_thread::sleep_for(chrono::seconds(1));c->funcC();
}/* 主线程 */
int main(int argc, char** argv) {C* c = new C();thread t1(threadHandler, c);delete c;t1.join();return 0;
}

运行结果:

结果显示c指向的对象被析构了,但是仍然使用该被析构的对象调用了其中的funcC函数,显然不合理。

因此在线程函数中,使用c指针访问A对象时,需要观察A对象是否存活

使用弱智能指针weak_ptr接收对象,访问对象之前尝试提升为强智能指针shared_ptr,提升成功则访问,否则对象被析构。

情形1:对象被访问之前就被析构了:

class C {
public:C() {cout << "C()" << endl;}~C() {cout << "~C()" << endl;}void funcC() {cout << "C::funcC()" << endl;}
private:};/*子线程执行函数*/
void threadHandler(weak_ptr<C> pw) {  // 引用时使用弱智能指针this_thread::sleep_for(chrono::seconds(1));shared_ptr<C> ps = pw.lock();  // 尝试提升if (ps != nullptr) {ps->funcC();} else {cout << "对象已经析构!" << endl;}
}/* 主线程 */
int main(int argc, char** argv) {{shared_ptr<C> p(new C());thread t1(threadHandler, weak_ptr<C>(p));t1.detach();}this_thread::sleep_for(chrono::seconds(5));return 0;
}

运行结果:

情形2: 对象访问完才被析构:

class C {
public:C() {cout << "C()" << endl;}~C() {cout << "~C()" << endl;}void funcC() {cout << "C::funcC()" << endl;}
private:};/*子线程执行函数*/
void threadHandler(weak_ptr<C> pw) {  // 引用时使用弱智能指针this_thread::sleep_for(chrono::seconds(1));shared_ptr<C> ps = pw.lock();  // 尝试提升if (ps != nullptr) {ps->funcC();} else {cout << "对象已经析构!" << endl;}
}/* 主线程 */
int main(int argc, char** argv) {{shared_ptr<C> p(new C());thread t1(threadHandler, weak_ptr<C>(p));t1.detach();this_thread::sleep_for(chrono::seconds(5));}  return 0;
}

运行结果:

 很显然shared_ptr与weak_ptr结合使用,能够较好地保证多线程访问共享资源的安全。


5.智能指针的删除器deleter

删除器是智能指针释放资源的方式,默认使用操作符delete来释放资源。

但并非所有智能指针管理的资源都可通过delete释放,如数组、文件资源、数据库连接资源等。

有如下智能指针对象管理一个数组资源:

unique_ptr<int> ptr1(new int[100]);

此时再用默认的删除器则会造成资源泄露。因此需要自定义删除器。

/* 方式1:类模板 */
template<typename T>
class MyDeleter {
public:void operator()(T* ptr) const {cout << "数组自定义删除器1." << endl;delete[] ptr;}
};/* 方式2:函数 */
void myDeleter(int* p) {cout << "数组自定义删除器2." << endl;delete[] p;
}void test09() {unique_ptr<int, MyDeleter<int>> ptr1(new int[100]);unique_ptr<int, void(*)(int*)> ptr2(new int[100], myDeleter);/* 方式3:Lambda表达式 */unique_ptr<int, void(*)(int*)> ptr3(new int[100], [](int* p) {cout << "数组自定义删除器3." << endl;delete[] p;});
}void test10() {unique_ptr<FILE, void(*)(FILE*)> ptr2(fopen("1.txt", "w"), [](FILE* f) {cout << "文件自定义删除器." << endl;fclose(f);});
}

运行结果:

 待补充

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_488549.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL笔记(四) 函数、变量、存储过程、游标、索引、存储引擎、数据库维护、指定字符集、锁机制

MySQL笔记&#xff08;四&#xff09; 文章目录 MySQL笔记&#xff08;四&#xff09;函数文本处理函数日期和时间处理函数数值处理函数类型转换函数流程控制函数自定义函数基本语法 局部变量全局变量聚集函数 aggregate functionDISTINCT 存储过程为什么要使用使用创建 删除建…

Rust Wasm Linux开发环境搭建

一、Linux 镜像版本 CentOS-7-x86_64-DVD-2009.iso&#xff0c;Virtual Box 7.0 选择 GNOME Desktop 版本&#xff0c; 配置远程连接&#xff08;可选&#xff09;&#xff0c; nmtui 激活连接 enp0s3 &#xff0c;查看 ip 地址&#xff0c; 绑定端口转发&#xff0c; 通过…

JQuery 详细教程

文章目录 一、JQuery 对象1.1 安装和使用1.2 JQuery包装集对象 二、JQuery 选择器2.1 基础选择器2.2 层次选择器2.3 表单选择器 三、JQuery Dom 操作3.1 操作元素3.1.1 操作属性3.1.2 操作样式3.1.3 操作内容 3.2 添加元素3.3 删除元素3.4 遍历元素 四、JQuery 事件4.1 ready 加…

PBR核心理论与渲染原理

基于物理的渲染&#xff08;Physically Based Rendering&#xff0c;PBR&#xff09;是指使用基于物理原理和微平面理论建模的着色/光照模型&#xff0c;以及使用从现实中测量的表面参数来准确表示真实世界材质的渲染理念。 以下是对PBR基础理念的概括&#xff1a; 微平面理论…

Android View 事件分发机制,看这一篇就够了

在 Android 开发当中&#xff0c;View 的事件分发机制是一块很重要的知识。不仅在开发当中经常需要用到&#xff0c;面试的时候也经常被问到。 如果你在面试的时候&#xff0c;能把这块讲清楚&#xff0c;对于校招生或者实习生来说&#xff0c;算是一块不错的加分项。对于工作…

对STM32栈的理解Stack_Size EQU 0x00000400

对STM32栈的理解Stack_Size EQU 0x00000400 Stack_Size EQU 0x00000400表示什么意思可以通过查找flash内存的方式定位存储1.flash2.RAM内部 本人主要为个人参考网络及个人总结而来比较&#xff0c;如有雷同请告知&#xff0c;即刻删除 以下引用网上资料 理解堆和栈的区别 &…

PFCdocumentation_FISH Rules and Usage

目录 FISH Scripting FISH Rules and Usage Lines Data Types Reserved Names for Functions and Variables Scope of Variables Functions: Structure, Evaluation, and Calling Scheme Arithmetic: Expressions and Type Conversions Redefining FISH Functions Ex…

中断相关内容大全

中断基本概念&#xff1a;程序中断指计算机执行现行程序过程中&#xff0c;出现某种急需处理的异常情况或特殊请求&#xff0c;CPU暂时中止现行程序&#xff0c;而转去对这些异常情况或特殊请求进行处理&#xff0c;处理完毕后CPU又自动返回到现行程序的断点处&#xff0c;继续…

算法修炼之练气篇——练气十七层

博主&#xff1a;命运之光 专栏&#xff1a;算法修炼之练气篇 前言&#xff1a;每天练习五道题&#xff0c;炼气篇大概会练习200道题左右&#xff0c;题目有C语言网上的题&#xff0c;也有洛谷上面的题&#xff0c;题目简单适合新手入门。&#xff08;代码都是命运之光自己写的…

【最新可用】chatGPT镜像网站国内使用,免费稳定!

新建了一个网站 https://ai.weoknow.com/ 每天给大家更新可用的国内可用chatGPT 2023.5.8新增一个 ChatGPT 国内免翻版 【网站名称】&#xff1a;Chat GPT Ai 【使用环境】&#xff1a;移动端/电脑网页端 ChatGPT是一款功能强大的免费在线聊天机器人&#xff0c;具有人工智能…

JavaScript通过js的方式来计算平行四边形的面积的代码

以下为通过js的方式来计算平行四边形的程序代码和运行截图 目录 前言 一、通过js的方式来计算平行四边形&#xff08;html部分&#xff09; 1.1 运行流程及思想 1.2 代码段 二、通过js的方式来计算平行四边形&#xff08;js部分&#xff09; 2.1 运行流程及思想 2.2 代码…

C#中如何使用ObjectPool来提高StringBuilder的性能

在C#中我们知道使用StringBuilder能提高大量字符串拼接的效率&#xff0c;其实StringBuilder的效率也可以提升&#xff0c;那就是使用ObjectPool。以下介绍怎么使用ObjectPool提高StringBuilder的性能。一、简介 C# ObjectPool类是一个内置的类库&#xff0c;用于实现对象…

AFG1062任意波形/函数发生器 产品资料

AFG1000 任意波形/函数发生器&#xff0c;提供 25MHz 或 60MHz 带宽&#xff0c;2 个输出通道&#xff0c;在整个带宽内 1mVpp 到 10Vpp 输出振幅&#xff0c;泰克 AFG1000 任意波形/函数发生器可以生成各种实验室测试所需波形。 *重要的是&#xff0c;它在泰克任意函数发生器系…

基于知识图谱的个性化学习资源推荐系统的设计与实现(论文+源码)_kaic

摘 要 最近几年来&#xff0c;伴随着教育信息化、个性化教育和K12之类的新观念提出,一如既往的教育方法向信息化智能化的转变&#xff0c;学生群体都对这种不受时间和地点约束的学习方式有浓厚的兴趣。而现在市面上存在的推荐系统给学生推荐资料时不符合学生个人对知识获取的…

小曾同学【五周年创作纪念日】——努力向前冲的菜鸟

&#x1f604;作者简介&#xff1a; 小曾同学.com,一个致力于测试开发的博主⛽️&#xff0c; 如果文章知识点有错误的地方&#xff0c;还请大家指正&#xff0c;让我们一起学习&#xff0c;一起进步。&#x1f60a; 座右铭&#xff1a;不想当开发的测试&#xff0c;不是一个好…

自主可控不走捷径,中国长城做难且正确的事

2020-2022年是中国信创产业的重要推广期&#xff0c;在国家战略的支持下&#xff0c;自主可控领域诸多相关企业均获得绝佳发展良机。 但信创产业“完成替代”不是终点&#xff0c;“实现领先”方是目标。如今势已启、路尚远&#xff0c;前景广阔的市场并不意味着自主可控相关企…

Hadoop之block切片

切片是一个逻辑概念 在不改变现在数据存储的情况下&#xff0c;可以控制参与计算的节点数目 通过切片大小可以达到控制计算节点数量的目的 有多少个切片就会执行多少个Map任务 hdfs上数据存储的一个单元,同一个文件中块的大小都是相同的 因为数据存储到HDFS上不可变&#xff0…

Lucene(1):Lucene介绍

Lucene官网&#xff1a; http://lucene.apache.org/ 1 搜索技术理论基础 1.1 lucene优势 原来的方式实现搜索功能&#xff0c;我们的搜索流程如下图&#xff1a; 上图就是原始搜索引擎技术&#xff0c;如果用户比较少而且数据库的数据量比较小&#xff0c;那么这种方式实现搜…

2路 QSFP,40G 光纤的数据实时采集(5GByte/s 带宽)板卡设计原理图 -PCIE732

板卡概述 PCIE732 是一款基于 PCIE 总线架构的高性能数据传输卡&#xff0c;板卡具有 1 个 PCIex8 主机接口、2 个 QSFP40G 光纤接口&#xff0c;可以实现 2 路 QSFP 40G 光纤的数据实时采集、传输。板卡采用 Xilinx 的高性 能 Kintex UltraScale 系列 FPGA 作为实时处理器…

小程序开发中的插件、组件、控件到底有什么区别?

小程序插件代码由一些自定义组件和 JS 代码文件构成&#xff0c;插件开发者在发布插件时&#xff0c;这些代码被上传到后台保存起来。当小程序使用插件时&#xff0c;使用者需填写插件的 AppID 和版本号&#xff0c;就可从后台获取相应的插件代码。小程序代码编译时&#xff0c…