立山 发表于 2024-8-13 03:57:14

【C++】C++的内存处置惩罚 --- 智能指针

https://i-blog.csdnimg.cn/direct/13efaca57e46427cbb105d95facf8f4c.png
   只有经历地狱般的锻炼,    才能练出创造天堂的力量 ;        只有流过血的手指,          才能弹出世间的绝唱 。           --- 泰戈尔 ---                

1 前言

我们往返首一下在学习异常机制中遇到的一种标题:在try catch语句中,如果我们开辟了一段空间,但是发生了异常,会直接终止掉函数栈桢,导致内存泄漏标题。以是此时就要在catch语句中举行一个特殊处置惩罚。如果我们开辟了多段空间,那么这个操纵就会变得更加复杂:假如new失败了,就会直接返回到上层的catch语句,也导致了内存泄漏标题!利用传统是异常机制来办理标题会产生大量冗余的语句 — 大量的try catch嵌套!
为相识决这个标题,可以利用智能指针!可以简朴的来举行办理!
2 智能指针

2.1 什么是智能指针

智能指针类似lock_guard,是对指针的封装,可以实如今超出生命周期之后主动销毁的功能!
void func()
{
        int* p1 = new int;
        int* p2 = nullptr;

        try
        {
                p2 = new int;
                try
                {

                        double a, b;
                        cin >> a >> b;
                        Division(a, b);
                }
                catch (...)
                {
                        delete[] p1;
                        cout << "delete: p1" << endl;
                        delete[] p2;
                        cout << "delete: p2" << endl;

                        throw ;
                }
        }
        catch(...)
        {
                delete[] p1;
                cout << "delete: p1" << endl;

                throw;
        }

       
        delete[] p1;
        cout << "delete: p1" << endl;
        delete[] p2;
        cout << "delete: p2" << endl;

        return;
}
在这个程序中,开辟空间和销毁空间是一个重要标题,为了防止被抛出异常就直接销毁堆栈,就要设置多重的try catch来包管不会发生内存泄漏!对于这样的标题,我们可以设计一个smartptr类来资助我们办理!
class SmartPtr
{
public:
        SmartPtr(int* ptr)
                :_ptr(ptr)
        {}
        ~SmartPtr()
        {
                delete[] _ptr;
                cout << "delete!" << _ptr << endl;
        }

private:
        int* _ptr;
};
这样一个类包装了一个指针,我们不需要在显式delete了,只要生命周期结束,就会主动释放空间!
这样在开辟空间时,就直接举行构造不就好了!这样就直接避免了复杂嵌套的try catch语句!
void func()
{
        SmartPtr sp1(new int);
        SmartPtr sp2(new int);


        double a, b;
        cin >> a >> b;
        Division(a, b);

        return;
}
再也不用担心忘记释放开辟的空间了!内存泄漏标题直接远去了~
我们把这种封装称之为RAII:
RAII(Resource Acquisition Is Initialization 资源请求立即初始化)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络毗连、互斥量等等)的简朴技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有用,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大利益:

[*]不需要显式地释放资源。
[*]采用这种方式,对象所需的资源在其生命期内始终保持有用
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此AutoPtr模板类中还得需要将* ->重载下,才可让其像指针一样去利用!还需要举行一个拷贝构造的特殊处置惩罚,否则就会出现对同一片地址析构两次的场景
2.2 C++库中的智能指针

