智能指针基础知识【C++】【RAII思想 || unique_ptr || shared_ptrweak_ptr || 循环引用问题】

本文主要是介绍智能指针基础知识【C++】【RAII思想 || unique_ptr || shared_ptrweak_ptr || 循环引用问题】,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

一,为什么需要智能指针

二,内存泄露的基本认识

1. 内存泄露分类

2. 常见的内存检测工具

3,如何避免内存泄露

三,智能指针的使用与原理

1. RAII思想

2. 智能指针

(1. unique_ptr

(2. shared_ptr & weak_ptr

 shared_ptr的循环引用问题

3. weak_ptr解决循环引用问题

4. 定制删除器(了解)


嗨!收到一张超美的风景图,愿你每天都能顺心! 

一,为什么需要智能指针

假设我们调用func函数,我们会发现:div函数操作,如果抛异常,则p1, p2就不会释放导致内存泄露。

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 ;
}

二,内存泄露的基本认识

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

1. 内存泄露分类

C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap leak)(本章关注点)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

系统资源泄漏
指程序使用 系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

2. 常见的内存检测工具

linux下内存泄漏检测: Linux下几款C++程序中的内存泄露检查工具_c++内存泄露工具分析-CSDN博客

windows下使用第三方工具 : VS编程内存泄漏:VLD(Visual LeakDetector)内存泄露库_visual leak detector vs2020-CSDN博客

其他工具: 内存泄露检测工具比较 - 默默淡然 - 博客园 (cnblogs.com)

3,如何避免内存泄露

1. 采用 RAII思想或者智能指针来管理资源。
2. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带 内存泄漏检测的功能选项。 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结一下:
内存泄漏非常常见,解决方案分为两种:1、事前预防型,如 RAII思想,智能指针等。2、事后查错型,如 泄漏检测工具

三,智能指针的使用与原理

1. RAII思想

RAII(Resource Acquisition Is Initialization)是一种在 利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效, 最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。
如下面例子:
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}~SmartPtr(){if(_ptr)delete _ptr;}private:T* _ptr;
};
int div(){int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;}
void Func()
{ShardPtr<int> sp1(new int);ShardPtr<int> sp2(new int);cout << div() << endl;
}
int main()
{try {Func();}catch(const exception& e){cout<<e.what()<<endl;}return 0;
}

2. 智能指针

上述的SmartPtr之所以称为RAll思想,而不是智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此: AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用

所以,基本的框架就有了:

// 功能像指针一样
template <class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return &(*_ptr);}
private:T* _ptr;
};

这只是初步的框架,我们知道这个SmartPtr类的拷贝构造是浅拷贝。如果两个该类对象指向同一个内容,在析构时将会析构两次,进而出现报错。换个方向,这个类类似我们学习的迭代器,但我们当时没有考虑析构的问题?

答:迭代器只是管理数据的机构,数据析构是数据本身的事情。

说到拷贝,我们要认识智能指针的发展史:

C++98的不好用,在C++11中引入了unique_ptr, shared_ptr&weak_ptr,他们两来自Boost库 。

Boost的准标准库(标准库就是C++编译器就支持的,准标准库需要外部引入源码库的),怎么理解Boost? 可以理解为标准库的体验服。

