首页 > 编程语言 >【C++进阶】智能指针的使用及原理(1)

【C++进阶】智能指针的使用及原理(1)

时间:2024-11-08 16:16:30浏览次数:3  
标签:进阶 C++ new Date shared unique ptr 指针

1. 智能指针的使用场景分析

下面程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后面的delete没有得到执行,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本身也可能抛异常,连续的两个new和下面的Divide都可能会抛异常,让我们处理起来很麻烦。智能指针放到这样的场景里面就让问题简单多了。
double Divide(int a, int b)
{
    // 当b == 0时抛出异常
    if (b == 0)
    {
        throw "Divide by zero condition!";
    }
    else
    {
        return (double)a / (double)b;
    }
}

void Func()
{
    // 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array和array2没有得到释放。
    // 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。
    // 但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案
    // 是智能指针,否则代码太戳了
    int* array1 = new int[10];
    int* array2 = new int[10]; // 抛异常呢

    try
    {
        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;
    }
    catch (...)
    {
        cout << "delete []" << array1 << endl;
        cout << "delete []" << array2 << endl;
        delete[] array1;
        delete[] array2;
        throw; // 异常重新抛出,捕获到什么抛出什么
    }

    // ...
    cout << "delete []" << array1 << endl;
    delete[] array1;
    cout << "delete []" << array2 << endl;
    delete[] array2;
}

int main()
{
    try
    {
        Func();
    }
    catch (const char* errmsg)
    {
        cout << errmsg << endl;
    }
    catch (const exception& e)
    {
        cout << e.what() << endl;
    }
    catch (...)
    {
        cout << "未知异常" << endl;
    }
    return 0;
}

2. RAII和智能指针的设计思路

  • RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏,这⾥的资源可以是内存、⽂件指针、⽹络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
  • 智能指针类除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会想迭代器类⼀样,重载 operator*/operator->/operator[] 等运算符,方便访问资源。
template<class T>
class SmartPtr
{
public:
    // RAII
    SmartPtr(T* ptr)
        :_ptr(ptr)
    {}

    ~SmartPtr()
    {
        cout << "delete[] " << _ptr << endl;
        delete[] _ptr;
    }

    // 重载运算符,模拟指针的⾏为,⽅便访问资源
    T& operator*()
    {
        return *_ptr;
    }
    T* operator->()
    {
        return _ptr;
    }
    T& operator[](size_t i)
    {
        return _ptr[i];
    }
private:
    T* _ptr;
};

double Divide(int a, int b)
{
    // 当b == 0时抛出异常
    if (b == 0)
    {
        throw "Divide by zero condition!";
    }
    else
    {
        return (double)a / (double)b;
    }
}

void Func()
{
    // 这⾥使⽤RAII的智能指针类管理new出来的数组以后,程序简单多了
    SmartPtr<int> sp1 = new int[10];
    SmartPtr<int> sp2 = new int[10];
    for (size_t i = 0; i < 10; i++)
    {
        sp1[i] = sp2[i] = i;
    }
    int len, time;
    cin >> len >> time;
    cout << Divide(len, time) << endl;
}

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

    catch (const char* errmsg)
    {
        cout << errmsg << endl;
    }
    catch (const exception& e)
    {
        cout << e.what() << endl;
    }
    catch (...)
    {
        cout << "未知异常" << endl;
    }
    return 0;
}