在C++memory库中有以下几种智能指针:
https://i-blog.csdnimg.cn/direct/90cf3dd3b34f4aa48e82197f38a37e51.png
我们来看auto_ptr是怎样办理拷贝标题标:
https://i-blog.csdnimg.cn/direct/3b543bedb6814785baa6faace59f2e58.png
也就是说auto_ptr支持两个对象指向同一片空间!通过拷贝时转移管理权来办理这种析构多次的标题(类似移动构造)。但是这样的处置惩罚方式实际上是很不合理的!sp1并不是一个将亡值,sp2凭什么将sp1的资源转移走!?“我还活着了 , 怎么就把我埋了!”,在接下来代码中,如果我们再次调用了sp1就会直接导致程序的瓦解!以是这样的设计是一个失败的设计!
以是auto_ptr尽量就不要举行利用!
以是auto_ptr尽量就不要举行利用!
以是auto_ptr尽量就不要举行利用!
在C++11中到场了shared_ptr unique_ptr weak_ptr ,一般建议利用unique+ptr 和 shared_ptr。来看一下他们支持什么操纵:
https://i-blog.csdnimg.cn/direct/f5a6e9da1b234aa283bdd1d9eb65780d.png

[*]unique_ptr构造支持无参构造,移动构造… , 就是不支持拷贝构造!因为拷贝有标题以是就不让拷贝!直接避免了标题出现!
[*]get:获取到智能指针内部的指针!
[*]release:显式释放空间!
[*]-> *:支持的指针操纵!
https://i-blog.csdnimg.cn/direct/064326573eb3460b85a6e4532111d44f.png

[*]shared_ptr支持全部的构造,包括拷贝构造!其引入了引用计数的概念(Linux中很常见)!
[*]get:获取到智能指针内部的指针!
[*]release:显式释放空间!
[*]-> *:支持的指针操纵!
[*]make_shared:类似make_pair,可以举行创建shared_ptr
2.3 循环指向标题与weak_ptr

我们一般保举利用shared_ptr,其独有的引用计数机制,极大程度复原了指针的实际用法,而且能做到RAII技术!
但是,shared_ptr存在一个标题:循环指向标题!这种标题主要出如今循环链表中,每个节点有两个指针,分别指向前一个节点和后一个节点。当我们有两个节点时,我们都利用shared_ptr举行包装管理:
struct Node
{


        bit::shared_ptr<Node> _next;
        bit::shared_ptr<Node> _prev;

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

};

int main()
{
        bit::shared_ptr<Node> sp1(new Node);
        bit::shared_ptr<Node> sp2(new Node);

        sp1->_next = sp2;
        sp2->_prev = sp1;

        return 0;
}
这样按理说每个节点的引用计数都为2(自身 + 别的节点中的智能指针),在程序结束运行时,会调用sp1 sp2的析构函数,这样会让其引用计数变为1。接下来就是复杂的标题了,由于刚才并没有让引用计数变为0,两个节点中的的_next; _prev;都还托管着数据,但是他们两个谁先析构呢?这类似经典的先有鸡 先有蛋标题,这就是循环指向标题!
办理这个标题单凭shared_ptr是没有办法办理的,这里就要引入weak_ptr了:
https://i-blog.csdnimg.cn/direct/19c381a57a7742ffb94026bc11724e42.png
weak_ptr并不支持直接来举行管理指针资源,不支持RAII。但支持无参构造和拷贝构造,专门用来辅助办理shared_ptr的循环指向标题!我们只需要将Node里面的指针利用weak_ptr来举行托管就可以了:
struct Node
{

        weak_ptr<Node> _next;
        weak_ptr<Node> _prev;

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

};

int main()
{
        shared_ptr<Node> sp1(new Node);
        shared_ptr<Node> sp2(new Node);

        sp1->_next = sp2;
        sp2->_prev = sp1;

        return 0;
}
因为weak_ptr本质赋值或拷贝时,只指向资源,不会增长引用计数!以是就不会造成先有鸡先有蛋的标题!
2.4 自定义删除器

智能指针内部还支持自定义删除器,因为在构造时并不能包管默认析构可以释放掉我们开辟的空间,比如

