【C++】21.智能指针

1.为什么需要智能指针

C++无gc new/malloc出来的资源 是需要我们去手动释放

1.忘记释放

2.发生异常安全问题

new/malloc

fun()://throw 异常

delete/free

最终都导致资源的泄漏

利用智能指针更好的去解决此类问题

2.智能指针

1°RAII

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

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

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

2°智能指针的原理

SmartPtr.h

//RAII + 像指针一样
template<class T>
class SmartPtr
{
public:
    SmartPtr(T* ptr)
        :_ptr(ptr)
    {}

    ~SmartPtr()
    {
        if (_ptr)
        {
            cout << "delete:" << _ptr << endl;
            delete _ptr;
            //制空解决不了
            //sp2还是正常指向
        }
    }

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

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

private:
    T* _ptr;
};
#include "SmartPtr.h"

//-------------------
int div()
{
    int a, b;
    cin >> a >> b;
    if (b == 0)
        throw invalid_argument("除0错误");

    return a / b;
}

void f1()
{
    //智能指针
    //法1:
    //int* p = new int;
    //SmartPtr<int> sp(p);
    //--------------------
    //无论是函数正常结束 还是抛异常 都会导致sp对象的
    //生命周期到了以后 调用析构函数
    //相当于类帮我们管理资源的释放
    //RAII是一种利用对象生命周期来控制程序资源
    //RAII与智能指针的关系
    //RAII是一种托管资源的思考 智能指针是依靠这种RAII实现的
    //unique_lock/lock_guard也是
    //法2:
    //需要重载operator*和operator-> 像指针一样
    SmartPtr<int> sp1(new int);
    *sp1 = 10;

    SmartPtr<pair<int, int>> sp2(new pair<int, int>);
    sp2->first = 20;
    sp2->second = 30;

    cout << div() << endl;
}
//抛异常
int main()
{
    try
    {
        f1();
    }

    catch (exception& e)
    {
        cout << e.what() << endl;
    }

    return 0;
}

智能指针有坑

#include "SmartPtr.h"

int main()
{
    //指向同一块资源就被释放两次
    SmartPtr<int> sp1(new int);
    SmartPtr<int> sp2 = sp1;
    return 0;
}

同一块空间被释放两次

有三种解决方法:

  • 管理权转移
  • 防拷贝
  • 引用计数

3°auto_ptr

管理权转移 早期设计缺陷 一般公司都明令禁止使用它

缺陷:ap2 = ap1场景下ap1就制空了 访问就会报错 如果不熟悉它的特性就会被坑

说白了就是ap2指向了ap1的空间 然后ap1制空

namespace szh
{
    template<class T>
    class auto_ptr
    {
    public:
        auto_ptr(T* ptr)
            :_ptr(ptr)
        {}

        auto_ptr(auto_ptr<T>& ap)
            :_ptr(ap._ptr)
        {
            ap._ptr = nullptr;
            //给你了 我不管了 直接制空
        }

        //赋值 sp1 = sp2
        auto_ptr<T>& operator=(const auto_ptr <T>& ap)
        {
            if (this != &ap)
            {
                if (_ptr)
                    delete _ptr;

                //赋给你后 我就制空
                _ptr = ap._ptr;
                ap._ptr = nullptr;
            }
        }

        ~auto_ptr()
        {
            if (_ptr)
            {
                cout << "delete:" << _ptr << endl;
                delete _ptr;
                //制空解决不了
                //sp2还是正常指向
            }
        }

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

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

    private:
        T* _ptr;
    };
}
#include "SmartPtr.h"

int main()
{
    szh::auto_ptr<int> ap1(new int);
    szh::auto_ptr<int> ap2 = ap1;
}

但是 如果访问ap1的话就会崩溃

#include "SmartPtr.h"

int main()
{
    szh::auto_ptr<int> ap1(new int);
    szh::auto_ptr<int> ap2 = ap1;
    *ap1 = 1; //悬空崩溃
    return 0;
}

闪光标

auto_ptr的缺陷挺大的 基本不会使用

4°unique_ptr

C++11 unique_ptr

防拷贝 推荐使用 简单粗暴

缺陷:如果有需要拷贝的场景 就没法使用

namespace szh
{
    template<class T>
    class unique_ptr
    {
    public:
        unique_ptr(T* ptr)
            :_ptr(ptr)
        {}