3. C++标准库智能指针的使用

  • C++标准库中的智能指针都在<memory>这个头⽂件下⾯,我们包含<memory>就可以是使⽤了,智能指针有好⼏种,除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,原理上⽽⾔主要是解决智能指针拷⻉时的思路不同。
  • auto_ptr是C++98时设计出来的智能指针,他的特点是拷⻉时把被拷⻉对象的资源的管理权转移给拷⻉对象,这是⼀个⾮常糟糕的设计,因为他会到被拷⻉对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使⽤auto_ptr。其他C++11出来之前很多公司也是明令禁⽌使⽤这个智能指针的。
  • unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点的不⽀持拷⻉,只⽀持移动。如果不需要拷⻉的场景就⾮常建议使⽤他。
  • shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是⽀持拷⻉,也⽀持移动。如果需要拷⻉的场景就需要使⽤他了。底层是⽤引⽤计数的⽅式实现的。
  • weak_ptr是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上⾯的智能指针,他不⽀持RAII,也就意味着不能⽤它直接管理资源,weak_ptr的产⽣本质是要解决shared_pt的⼀个循环引用导致内存泄漏的问题。具体细节下⾯我们再细讲。
  • 智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。智能指针⽀持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调⽤对象,这个可调⽤对象中实现你想要的释放资源的⽅式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调⽤删除器去释放资源。因为new[]经常使用,所以为了简洁⼀点,
  • unique_ptr和shared_ptr都特化了⼀份[]的版本,使用时 unique_ptr<Date[]> up1(new Date[5]);shared_ptr<Date[]> sp1(new Date[5]); 就可以管理new []的资源。
  • template <class T, class... Args> shared_ptr<T> make_shared (Args&&... args);
  • shared_ptr 除了⽀持⽤指向资源的指针构造,还⽀持 make_shared 用初始化资源对象的值直接构造。
  • shared_ptr 和 unique_ptr 都⽀持了operator bool的类型转换,如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。
  • shared_ptr 和 unique_ptr 都得构造函数都使⽤explicit 修饰,防止普通指针隐式类型转换成智能指针对象。
struct Date
{
    int _year;
    int _month;
    int _day;
    Date(int year = 1, int month = 1, int day = 1)
        :_year(year)
        ,_month(month)
        ,_day(day)
    {}

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

int main()
{
    auto_ptr<Date> ap1(new Date);

    // 拷⻉时,管理权限转移,被拷⻉对象ap1悬空
    auto_ptr<Date> ap2(ap1);

    // 空指针访问,ap1对象已经悬空
    //ap1->_year++;
    unique_ptr<Date> up1(new Date);

    // 不⽀持拷⻉
    //unique_ptr<Date> up2(up1);
    // ⽀持移动,但是移动后up1也悬空,所以使⽤移动要谨慎
    unique_ptr<Date> up3(move(up1));
    shared_ptr<Date> sp1(new Date);

    // ⽀持拷⻉
    shared_ptr<Date> sp2(sp1);
    shared_ptr<Date> sp3(sp2);
    cout << sp1.use_count() << endl;
    sp1->_year++;
    cout << sp1->_year << endl;
    cout << sp2->_year << endl;
    cout << sp3->_year << endl;

    // ⽀持移动,但是移动后sp1也悬空,所以使⽤移动要谨慎
    shared_ptr<Date> sp4(move(sp1));
    return 0;
}
template<class T>
void DeleteArrayFunc(T* ptr)
{
    delete[] ptr;
}

template<class T>
class DeleteArray
{
public:
    void operator()(T* ptr)
    {
        delete[] ptr;
    }
};

class Fclose
{
public:
    void operator()(FILE* ptr)
    {
        cout << "fclose:" << ptr << endl;
        fclose(ptr);
    }
};

int main()
{
    // 这样实现程序会崩溃
    // unique_ptr<Date> up1(new Date[10]);
    // shared_ptr<Date> sp1(new Date[10]);
    // 解决⽅案1
    // 因为new[]经常使⽤,所以unique_ptr和shared_ptr
    // 实现了⼀个特化版本,这个特化版本析构时⽤的delete[]
    unique_ptr<Date[]> up1(new Date[5]);
    shared_ptr<Date[]> sp1(new Date[5]);

    // 解决⽅案2
    // 仿函数对象做删除器
    //unique_ptr<Date, DeleteArray<Date>> up2(new Date[5], DeleteArray<Date>());
    // unique_ptr和shared_ptr⽀持删除器的⽅式有所不同
    // unique_ptr是在类模板参数⽀持的,shared_ptr是构造函数参数⽀持的
    // 这⾥没有使⽤相同的⽅式还是挺坑的
    // 使⽤仿函数unique_ptr可以不在构造函数传递,因为仿函数类型构造的对象直接就可以调⽤
    // 但是下⾯的函数指针和lambda的类型不可以
    unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
    shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());

    // 函数指针做删除器
    unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
    shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);

    // lambda表达式做删除器
    auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
    unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
    shared_ptr<Date> sp4(new Date[5], delArrOBJ);

    // 实现其他资源管理的删除器
    shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
    shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
        cout << "fclose:" << ptr << endl;
        fclose(ptr);
    });
    return 0;
}