[*]在举行malloc的时候,默认的delete是不能满足条件的
[*]在管理文件指针的时候,需要利用fclose来释放空间,而不是默认的delete
[*]开辟一个数组空间时 , 需要利用delete[]来举行释放空间
以是为了更是适配内存管理的多样性,智能指针支持自定义删除器,即支持用户显式传递删除方法!
https://i-blog.csdnimg.cn/direct/043d105e020943d585dbc24f4894c6a2.png
这里可以利用仿函数来举行传递,但是在C++11之后利用lambda表达式更加简约直观!
int main()
{
        shared_ptr<A> sp1(new A(1 , 1));
        shared_ptr<A[]> sp2(new A);

        shared_ptr<FILE> sp3(fopen("file.txt", "w"), [](FILE* sp) { fclose(sp); });
        shared_ptr<int> sp4( (int*)malloc(4) , [](int* sp) { free(sp); });
        shared_ptr<A> sp5(new A, [](A* sp) {delete[] sp; });

        return 0;
}
这样就办理了不同范例指针释放方式不同等的标题!
3 手搓shared_ptr

我们来实践一下shared_ptr,其在面试中经常会考到,这智能指针的主要头脑是RAII,将指针封装起来,包管其在生命周期内存在,离开生命周期就主动释放掉!
3.1 框架搭建

起首智能指针内部需要一个指针变量来储存数据。重要的是怎样将引用计数到场其中,如果直接利用一个int count肯定是不可的,这样每个对象都有自己的count,无法做到引用计数的功能。如果利用静态变量,那么全部的类对象只有一个计数,这样肯定也是不可以的!那么要怎样办理这个标题呢?为引用计数单独开辟一块空间,举行拷贝的时候就将这个空间举行传值,这样全部举行拷贝的对象都可以读取到同一个引用计数的数据!
构造函数可以直接写出来,析构就在引用计数为0的时候举行释放空间!
        template< class T>
        class shared_ptr
        {
        public:
                //默认构造函数
                shared_ptr(T* ptr = nullptr)
                        :_ptr(ptr),
                        _pcount(new int(1))
                {
                }
                ~shared_ptr()
                {
                        release();
                }
                void release()
                {
                        if (--(*_pcount) == 0)
                        {
                                //最后管理的一个对象 , 释放资源
                                delete _ptr;
                                delete _pcount;
                        }
                }


        private:
                //内部指针
                T* _ptr;
                //引用计数
                int* _pcount;

        };
3.2 拷贝构造和赋值重载

这里最为重要的就是这个拷贝构造和赋值重载怎样举行誊写!我们需要模拟到和原生指针一样,可以让不同的指针对象指向同一块空间 ,而且不能发生重复析构的标题:

[*]拷贝构造直接将对象的_ptr _pcount 举行拷贝就可以,不要忘记举行引用计数的++
[*]赋值重载就要思量的多一点:

[*]起首需要对本来指向的空间举行一次析构,包管本来的空间引用计数 - -
[*]然后举行拷贝,引用计数++
[*]注意:避免自己给自己赋值的情况需要举行一次判断!不然在引用计数只有1的时候会出现野指针标题

//拷贝构造

shared_ptr(const shared_ptr<T>& sp)
        :_ptr(sp._ptr),
        _pcount(sp._pcount)
{
        (*_pcount)++;
}

shared_ptr<T>&operator=(const shared_ptr<T>& sp)
{
        if( _ptr == sp._ptr)
        {
                return *this;
        }
        this->release();
        _ptr = sp._ptr;
        _pcount = sp._pcount;
        (*_pcount)++;

        return *this;
}
3.3 自定义删除器

起首为了适配自定义删除器,我们需要多加一个成员变量_del,利用function包装器举行包装,可以省去很多不必要的操纵!
成员变量添加:
std::function<void(T*)> _del = [](T* p){ delete p; } ;
这个包装器就是用来包装删除器的,到场了删除器,我们就要再写一个单独的构造函数来满足:
                template<class D>
                shared_ptr(T* ptr = nullptr , D del = [](T* p) { delete p; })
                        : _ptr(ptr),
                        _pcount(new std::atomic<int>(1)),
                        _del(del)
                {

                }