(1. unique_ptr

主旨:简单粗暴禁止拷贝。(auto_ptr栽在拷贝上)

方法一:将构造函数声明并放入private区中,这样外部无法定义访问。

方法二:强制禁用

(2. shared_ptr & weak_ptr

玩法:引用计数,并且支持拷贝

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

 

下面是简单模拟实现的shared_ptr: 

template <class T>
class share_ptr
{
public:share_ptr(T* ptr):_ptr(ptr),_pcount(new int(1)),_mtx(new mutex){}~share_ptr(){destory();}void destory(){_mtx->lock();bool flag = false;if (--(*_pcount) == 0){cout << " delete :" << *_ptr << endl;delete _ptr;delete _pcount;flag = true;}_mtx->unlock();if (flag == true){delete _mtx;}}void AddCount(const share_ptr<T>& it){_mtx->lock();(*it._pcount)++;_mtx->unlock();}T& operator*(){return *_ptr;}T* operator->(){return &(*_ptr);}share_ptr(const share_ptr<T>& it):_ptr(it._ptr),_pcount(it._pcount),_mtx(it._mtx){AddCount(it);}share_ptr<T>& operator=(const share_ptr<T>& it){// 防止自己赋值自己导致数据丢失if (_ptr != it._ptr){destory();_ptr = it._ptr;_mtx = it._mtx;_pcount = it._pcount;AddCount(it);  // 计数++return *this;}}
private:T* _ptr;int* _pcount;mutex* _mtx;
};

 

 shared_ptr的循环引用问题

特征:互相使用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() << " "; // 打印链接数
    cout << node2.use_count() << endl;
    node1->_next = node2;
    node2->_prev = node1;
    cout << node1.use_count() << " ";
    cout << node2.use_count() << endl;
    return 0;}

 

3. weak_ptr解决循环引用问题

特征:

1. 不支持RAII思想

2. 像指针

3. 专门用来辅助shared_ptr,可以指向资源,但不管理资源,也不增加计数。

// 简化版本的weak_ptr实现
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 share_ptr<T>& sp){_ptr = sp.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};

4. 定制删除器(了解)

我们注意到,我们的析构函数底层都是delete,但对象是数组,文件指针,delete析构就不合适,所以我们的shared_ptr有了定制删除器的用法。

 

本质上就是类似仿函数,下面是几个使用案例: 

template <class T>
struct DeleteArray
{void operator()(T* ptr){if (ptr != nullptr){delete[] ptr;}}
};
int main()
{// 自己写仿函数法shared_ptr<Date> pt1(new Date[10], DeleteArray<Date>());// 函数指针法这个过于简单的就不做演示// lambda法shared_ptr<Date> pt2(new Date[100], [](Date* ptr) { delete[] ptr; });// function对象法shared_ptr<Date> pt3(new Date[1000], function<void(Date*)> (DeleteArray<Date>()));// 文件管理shared_ptr<FILE> pt4(fopen("test.txt", "r"), [](FILE* ptr) { fclose(ptr); });cout << "endl";return 0;
}

结语

   本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论,如果给小伙伴带来一些收获请留下你的小赞,你的点赞和关注将会成为博主创作的动力。

这篇关于智能指针基础知识【C++】【RAII思想 || unique_ptr || shared_ptrweak_ptr || 循环引用问题】的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/788887

相关文章

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

SpringBoot首笔交易慢问题排查与优化方案

《SpringBoot首笔交易慢问题排查与优化方案》在我们的微服务项目中,遇到这样的问题:应用启动后,第一笔交易响应耗时高达4、5秒,而后续请求均能在毫秒级完成,这不仅触发监控告警,也极大影响了用户体... 目录问题背景排查步骤1. 日志分析2. 性能工具定位优化方案:提前预热各种资源1. Flowable

C/C++错误信息处理的常见方法及函数

《C/C++错误信息处理的常见方法及函数》C/C++是两种广泛使用的编程语言,特别是在系统编程、嵌入式开发以及高性能计算领域,:本文主要介绍C/C++错误信息处理的常见方法及函数,文中通过代码介绍... 目录前言1. errno 和 perror()示例:2. strerror()示例:3. perror(

C++变换迭代器使用方法小结

《C++变换迭代器使用方法小结》本文主要介绍了C++变换迭代器使用方法小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1、源码2、代码解析代码解析:transform_iterator1. transform_iterat

详解C++中类的大小决定因数

《详解C++中类的大小决定因数》类的大小受多个因素影响,主要包括成员变量、对齐方式、继承关系、虚函数表等,下面就来介绍一下,具有一定的参考价值,感兴趣的可以了解一下... 目录1. 非静态数据成员示例:2. 数据对齐(Padding)示例:3. 虚函数(vtable 指针)示例:4. 继承普通继承虚继承5.

C++中std::distance使用方法示例

《C++中std::distance使用方法示例》std::distance是C++标准库中的一个函数,用于计算两个迭代器之间的距离,本文主要介绍了C++中std::distance使用方法示例,具... 目录语法使用方式解释示例输出:其他说明:总结std::distance&n编程bsp;是 C++ 标准

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La

SpringBoot启动报错的11个高频问题排查与解决终极指南

《SpringBoot启动报错的11个高频问题排查与解决终极指南》这篇文章主要为大家详细介绍了SpringBoot启动报错的11个高频问题的排查与解决,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一... 目录1. 依赖冲突:NoSuchMethodError 的终极解法2. Bean注入失败:No qu

MySQL新增字段后Java实体未更新的潜在问题与解决方案

《MySQL新增字段后Java实体未更新的潜在问题与解决方案》在Java+MySQL的开发中,我们通常使用ORM框架来映射数据库表与Java对象,但有时候,数据库表结构变更(如新增字段)后,开发人员可... 目录引言1. 问题背景:数据库与 Java 实体不同步1.1 常见场景1.2 示例代码2. 不同操作

C++ 中的 if-constexpr语法和作用

《C++中的if-constexpr语法和作用》if-constexpr语法是C++17引入的新语法特性,也被称为常量if表达式或静态if(staticif),:本文主要介绍C++中的if-c... 目录1 if-constexpr 语法1.1 基本语法1.2 扩展说明1.2.1 条件表达式1.2.2 fa