    //拷贝构造和operator=直接搞成删除函数
        unique_ptr(unique_ptr<T>& up) = delete;
        unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;

        ~unique_ptr()
        {
            if (_ptr)
            {
                cout << "delete:" << _ptr << endl;
                delete _ptr;
                //制空解决不了
                //sp2还是正常指向
            }
        }

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

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

    private:
        T* _ptr;
    };
}
#include "SmartPtr.h"

int main()
{
    szh::unique_ptr<int> up1(new int);
    szh::unique_ptr<int> up2(up1);//拷贝的时候直接编译不过
    return 0;
}

5°shared_ptr

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

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

在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减1

如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;

如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

计数:

  • 如果使用普通count 不会释放 sp1 sp2都会减到1 希望的是引用一个计数

  • 如果使用静态的 static count可以正常释放 但遇到下面情况还是有问题

    szh::shared_ptr sp1(new int);

    szh::shared_ptr sp2(sp1);

    szh::shared_ptr sp3(new int);

    应该释放两次 但只释放一次

    此时s3自己管理一块资源空间 希望有两个计数

    因为静态只有一个计数

  • 最后使用指针可以解决问题

namespace szh
{
    template<class T>
    class shared_ptr
    {
    public:
        shared_ptr(T* ptr = nullptr)
            :_ptr(ptr)
            ,_pcount(new int(1))
            ,_pmtx(new mutex)
        {}

        shared_ptr(shared_ptr<T>& sp)
            :_ptr(sp._ptr)
            , _pcount(sp._pcount)
            ,_pmtx(sp._pmtx)
        {
            //++(*_pcount);
            add_ref_count();
        }
        
        //sp1 = sp4
        shared_ptr<T>& operator=(shared_ptr<T>& sp)
        {
            if (this != &sp)
            {
                //--引用计数 如果我是最后一个管理资源的对象 则释放资源
                //if (--(*_pcount) == 0)
                //{
                //	delete _pcount;
                //	delete _ptr;
                //}
                release();

                //共同管理后++
                _ptr = sp._ptr;
                _pcount = sp._pcount;
                _pmtx = sp._pmtx;
                //++(*_pcount);
                add_ref_count();
            }
            
            return *this;
        }

        void add_ref_count()
        {
            _pmtx->lock();
            ++(*_pcount);
            _pmtx->unlock();
        }

        void release()
        {
            _pmtx->lock();
            bool flag = false;
            if (--(*_pcount) == 0)
            {
                if (_ptr)
                {
                    cout << "delete:" << _ptr << endl;
                    delete _ptr;
                    //制空解决不了
                    //sp2还是正常指向
                    _ptr = nullptr;
                }
                //注意释放
                delete _pcount;
                _pcount = nullptr;
                flag = true;
            }
            _pmtx->unlock();

            if (flag == true)
            {
                delete _pmtx;
                _pmtx = nullptr;
            }
        }

        ~shared_ptr()
        {
            //if (--(*_pcount) == 0 && _ptr)
            //{
            //	cout << "delete:" << _ptr << endl;
            //	delete _ptr;
            //	//制空解决不了
            //	//sp2还是正常指向
            //	_ptr = nullptr;

            //	//注意释放
            //	delete _pcount;
            //	_pcount = nullptr;
            //}
            release();
        }

        int use_count()
        {
            return *_pcount;
        }

        T* get_ptr() const
        {
            return _ptr;
        }

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

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

    private:
        T* _ptr;

        int* _pcount;//记录有多少个对象一起共享管理资源 最后一个析构释放资源

        mutex* _pmtx;//解决线程安全问题
    };
}
#include "SmartPtr.h"

int main()
{
    szh::shared_ptr<int> sp1(new int);
    szh::shared_ptr<int> sp2(sp1);
    szh::shared_ptr<int> sp3(new int);
    szh::shared_ptr<int> sp4(sp3);
    szh::shared_ptr<int> sp5(sp3);

    sp1 = sp4;
    //sp1不再管理原来的资源 与sp4共同管理一个资源
    //sp1管理资源的计数-1 sp4管理资源的计数+1
    return 0;
}

 

shared_ptr有线程安全问题

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

智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。

需要加锁

shared+str是否是线程安全的

答:注意这里问题的shared_ptr对象拷贝和析构++/--引用计数是否是安全的

库的实现中是安全的