int main()
{
    shared_ptr<Date> sp1(new Date(2024, 9, 11));
    shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
    auto sp3 = make_shared<Date>(2024, 9, 11);
    shared_ptr<Date> sp4;

    // if (sp1.operator bool())
    if (sp1)
    cout << "sp1 is not nullptr" << endl;
    if (!sp4)
        cout << "sp1 is nullptr" << endl;

    // 报错
    shared_ptr<Date> sp5 = new Date(2024, 9, 11);
    unique_ptr<Date> sp6 = new Date(2024, 9, 11);
    return 0;
}

4. 智能指针的原理

  • 下面我们模拟实现了auto_ptr和unique_ptr的核心功能,这两个智能指针的实现比较简单,大家了解⼀下原理即可。auto_ptr的思路是拷贝时转移资源管理权给被拷贝对象,这种思路是不被认可的,也不建议使⽤。unique_ptr的思路是不支持拷贝。
  • 大家重点要看看shared_ptr是如何设计的,尤其是引用计数的设计,主要这⾥⼀份资源就需要⼀个 引用计数,所以引用计数才用静态成员的方式是无法实现的,要使⽤堆上动态开辟的⽅式,构造智能指针对象时来⼀份资源,就要new⼀个引用计数出来。多个shared_ptr指向资源时就++引用计数,shared_ptr对象析构时就引⽤计数,引⽤计数减到0时代表当前析构的shared_ptr是最后⼀个管理资源的对象,则析构资源。

namespace bit
{
    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;
    };

    template<class T>
    class unique_ptr
    {
    public:
        explicit 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;
        unique_ptr(unique_ptr<T>&& sp)
            :_ptr(sp._ptr)
        {
            sp._ptr = nullptr;
        }

        unique_ptr<T>& operator=(unique_ptr<T>&& sp)
        {
            delete _ptr;
            _ptr = sp._ptr;
            sp._ptr = nullptr;
        }
    private:
        T* _ptr;
    };

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

    template<class D>
    shared_ptr(T* ptr, D del)
        : _ptr(ptr)
        , _pcount(new int(1))
        , _del(del)
    {}

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

    void release()
    {
        if (--(*_pcount) == 0)
        {
            // 最后⼀个管理的对象,释放资源
            _del(_ptr);
            delete _pcount;
            _ptr = nullptr;
            _pcount = nullptr;
        }
    }

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

    ~shared_ptr()
    {
        release();
    }

    T* get() const
    {
        return _ptr;
    }

    int use_count() const
    {
        return *_pcount;
    }

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

    T* operator->()
    {
        return _ptr;
    }