这样在显式调用构造的时候就可以举行自定义删除器的添加了:
        bit::shared_ptr<FILE> sp3(fopen("file.txt", "w"), [](FILE* sp) { fclose(sp); });
        bit::shared_ptr<int> sp4( (int*)malloc(4) , [](int* sp) { free(sp); });
        bit::shared_ptr<A> sp5(new A, [](A* sp) {delete[] sp; });
3.4 功能函数

为了让shared_ptr可以有原生指针的利用方法,我们需要对* ->举行一个重载!这我们已经很熟悉不过了!
然后就是设计一个get()和use_count()函数!都很简朴!
T* get()
{
        return _ptr;
}

T& operator*()
{
        return *_ptr;
}
T* operator->()
{
        return _ptr;
}

int use_count()
{
        return *(_pcount);
}
3.4 多线程下的特殊处置惩罚

上面已经实现了正常情况下的智能指针的利用,我们来看多线程情况下会不会出现标题。
在下面的程序中,我们设置了三个线程同时对sp1内部的链表举行尾插,尾插是临界的,我们用锁举行保护。
https://i-blog.csdnimg.cn/direct/c2e17723df3c4566ae7fb1dce302ebca.png
我们用锁举行了保护,但是照旧出现了错误!
为什么会出现这样的标题?起首我们分析一下临界区,在share_ptr中引用计数是临界的!为什么呢?因为引用计数的操纵++ -- 是非原子的!多个线程中我们不停举行copy拷贝,会对引用计数不停举行++--,导致了标题!为了从根本上办理这个标题,我们就要包管操纵是原子的!我们可以在类中到场一个锁来包管++--中举行保护。但是最直接的就是将引用计数酿成原子的就可以了!
                //引用计数
                std::atomic<int>* _pcount;
这样就可以包管拷贝和析构的时候就是原子的了就不会出现标题了!!!就可以包管线程安全了!
注意我将shared_ptr美满之后:

[*]智能指针对象自己拷贝是线程安全的
[*]底层引用计数加减是线程安全的
[*]指向的资源访问不是线程安全的,该加锁照旧要加锁!
4 内存泄漏

最后我们往返首一下内存泄漏标题:


[*]什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再利用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
[*]内存泄漏的危害:恒久运行的程序出现内存泄漏,影响很大,如操纵系统、背景服务等等,出现
内存泄漏会导致相应越来越慢,终极卡死。
对于C++来说,内存泄漏是很严峻的标题!C++没有和JAVA的垃圾回收机制。
在正常的一个程序中,内存泄漏实在影响并不大,我们开辟一段空间,如果没有释放,在进程结束的时候也会被释放掉,因为我们开辟的空间都是虚拟内存,进程结束之后会把虚拟地址一并收拾带走。就怕进程异常结束,酿成僵尸进程挂起,此时虚拟地址和物理内存依然存在映射,此时就完蛋了。再加上如果是恒久运行的代码,内存泄漏的不停积累会导致内存空间越来越小!
C/C++程序中一般我们关心两种方面的内存泄漏:

[*]堆内存泄漏(Heap leak):
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部门内存没有被释放,那么以后这部门空间将无法再被利用,就会产生Heap Leak。
[*]系统资源泄漏:
指程序利用系统分配的资源,比方套接字、文件描述符、管道等没有利用对应的函数释放掉,导致系统资源的浪费,严峻可导致系统效能减少,系统执行不稳定。
内存泄漏可以通过第三方库来举行检测,当时这些并不是很好用,而且在实际工作中,编译运行一次程序可能需要很长时间,那么通过第三方库来检测是很费事的!以是尽量在利用中就要避免内存泄漏的标题:

[*]工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记取匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,照旧可能会出标题。需要下一条智能指针来管理才有包管。
[*]采用RAII头脑或者智能指针来管理资源。只要正常利用智能指针一般不会出现内存泄漏!
[*]有些公司内部规范利用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
[*]出标题了利用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总而言之:
内存泄漏非经常见,办理方案分为两种:1、事前防备型。如智能指针等。2、过后查错型。如泄漏检测工具。
Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【C++】C++的内存处置惩罚 --- 智能指针