#include "SmartPtr.h"

#include <thread>

int main()
{
    szh::shared_ptr<int> sp(new int);
    cout << sp.use_count() << endl;
    int n = 10000;
    
    //一次拷贝构造问题不大 多了就有问题了
    std::thread t1([&]() {
        //szh::shared_ptr<int> sp1(sp);
        for (int i = 0; i < n; ++i)
        {
            szh::shared_ptr<int> sp1(sp);
        }
    });

    std::thread t2([&]() {
        //szh::shared_ptr<int> sp2(sp);
        for (int i = 0; i < n; ++i)
        {
            szh::shared_ptr<int> sp2(sp);
        }
    });

    t1.join();
    t2.join();

    //析构后回到1
    cout << sp.use_count() << endl;
    return 0;
}

 

shared_ptr会有循环引用问题 使用弱指针来解决

循环引用:

  • node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  • node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  • node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  • 也就是说_next析构了,node2就释放了。
  • 也就是说_prev析构了,node1就释放了。
  • 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。

此时引入弱指针

当调用拷贝构造的时候 直接返回 不进行++

namespace szh
{
    template<class T>
    class shared_ptr
    {
    public:
        shared_ptr(T* ptr = nullptr)
            :_ptr(ptr)
            ,_pcount(new int(1))
            ,_pmtx(new mutex)
        {}

        shared_ptr(shared_ptr<T>& sp)
            :_ptr(sp._ptr)
            , _pcount(sp._pcount)
            ,_pmtx(sp._pmtx)
        {
            //++(*_pcount);
            add_ref_count();
        }
        
        //sp1 = sp4
        shared_ptr<T>& operator=(shared_ptr<T>& sp)
        {
            if (this != &sp)
            {
                //--引用计数 如果我是最后一个管理资源的对象 则释放资源
                //if (--(*_pcount) == 0)
                //{
                //	delete _pcount;
                //	delete _ptr;
                //}
                release();

                //共同管理后++
                _ptr = sp._ptr;
                _pcount = sp._pcount;
                _pmtx = sp._pmtx;
                //++(*_pcount);
                add_ref_count();
            }
            
            return *this;
        }

        void add_ref_count()
        {
            _pmtx->lock();
            ++(*_pcount);
            _pmtx->unlock();
        }

        void release()
        {
            _pmtx->lock();
            bool flag = false;
            if (--(*_pcount) == 0)
            {
                if (_ptr)
                {
                    cout << "delete:" << _ptr << endl;
                    delete _ptr;
                    //制空解决不了
                    //sp2还是正常指向
                    _ptr = nullptr;
                }
                //注意释放
                delete _pcount;
                _pcount = nullptr;
                flag = true;
            }
            _pmtx->unlock();

            if (flag == true)
            {
                delete _pmtx;
                _pmtx = nullptr;
            }
        }

        ~shared_ptr()
        {
            //if (--(*_pcount) == 0 && _ptr)
            //{
            //	cout << "delete:" << _ptr << endl;
            //	delete _ptr;
            //	//制空解决不了
            //	//sp2还是正常指向
            //	_ptr = nullptr;

            //	//注意释放
            //	delete _pcount;
            //	_pcount = nullptr;
            //}
            release();
        }

        int use_count()
        {
            return *_pcount;
        }

        T* get_ptr() const
        {
            return _ptr;
        }

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

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

    private:
        T* _ptr;

        int* _pcount;//记录有多少个对象一起共享管理资源 最后一个析构释放资源

        mutex* _pmtx;//解决线程安全问题
    };
    
    //弱指针
    //严格来说weak_ptr不是智能指针 因为他没有RAII资源管理机制
    //专门解决shared_ptr的循环引用问题

    template<class T>
    class weak_ptr
    {
    public:
        weak_ptr() = default;

        weak_ptr(const shared_ptr<T>& sp)
            :_ptr(sp.get_ptr())
        {}
        
        weak_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            _ptr = sp.get_ptr();
            return *this;
        }

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

        T* operator->()
        {
            return _ptr;
        }
    private:
        T* _ptr;
    };
}
#include "SmartPtr.h"

