【C++杂货铺】智能指针

秋刀鱼的滋味@ 2024-08-14 16:35:04 阅读 91

        

目录

🌈 前言🌈   

📁 内存泄漏

 📂 概念

 📂 分类 

 📂 如何避免

📁 RAII

📁 C++11智能指针

 📂 auto_ptr

 📂 unique_ptr 

 📂 shared_ptr

 📂 weaked_ptr

 📂 定制删除器

📁 C+11智能指针和boost智能指针关系

📁 总结


🌈 前言🌈   

        欢迎收看本期【C++杂货铺】,本期内容,主要讲解内存泄露的概念,如何避免内存泄漏,RAII是如何解决,引入智能指针对象,智能指针的三种类型,以及各自的优缺点,及其使用陷阱,此外,还将展示其底层实现。

📁 内存泄漏

 📂 概念

        内存泄漏是因为疏忽或错误造成程序未能释放已经不能使用的内存的情况,例如ner一段空间没有free。内存泄漏并不是指内存在物理上的小事,而是应用程序分配某段内存后,因为设计问题,失去了对该内存的控制,因而造成的内存的浪费。

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

<code>void MemoryLeaks()

{

  // 1.内存申请了忘记释放

 int* p1 = (int*)malloc(sizeof(int));

 int* p2 = new int;

 

 // 2.异常安全问题

 int* p3 = new int[10];

 

 Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.

 

 delete[] p3;

}

 📂 分类 

● 堆内存泄漏(Heap leak)

        程序执行过程中,使用malloc/calloc/realloc/new等从堆中分配一块内存,用完后必须通过调用相应的free或者delete。假设程序的设计错误导致这部分内存没有释放,那么以后这段内存无法再被释放,就会产生Heap Leak。

● 系统资源泄露

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

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

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

其他工具:

https://www.cnblogs.com/liangxiaofeng/p/4318499.html

 📂 如何避免

        1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps: 这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智 能指针来管理才有保证。

        2. 采用RAII思想或者智能指针来管理资源。

        3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。

        4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

        总结分为:1.事前预防型,如智能指针;2. 事后查错型,如内存泄漏工具。

📁 RAII

        RAII(Resource Acquisition Is Initialization)是一种利用对象声明后期来控制程序资源(如内存,文件句柄,网络连接,互斥量)的简单技术。

        在对象构造时获取资源,最后在对象析构时释放资源。借此,我们实际上把一份资源的责任托管给一个对象,这么做有两个好处:

        1. 不需要显示地释放资源。

        2. 对象所需要的资源在其生命周期内始终有效。

        总体来说,就是实现一个模板类,当模板类实例化出一个对象时,更构造函数提供一段堆内存空间,就获取一份资源,通过对象来管理,当出作用域,对象析构时,自动释放资源,因为析构是自动的。

📁 C++11智能指针

 📂 auto_ptr

        C++98版本中的库就提供了auto_ptr的智能指针。

        auto_ptr的实现原理:管理权转移的思想。

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 = NULL;

}

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就交换两个智能指针指向。

        因此,实际中很少会使用auto_ptr,会导致需要问题,因此很多地方也禁止使用auto_ptr。

 📂 unique_ptr 

        如果我们想要两个指针指向同一段内存空间是可以的,但是智能指针是两个对象,当两个对象处在同一个作用域,也要指向同一段内存空间时,是可以的。当两个对象出作用域时,第一个完成析构,释放资源,第二个智能指针析构时,没有资源却依然释放,就会导致程序错误。

        如何解决这个问题?就引入了另外两个智能指针unique_str和shared_ptr。unique_ptr表示,既然你不能这样,那我直接不让你拷贝构造,赋值不就行了,每一个智能指针都只能指向不同的内存空间,不就解决问题了吗。

        原理非常简单,就是防拷贝。

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

};

        但是某些情况下,我们就是想要两个智能指针指向同一个内存空间,那么就要使用shared_ptr了。

 📂 shared_ptr

        C++11提供了更靠谱且能支持拷贝构造的shared_ptr。原理是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

        shared_ptr在其内部,给每个资源都维护了一份计数,用来记录该资源被几个对象共享。        

        在对象析构时,先将计数-1,在判断是否为0,如果为0,表示没有指针指向,释放资源。如果不是0,就说明除了自己还有其他对象在使用改份资源,不能释放该资源,否则其他对象就变成野指针了。

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 Release()

{

_pmtx->lock();

bool flag = false;

if (--(*_pRefCount) == 0 && _ptr)

{

cout << "delete:" << _ptr << endl;

delete _ptr;

delete _pRefCount;

flag = true;

}

_pmtx->unlock();

if (flag == true)

{

delete _pmtx;

}

}