private:
    T* _ptr;
    int* _pcount;
    //atomic<int>* _pcount;
    function<void(T*)> _del = [](T* ptr) {delete ptr; };
};

    // 需要注意的是我们这⾥实现的shared_ptr和weak_ptr都是以最简洁的⽅式实现的,
    // 只能满⾜基本的功能,这⾥的weak_ptr lock等功能是⽆法实现的,想要实现就要
    // 把shared_ptr和weak_ptr⼀起改了,把引⽤计数拿出来放到⼀个单独类型,shared_ptr
    // 和weak_ptr都要存储指向这个类的对象才能实现,有兴趣可以去翻翻源代码
    template<class T>
    class weak_ptr
    {
    public:
        weak_ptr()
        {}

        weak_ptr(const shared_ptr<T>& sp)
            :_ptr(sp.get())
        {}

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

int main()
{
    bit::auto_ptr<Date> ap1(new Date);

    // 拷⻉时,管理权限转移,被拷⻉对象ap1悬空
    bit::auto_ptr<Date> ap2(ap1);

    // 空指针访问,ap1对象已经悬空
    //ap1->_year++;
    bit::unique_ptr<Date> up1(new Date);

    // 不⽀持拷⻉
    //unique_ptr<Date> up2(up1);
    // ⽀持移动,但是移动后up1也悬空,所以使⽤移动要谨慎
    bit::unique_ptr<Date> up3(move(up1));
    bit::shared_ptr<Date> sp1(new Date);

    // ⽀持拷⻉
    bit::shared_ptr<Date> sp2(sp1);
    bit::shared_ptr<Date> sp3(sp2);
    cout << sp1.use_count() << endl;
    sp1->_year++;
    cout << sp1->_year << endl;
    cout << sp2->_year << endl;
    cout << sp3->_year << endl;
    return 0;
}

这篇文章介绍了智能指针的相关知识,下篇我们继续讨论智能指针,欢迎评论交流!

标签:进阶,C++,new,Date,shared,unique,ptr,指针
From: https://blog.csdn.net/2402_82689232/article/details/143626002

相关文章

  • c++执行shell操作
     message("----->>>>>shelldemo/CMakeLists.txt")#收集当前目录下所有的.cpp文件aux_source_directory(.DIR_SRCS)#添加可执行文件add_executable(shelldemo${DIR_SRCS})#链接需要的库target_link_libraries(shelldemo${LIB_LIB})/shelldemo/CMakeList......
  • 【C++篇】无序中的法则:探索 STL之unordered_map 与 unordered_set容器的哈希美学
    文章目录C++`unordered_map`和`unordered_set`容器详解前言第一章:`unordered_map`和`unordered_set`的概念1.1`unordered_map`和`unordered_set`的定义1.2与`map`、`set`的区别第二章:`unordered_map`和`unordered_set`的构造方法2.1`unordered_map`......
  • Chromium 进程降权和提权模拟示例c++
     一、背景知识概念参考微软链接:强制完整性控制-Win32应用程序|Microsoft学习授权)(模拟级别-Win32apps|MicrosoftLearnDuplicateTokenEx函数(securitybaseapi.h)-Win32apps|MicrosoftLearn本文主要演示 low,medium,high,andsystem四种权限创建......
  • C++之OpenCV入门到提高004:Mat 对象的使用
    一、介绍今天是这个系列《C++之Opencv入门到提高》得第四篇文章。这篇文章很简单,介绍如何使用Mat对象来实例化图像实例,了解它的构造函数和常用的方法,这是基础,为以后的学习做好铺垫。虽然操作很简单,但是背后有很多东西需要我们深究,才能做到知其然知其所以然。OpenCV具......
  • C++ 模板参数的两种类型转换
    与非模板函数一样,我们在一次调用中传递给函数模板的实参被用来初始化函数的形参。如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则。只有很有限的几种类型转换会自动地应用于这些实参。编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。与往常一......
  • C++ 模板显式实例化
    //template.hpptemplate<typenameT>classDylan{public:Dylan(Tt);Tm_data;};//template.cpp#include"template.hpp"template<typenameT>Dylan<T>::Dylan(Tt){m_data=t;}templateclassDylan<int&g......
  • 【模块一】kubernetes容器编排进阶实战之k8s基础概念
    kubernetes基本介绍kubernetes组件简介   -master:       主人,并不部署服务,而是管理salve节点。      后期更名为:controllplane,控制面板。         etcd:      2379(客户端通信)、2380(集群内部通信)         ......
  • C++关于DLL导出模板类和模板函数
    这两天写了个Dll,要导出普通类中的模板函数,稍微查了一下,没查到具体资料。自己根据C++模板的编译原理,推断出应该要源码放在头文件中直接导出,查了下接触的OpenSource项目,确实如此。这里记录一下,方便下次查阅。1、宏定义说明:#ifdefDLL_PROJECT#defineTEMPLATE_IM_EXPORT__decl......
  • c++多态学习:多态含义与使用
    目录 多态的概念多态的定义多态的实现注意事项 多态的概念多态是面向对象编程中的一个重要概念,它指的是同一个行为具有多个不同表现形式或形态的能力。在C++中,多态主要通过虚函数来实现,允许将子类类型的指针赋值给父类类型的指针,并在运行时根据实际对象类型调用相......
  • 大模型(LLMs)学习笔记——进阶知识
    一.生成式大模型简介1.什么是生成式大模型前排提示,文末有大模型AGI-CSDN独家资料包哦!生成式大模型(一般简称大模型LLMs)是指能用于创作新内容,例如文本、图片、音频以及视频的一类深度学习模型。相比普通深度学习模型,主要有两点不同:模型参数量更大,参数量都在Billion......