struct ListNode
{
    int val;
    //ListNode* _prev;
    //ListNode* _next;
    //szh::shared_ptr<ListNode> _spnext;
    //szh::shared_ptr<ListNode> _spprev;
    //弱指针
    szh::weak_ptr<ListNode> _spprev;
    szh::weak_ptr<ListNode> _spnext;

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

int main()
{
    //可正常释放
    szh::shared_ptr<ListNode> spn1(new ListNode);
    szh::shared_ptr<ListNode> spn2(new ListNode);

    //为什么编译不过
    //spn1->_next = spn2;
    //spn2->_prev = spn1;
    //这里next prev是原生指针
    //next和prev改成智能指针即可
    //-------------------
    //改了之后 加上这两句 无法正常释放
    //spn1->_spnext = spn2;
    //spn2->_spprev = spn1;
    //此时发生了循环引用问题
    //引用计数都变成了2
    //spn1 spn2析构后 引用计数都变成1
    //我的释放由你管理着 你的释放由我管理着
    //互相管理对方的空间
    //就像我抓着你的头发 你抓着我的头发 两人互相抓着 
    //谁也不愿意放手 最后就互相伤害
    //那么如何解决?
    //使用weak_ptr 不增加引用计数

    cout << spn1.use_count() << endl;
    cout << spn2.use_count() << endl;

    spn1->_spnext = spn2;
    spn2->_spprev = spn1;

    cout << spn1.use_count() << endl;
    cout << spn2.use_count() << endl;

    return 0;
}

6°智能指针的发展历史

智能指针的历史

C++没有gc(垃圾回收器) 申请的资源需要释放是一个问题 尤其是碰到异常安全问题 特别难处理

稍不注意就会出现内存泄漏 内存泄漏导致程序可用的内存越来越少 程序中很多操作都是需要内存的

那么会导致程序基本处于瘫痪状态 所以我们尽量要杜绝内存泄漏问题

所以就发展处于基于RAII思想的智能指针 但是由于没有gc的坑 引入智能指针

而智能指针经历了十几年发展的坑爹血泪史

第一阶段:

C++98中首次退出了auto_ptr 但是auto_ptr的设计存在重大缺陷 不建议使用

第二阶段:

C++官方在接下来的十几年中没有作为 有一帮牛人就生气了 觉得C++的库太简陋了 所以自己搞一个非官方社区 写了一个库叫boost boost库中就重新写了智能指针(注意boost库中其他很多其他实现的东西)_

scoped_ptr/scoped_array 防拷贝版本

scoped_ptr/scoped_array 引用计数版本

scoped/shared->new 析构delete

scoped_array/shared_array-> new[] 析构 delete[] 重载的是operator[]

weak_ptr

第三阶段:

C++11中引入智能指针 参考了boost的实现 微改了一下

其实C++11其他类似右值引用移动语句等等也是参考boost

unique_ptr(参考的scoped_ptr)

shared_ptr

weak_ptr

7°定制删除器

分析:C++11中没有xxx_array版本

那么如果是new[]出来的对象 怎么办?(了解)

定制删除器--(了解)

传一个实现释放方式的仿函数对象进去给智能指针

#include <iostream>
using namespace std;
#include <memory>

class A
{
public:
    ~A()
    {
        cout << "~A()" << endl;
    }

private:
    int _a1;
    int _a2;
};

template<class T>
struct DeleteArray
{
    void operator()(T* pa)
    {
        cout << "delete[] pa" << endl;
        delete[] pa;
     }
};

struct Free
{
    void operator()(void* pa)
    {
        cout << "free(p)" << endl;
        free(pa);
    }
};

struct Fclose
{
    void operator()(FILE* p)
    {
        cout << "fclose(p)" << endl;
        fclose(p);
    }
};


int main()
{
    std::shared_ptr<A> sp1(new A);
    //默认delete就是delete 无delete[]
    //此时需要传仿函数调delete[] 仿函数拿到对象 对象调用operator()
    std::shared_ptr<A> sp2(new A[10], DeleteArray<A>());//直接给一个匿名对象 释放10次
    std::shared_ptr<A> sp3((A*)malloc(sizeof(A)), Free());
    std::shared_ptr<FILE> sp4(fopen("test.txt","w"),Fclose());
    return 0;
}

8°智能指针与RAII

智能指针是RAII思想的一种应用的体现

本质RAII就是借助构造函数和析构函数来搞事情

因为析构函数和构造函数的特点都是自动调用

使用RAII思想设计的锁管理守卫 防止死锁出现

template<class Lock>
class LockGuard
{
public:
    LockGuard(Lock& lock)
        :_lk(lock)//锁不支持拷贝
    {
        _lk.lock();
    }
    //解锁也是解拷贝后的锁 但变成引用后 你解锁的锁就是原来的锁