void AddRef()

{

_pmtx->lock();

++(*_pRefCount);

_pmtx->unlock();

}

shared_ptr<T>& operator=(const shared_ptr<T>& sp)

{

//if (this != &sp)

if (_ptr != sp._ptr)

{

Release(); _ptr = sp._ptr;

_pRefCount = sp._pRefCount;

_pmtx = sp._pmtx;

AddRef();

}

return *this;

}

int use_count()

{

return *_pRefCount;

}

~shared_ptr()

{

Release();

}

// 像指针一样使用

T& operator*()

{

return *_ptr;

}

T* operator->()

{

return _ptr;

}

T* get() const

{

return _ptr;

}

private:

T* _ptr;

int* _pRefCount;

mutex* _pmtx;

};

        上述模拟实现中,计数器使用指针,这样多个指向同一份资源的对象拥有同一份计数器。但是这里也引入了线程安全的问题,当多线程开发环境下,指针同时++,--需要保证原子性操作,所以这里int*,可以改为atomic<int>*,也可以使用锁来实现。

        智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时 ++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错 乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、--是需要加锁的,也就是说引用计数的操作是线程安全的。

        但需要注意的是,std::shared_ptr保证了++,--等使用智能指针操作的线程安全,无法保证使用指向资源是线程安全的。

 📂 weaked_ptr

        shared_ptr的引用计数原理,十分的完美,但是它也有一个致命的问题,就是循环引用。当我们使用shared_ptr来使用双向链表,就会发现,最后无法析构了。

        Node1析构时,引用计数--,不为0,资源不会释放;Node2析构时,引用计数--,不为0,资源不会释放。这就会导致循环引用,双方都不释放资源,进而造成内存泄漏。

<code>class Node

{

public:

Node()

{}

~Node()

{

cout << "~Node()" << endl;

}

int _data = 0;

weak_ptr<Node> _next;

weak_ptr<Node> _prev;

//shared_ptr<Node> _next;

//shared_ptr<Node> _prev;

};

int main()

{

shared_ptr<Node>p1(new Node);

shared_ptr<Node>p2(new Node);

p1->_next = p2;

p2->_prev = p1;

return 0;

}

        weak_ptr引入,就是为了解决这个问题。weak_ptr并不直接参与 RAII(资源获取即初始化)原则,因为它本身不控制资源的生命周期。weak_ptr通常用于解决 C++ 中的循环引用问题,它指向一个由 shared_ptr管理的对象,但不增加引用计数,因此不会阻止对象的销毁。

        weak_ptr本身不拥有对象,就是一个弱引用,它并不符合RAII(对象生命周期控制资源),所以它不直接参与RAII,一般用来和shared_ptr配合,解决循环引用问题。

 📂 定制删除器

        智能指针构造是通过用户给出的内存空间来构造,析构默认使用delete来释放资源。但是如果我们使用智能指针来指向malloc出的空间,或者文件描述符等系统资源,就不能使用delete来释放资源。

        这是就需要我们来手写一个删除器,来告诉编译器怎么删除。本质就是一个底层执行一个回调函数,来释放资源。

        因此,底层就要一个可执行对象function来接收用户给定的定制删除器,如果用户没有指定,就使用默认delete删除。                                                                            

~shared_ptr()

{

if (--(*_size) == 0)

{

_del(_ptr);

delete _size;

}

}

T* _ptr;

function<void(T*)> _del = [](T* ptr) {delete ptr;};

        上述是我们自己写个一个shared_ptr中一部分,下面展示如何使用:

class A

{

public:

A()

{}

A(int a)

:_a(a)

{}

~A()

{}

private:

int _a = 10;;

};

template<class T>

struct Freefunc

{

void operator()(T* ptr)

{

cout << "free ptr: " << ptr << endl;

free(ptr);

}

};

int main()

{

bit::shared_ptr<A> sp1(new A);

bit::shared_ptr<int> sp2((int*)malloc(4), Freefunc<int>());

bit::shared_ptr<FILE>sp3(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr);});

return 0;

}

        因此,定制删除器作用就是,将不能使用delete释放的资源,通过定制删除器,释放了。

📁 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中的实现的。

        

📁 总结

        本期【C++杂货铺】主要内容,就是讲解什么是智能指针,RAII思想如何解决内存泄漏,三种智能指针类型,以及使用陷阱,需要注意什么,展示了模拟了底层实现。

        如果感觉本期内容对你有帮助,欢迎点赞,收藏,关注Thanks♪(・ω・)ノ



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。