    ~LockGuard()
    {
        cout << "解锁" << endl;
        _lk.unlock();
    }

private:
    //Lock _lk;
    Lock& _lk;//解决锁不能拷贝
    //注意这里是引用
};

void f()
{
    mutex mtx;
    mtx.lock();
    
    //func() //假设func函数有可能抛异常

    mtx.unlock();
    //t1线程先进来 t2线程锁住
    //此时t1抛异常 t1没有往下走 t2成了死锁
}

int main()
{
    try
    {
        f();
    }

    catch (exception& e)
    {
        cout << e.what() << endl;
    }
    return 0;
}

如果t1抛异常 t2就会成死锁 因此加守卫锁

f换成下面

int div()
{
    int a, b;
    cin >> a >> b;
    if (b == 0)
        throw invalid_argument("除0错误");

    return a / b;
}


void f()
{
    mutex mtx;
    LockGuard<mutex> lg(mtx);

    cout << div() << endl; //假设div可能抛异常
}

 

3°内存泄漏

  • 什么是内存泄漏?

内存泄漏一般是我们申请了资源 忘记释放 或者因为异常安全等问题没有释放

虚拟进程地址空间

mm_struct { void* _heap_start; void* _heap_end; ... }

物理地址

用页表按页为单位进行映射

多个进程的虚拟地址空间按页为单位跟物理地址建立映射关系

  • 内存泄漏的危害是什么?

如果我们申请了内存没有释放 如果进程正常结束 那么这个内存也会释放

一般程序碰到内存泄漏 重启后就OK了 长期运行 不能随便重启的程序 碰到内存泄漏危害非常大

比如操作系统 服务器上的服务 危害是:这些程序长期运行 不用的内存没有释放 内存泄漏

可用内存越来越少 导致服务很多操作失败(因为容器存数据 打开文件 创建套接字 发送数据等等都是需要内存的)

ps:一般后台服务器开发 如果出现内存泄漏 都是事故

  • 如何解决内存泄漏相关问题?

a.写C/C++代码时小心谨慎一点

b.不好处理的地方多用智能指针等等去管理(事前预防)

c.如果怀疑存在内存泄漏 或者已经出现 可以使用内存泄漏工具去检测(事后解决)

比如valgrind是一个linux下的强大工具

4°特殊类设计

  • 这个类只能在堆上创建对象

创建出的类对象只能在堆上

class HeapOnly
{
public:
    static HeapOnly* GetObj()
    {
        return new HeapOnly;
    }
    //静态的不需要对象就可以调用
private:
    //...
    HeapOnly()
    {}

    //C++98 声明成私有
    HeapOnly(const HeapOnly& );

    //C++11 delete 公有私有都ok
    HeapOnly(const HeapOnly&) = delete;
};
    
int main()
{
    //HeapOnly hp;
    //HeapOnly* p = new HeapOnly;
    //调不动private
    //HeapOnly* p = HeapOnly::GetObj();//都是new出来的 在堆上
    //内存泄漏
    //改为shared_ptr
    std::shared_ptr<HeapOnly> sp1(HeapOnly::GetObj());	
    
    //HeapOnly copy(*sp1);
    //拷贝构造 还是在栈上
    //因此拷贝构造也要封掉

    //三步
    //1:构造函数封死
    //2.写一个GetObj只new出对象
    //3.拷贝构造也封死
    return 0;
}
  • 只在栈上创建出对象的类
class StackOnly
{
public:
    static StackOnly GetObj()
    {
        return StackOnly();
    }

    /*void* operator new(size_t size) = delete;*/
    //禁掉operator new
private:
    StackOnly()
    {}
};

int main()
{
    StackOnly so = StackOnly::GetObj();
    //StackOnly* p = new StackOnly;//new不出来
    //或者禁掉operator new
    //但有缺陷
    //static StackOnly sso;在静态区 
    return 0;
}
  • 设计一个类 不能被拷贝

只在私有里声明 不定义

  • 设计一个类 不能被继承

a.构造函数私有化 父类的私有在子类中不可见

b.C++11 final

【C++】21.智能指针 完

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力的小恒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值