✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:c++篇–CSDN博客

在这里插入图片描述

智能指针

一.为什么需要智能指针?

先来分析一下下面这段代码有没有什么内存方面的问题?

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

void f1(){
    pair<string, string> *p1 = new pair<string, string>;
    pair<string, string> *p2 = new pair<string, string>;
    try
    {
        div();
    }
    catch(...){
        delete p1;
        cout << "delete: " << p1 << endl;
        throw;
    }

    delete p1;
    cout << "delete: " << p1 << endl;
}

int main(){
    try{
        f1();
    }
    catch(const exception& e){
        cout << e.what() << endl;
    }

    return 0;
}

在这里插入图片描述

通过分析上面的代码,可以发现,这是一个典型的内存泄漏问题!

上面代码中内存泄露的具体情况:

  • 情况一:异常发生时的泄露
    • div()抛出异常;
    • 但是catch块中只释放了p1p2并没有被释放,造成了内存泄漏
  • 情况二:正常执行时的泄露
    • 即使div()不抛出异常;
    • 正常流程中也只释放了p1p2仍然没有被释放,造成了内存泄漏

正确的做法就需要我们手动管理内存了:

void f1(){
    pair<string, string> *p1 = new pair<string, string>;
    pair<string, string> *p2 = new pair<string, string>;
    
    try{
        div();
    }
    catch(...){
        delete p1;  // 释放p1
        delete p2;  // 必须释放p2
        cout << "delete: " << p1 << endl;
        cout << "delete: " << p2 << endl;
        throw;
    }

    delete p1;  // 正常情况释放p1
    delete p2;  // 必须释放p2
    cout << "delete: " << p1 << endl;
    cout << "delete: " << p2 << endl;
}

很显然,手动管理内存容易带来各种各样的问题,不仅仅是内存泄漏,还有代码复杂,需要在多个地方管理内存等

既然如此,我们就需要一种便捷的方式来替代手动管理内存,这就是为什么需要智能指针的原因:

智能指针可以帮助我们自动管理内存生命周期,代码简洁,防止内存管理错误等

二.智能指针的原理和使用

1.RAII原理

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

基本思想

  • 在对象构造时获取资源:在构造函数中获取资源,控制对资源的访问使之在对象的生命周期内始终保持有效;
  • 在对象析构时释放资源:当作用域结束时,自动调用析构函数,在析构函数中释放资源;
  • 实际上是把管理一份资源的责任托管给了一个对象,利用对象的生命周期来自动管理资源!!!

这种做法有两种好处

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

智能指针的原理就是RAII思想,是RAII的典型应用

使用RAII思想设计一个SmartPtr

template <class T>
class SmartPtr{
public:
    // 构造函数获取资源
    SmartPtr(T *ptr = nullptr)
        : _ptr(ptr)
    {}
    
    // 析构函数释放资源
    ~SmartPtr(){
        if (_ptr){
            std::cout << "delete: " << _ptr << std::endl;
            delete _ptr;
        }
    }

private:
    T *_ptr;
};

还是用上面的那份测试代码,这不过这次用我们自己写好的SmartPtr类来管理资源:

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

void f2(){
  	SmartPtr<pair<string, string>> p1(new pair<string, string>("aaa", "111"));
    SmartPtr<pair<string, string>> p2(new pair<string, string>("bbb", "222"));

    div();
}

int main(){
    try{
        f2();
    }
    catch(const exception& e){
        cout << e.what() << endl;
    }

    return 0;
}

在这里插入图片描述

这次使用SmartPtr类来管理p1p2,即使我们没有显示的释放资源,最后在进程结束时,也会自动释放掉,这样就不会造成内存泄露的问题了,还能简化代码。

2.智能指针的使用

上面的SmartPtr类还不能称其为智能指针,因为他还不具备指针的行为因为指针可以解引用,也可以通过->去访问所指空间中的内容,因此还需要在SmartPtr类中增加*->运算符重载,才可以像指针一样使用。

operator*()——解引用运算符

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

作用

  • 返回指针指向的对象的引用
  • 访问的是指针指向的对象本身

operator->()——箭头运算符

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

作用

  • 返回指针本身
  • 访问的是对象的成员函数或成员变量

测试代码

int main(){
    SmartPtr<pair<string, string>> p1(new pair<string, string>("aaa", "111"));
    SmartPtr<pair<string, string>> p2(new pair<string, string>("bbb", "222"));

    cout << ((*p1).first) << " " << ((*p1).second) << endl;
    // 实际是p1.operator*().first
    cout << (p2->first) << " " << (p2->second) << endl;
    // 实际是p2.operator->()->first
    return 0;
}

在这里插入图片描述

注意

使用*解引用后,返回的是一个pair对象,所以需要用.继续访问成员变量;

而使用->后,返回的是一个pair对象的指针,所以需要用->继续访问成员变量,但是为了可读性一般都是省略第二个->,编译器会再自动调用,不需要真的写两个->

3.智能指针的拷贝问题

先来看一个测试(继续用上面写好的SmartPtr类):

int main(){
    SmartPtr<pair<string, string>> p1(new pair<string, string>("aaa", "111"));
    SmartPtr<pair<string, string>> p2(new pair<string, string>("bbb", "222"));
    SmartPtr<pair<string, string>> p3(p1);

    return 0;
}

在这里插入图片描述

很明显当我们尝试用一个对象去拷贝构造一个新对象时,发生了错误,这是一个典型的浅拷贝导致的双重释放问题;

问题所在

  • 首先p1指向内存空间0x5598ec3dc2b0
  • 因为SmartPtr类中没有定义拷贝构造函数,所以编译器会自动生成一个默认的拷贝构造函数,并进行浅拷贝
  • 所以用p1拷贝构造p3时,发生浅拷贝,使p1p3指向了同一个内存地址;
  • 当程序结束时,p3析构,释放内存0x5598ec3dc2b0p1析构,再次释放同一个内存地址,导致双重释放;

这个问题其实有好几种解决方案,并且也可以算得上是智能指针的发展历程了。

这里直接展示库里的几个智能指针:

1.C++98中:auto_ptr(已废弃)

auto_ptr<string> a1(new string("hello"));
auto_ptr<string> a2(a1);        // 移交管理权,a1已经置为nullptr
// cout << a1->size() << endl;    // 错误使用

解决方式:通过移交管理权导致原指针失效(原指针置为空),弊端也很明显,容易误用原指针,造成悬垂指针(所以现在很少用,基本废弃了);

2.C++11中:unique_ptr(独占所有权)

unique_ptr<string> u1(new string("hello"));
//unique_ptr<string> u2(u1);

unique_ptr<string> u3(move(u1));   // 移动构造 移动后u1置为nullptr
cout << (*u3) << endl;  // 输出hello

解决方式:明确禁止拷贝,防止误用;强制使用移动语义(可以移动右值),语义更清晰;并且是在编译时检查,更加安全;

在这里插入图片描述

3.C++11中:shared_ptr(共享所有权)

shared_ptr<string> s1(new string("hello"));
shared_ptr<string> s2(s1);
cout << (*s1) << " " << s1 << endl;
cout << (*s2) << " " << s2 << endl;

解决方式:允许多个对象共享同一分资源,通过引用计算管理,最后一个对象销毁时,自动释放资源;

在这里插入图片描述

上面三种智能指针从auto_ptr的尝试解决拷贝问题,但语义不清晰;到unique_ptr的明确禁止,强制移动语义;再到shared_ptr允许共享等,智能指针的设计也从简单到复杂,所以也算得上是智能指针的发展历程了。

三.智能指针的模拟实现

1.auto_ptr

重点是实现拷贝构造和拷贝赋值,像构造,析构,*->重载其实和上面的SmartPtr类差不多

namespace MySmartPtr{
    template<typename T>
    class auto_ptr{
    public:
        auto_ptr(T* ptr = nullptr)
        :_ptr(ptr)
        {}

        ~auto_ptr(){
            if(_ptr){
                std::cout << "delete: " << _ptr << std::endl;
                delete _ptr;
            }
        }
        
        // 拷贝构造
        auto_ptr(auto_ptr<T>& a)
        :_ptr(a._ptr)            // 移交管理权
        {
            a._ptr = nullptr;    // 原对象置为空
        }
        
        // 拷贝赋值
        auto_ptr<T>& operator=(auto_ptr<T>& a){
            if(_ptr != a._ptr){
                _ptr = a._ptr;        // 移交管理权
                a._ptr = nullptr;     // 原对象置为空
            }
            return *this;
        }

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

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

    private:
        T *_ptr;
    };
}

测试代码

int main(){
    MySmartPtr::auto_ptr<string> a1(new string("hello"));
    cout << (*a1) << " " << (a1.GetPtr()) << endl;  //打印a1的资源以及资源地址
    MySmartPtr::auto_ptr<string> a2(a1);
    cout << (*a2) << " " << (a2.GetPtr()) << endl;  // 打印移交后的a2的资源以及资源地址
    return 0;
}

在这里插入图片描述

2.unique_ptr

unique_ptr因为明确禁止拷贝构造和拷贝赋值,所以对于这两个函数可以直接删除;主要是实现移动语义(移动构造和移动赋值),实现思路也很简单,交换资源,然后再将被移动对象置为空即可,但是有一点,在移动赋值这里需要先检查一下自赋值;

namespace MySmartPtr{
    template<typename T>
    class unique_ptr{
    public:
        unique_ptr(T* ptr = nullptr)
        :_ptr(ptr)
        {}

        ~unique_ptr(){
            if(_ptr){
                std::cout << "delete: " << _ptr << std::endl;
                delete _ptr;
            }
        }

        unique_ptr(unique_ptr<T> &u) = delete;                     // 删除拷贝构造
        unique_ptr<T> &operator=(unique_ptr<T> &u) = delete;       // 删除拷贝赋值

        // 移动构造
        unique_ptr(unique_ptr<T> &&u)
        :_ptr(u._ptr)                 
        {
            u._ptr = nullptr;
        }

        // 移动赋值
        unique_ptr<T> &operator=(unique_ptr<T> &&u){
            // 自赋值检查
            if (_ptr != u._ptr){
                _ptr = u._ptr;
                u._ptr = nullptr;
            }
            return *this;
        }

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

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

    private:
        T *_ptr;
    };
}

测试代码

int main(){
    MySmartPtr::unique_ptr<string> u1(new string("hello"));
    cout << (*u1) << " " << (u1.GetPtr()) << endl;
    MySmartPtr::unique_ptr<string> u2(move(u1));    // 移动拷贝
    cout << (*u2) << " " << (u2.GetPtr()) << endl;
    MySmartPtr::unique_ptr<string> u3(new string);
    u3 = move(u2);                                  // 移动赋值
    cout << (*u3) << " " << (u3.GetPtr()) << endl;
    u3 = move(u3);                                  // 自赋值
    cout << (*u3) << " " << (u3.GetPtr()) << endl;
    return 0;
}

在这里插入图片描述

3.shared_ptr

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

  • shared_ptr在其内部,给每个资源都维护了一份计数器,用来记录该资源被几个对象共享
  • 对象被销毁时(也就是调用析构函数时),就说明自己已经不再使用该资源了,对象的引用计数减一
  • 如果减完后引用计数变为0,就说明自己是最后一个使用该资源的对象,必须释放该资源
  • 如果减完后引用计数不为0,就说明除了自己以外还有其他的对象在使用该份资源,不能释放资源,否则其他对象就变成野指针了;
namespace MySmartPtr{
    template<typename T>
    class shared_ptr{
    public:
        shared_ptr(T* ptr = nullptr)
        :_ptr(ptr)
        ,_count(new int(1))
        {}

        ~shared_ptr(){
            std::cout << "~shared_ptr()" << std::endl;
            // 析构时,先将计数器减一,然后再判断是否需要释放
            if (--(*_count) == 0){
                std::cout << "delete: " << _ptr << std::endl;
                delete _ptr;
                delete _count;
            }
        }

        // 拷贝构造
        shared_ptr(const shared_ptr<T> &s)
        :_ptr(s._ptr)
        ,_count(s._count)
        {
            ++(*_count); //同一个动态计数器,当计数器发生改变,所有共享这份资源的对象都能看到
        }

        // 拷贝赋值
        shared_ptr<T> &operator=(const shared_ptr<T> &s){
            if (_ptr != s._ptr){         
                if (--(*_count) == 0){   
                    delete _ptr;
                    delete _count;
                }

                _ptr = s._ptr;
                _count = s._count;
                ++(*_count);
            }
            return *this;
        }

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

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

        int& use_count() const {
            return *_count;
        }

    public:
        T *_ptr;
        int *_count;    // 指针形式的计数器
    };
}

几个细节点:

1.为什么要用指针形式的计数器

关键点

  • int* _count 指向堆上的一个int值

  • 所有shared_ptr对象都指向同一个计数器

  • 当计数器变化时,所有对象都能看到

2.拷贝赋值时的关键点

自赋值检查

  • 检查当前对象和赋值对象是否指向同一个资源

  • 避免自赋值时的资源释放问题

  • 这是关键的安全检查

引用计数管理

  • 先将当前对象的引用计数减1

  • 如果减到0,说明没有其他对象共享这个资源,需要释放

  • 释放包括:原始指针_ptr和计数器_count

资源转移

  • 将新资源的指针和计数器赋值给当前对象

  • 增加新资源的引用计数

测试代码

int main(){
    MySmartPtr::shared_ptr<string> s1(new string("hello"));
    cout << (*(s1.GetPtr())) << " " << (s1.GetPtr()) << " " << (s1.use_count()) << endl;
    MySmartPtr::shared_ptr<string> s2(s1);                                          // 拷贝构造
    cout << (*(s2.GetPtr())) << " " << (s2.GetPtr()) << " " << (s2.use_count()) << endl;
    MySmartPtr::shared_ptr<string> s3(new string);
    s3 = s2;                                                                        // 拷贝赋值
    cout << (*(s3.GetPtr())) << " " << (s3.GetPtr()) << " " << (s3.use_count()) << endl;
    s3 = s3;                                                                        // 自赋值
    cout << (*(s3.GetPtr())) << " " << (s3.GetPtr()) << " " << (s3.use_count()) << endl;

    return 0;
}

在这里插入图片描述

四.循环引用和weak_ptr

1.循环引用现象

通过一份测试代码来看一个现象:

struct ListNode{
    ListNode()
    {}

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

    int _data;
    shared_ptr<ListNode> _next;
    shared_ptr<ListNode> _prev;
};

int main(){
    shared_ptr<ListNode> node1(new ListNode);
    shared_ptr<ListNode> node2(new ListNode);
    cout << node1.use_count() << endl;
    cout << node2.use_count() << endl;
    // node1->_next = node2;
    // node2->_prev = node1;
    cout << node1.use_count() << endl;
    cout << node2.use_count() << endl;
    return 0;
}

先把中间那两句注释掉看一下结果:

在这里插入图片描述

最后两个节点都会正常释放掉,但是如果中间两句取消注释后,结果就是:

在这里插入图片描述

这一次两个节点的引用计数都变为2,并且最后没有自动释放掉;

这就是shared_ptr中的一个特殊场景,循环引用问题。

循环引用分析

  • node1node2两个智能指针对象指向两个节点,引用计数变成1;
  • node1_next指向node2node2_prev指向node1,引用计数变成2;
  • node1node2析构时,引用计数减一变成1,此时node1_next还指向node2node2_prev还指向node1
  • 也就是说_next管着node2_prev管着node1
  • 那什么时候_prev析构?——node2析构时,_prev就会析构;
  • 那什么时候node2析构呢?——_next析构,node2就会析构;
  • 那什么时候_next析构呢?——node1析构,_next就会析构;
  • 那什么时候node1析构呢?——_prev析构,node1就会析构;
  • 由于在进程结束时引用计数不为 0,所以两个节点都不会被销毁,造成内存泄漏
  • 上面这种情况就是循环引用,谁也不会释放;

解决方式就是使用weak_ptr来设置节点的_next_prev

2.weak_ptr

weak_ptr的本质

weak_ptrshared_ptr弱引用,它有以下几个关键特性:

  • 不增加引用计数weak_ptr指向一个对象时,不会增加该对象的引用计数;
  • 不用有对象weak_ptr不拥有对象的所有权,只是观察者
  • 可以检测对象是否还存在:通过expired()方法检查对象是否已经被销毁;

weak_ptr解决循环引用

_prev_next改成weak_ptr

struct ListNode{
    ListNode()
    {}

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

    int _data;
    weak_ptr<ListNode> _next;
    weak_ptr<ListNode> _prev;
};

现在的情况是:

  • node1_next指向node2时,node2的引用计数不变
  • node2_prev指向node1时,node1的引用计数不变
  • node1node2的引用计数始终保持1不变;
  • 当函数结束时,node1node2调用析构,引用计数减一变为0,此时两个节点被释放;

在这里插入图片描述

weak_ptr 解决循环引用的核心原理

  • 打破引用计数的循环:weak_ptr 不增加引用计数,所以不会形成循环

  • 提供安全的观察机制:可以检测对象是否还存在,避免访问已销毁的对象

  • 保持对象生命周期管理:只有 shared_ptr 才真正拥有对象,weak_ptr 只是观察者

3.模拟实现一个weak_ptr

namespace MySmartPtr{
    template<typename T>
    class weak_ptr{
    public:
        weak_ptr()
        :_ptr(nullptr)
        {}

        weak_ptr(const shared_ptr<T> &s)
        :_ptr(s.GetPtr())
        {}
        
        // shared_ptr -> weak_ptr 的拷贝构造
        weak_ptr<T> &operator=(const shared_ptr<T> &s){
            _ptr = s.GetPtr();
            return *this;
        }
        
        // weak_ptr -> weak_ptr 的拷贝构造
        weak_ptr<T> &operator=(const weak_ptr<T> &w){
            _ptr = w._ptr;
            return *this;
        }

        ~weak_ptr(){
            // weak_ptr 不负责删除对象,只是观察者
        }

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

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

    private:
        T *_ptr;
    };
}

weak_ptr 的设计原则

  • 不增加引用计数weak_ptr 不会影响对象的生命周期

  • 不删除对象:只有 shared_ptr 负责删除对象

  • 只是观察者weak_ptr 只是观察对象是否存在,不拥有对象

测试代码

struct ListNode{
    ListNode()
    {}

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

    int _data;
    MySmartPtr::weak_ptr<ListNode> _next;
    MySmartPtr::weak_ptr<ListNode> _prev;
};

int main(){
    MySmartPtr::shared_ptr<ListNode> node1(new ListNode);
    MySmartPtr::shared_ptr<ListNode> node2(new ListNode);
    cout << node1.use_count() << endl;
    cout << node2.use_count() << endl;
    node1->_next = node2;
    node2->_prev = node1;
    cout << node1.use_count() << endl;
    cout << node2.use_count() << endl;
    return 0;
}

在这里插入图片描述

weak_ptr 除了可以解决循环引用外,还可以用来安全观察对象是否存在,避免悬挂指针等各种引用场景,是现代C++中非常重要的一个智能指针!

补充内容

定制删除器

1.什么是定制删除器?

定制删除器就是自定义智能指针销毁对象时调用的函数,而不是默认的delete

2.为什么需要定制删除器?

因为不是所有资源都可以像堆内存一样用delete销毁;

比如下面这些资源

// 文件句柄 需要使用fclose()
FILE* file = fopen("test.txt", "r");

// 数组 需要使用delete[]
int* array = new int[100];

// C风格内存  需要使用free()
void* men = malloc(1024);

// 除此之外,还有网络连接,数据库连接等各种资源

正是因为定制删除器的存在,可以让智能指针管理任何类型的资源,而不单单是堆内存

3.常用的实现方式

在这里插入图片描述

在标准库中,其中一个构造函数可以使用定制删除器,通过第二个模板参数D del(可调用对象)来传递删除器;

  • 函数指针
void file_deleter(FILE* fp) {
    if (fp) fclose(fp);
}

shared_ptr<FILE> file_ptr(fopen("test.txt", "r"), file_deleter);
  • Lambda表达式
shared_ptr<FILE> file_ptr(fopen("test.txt", "r"), 
    [](FILE* fp) { if (fp) fclose(fp); });
  • 仿函数(函数对象)
struct ArrayDeleter {
    void operator()(int* p) {
        delete[] p;
    }
};

shared_ptr<int> array_ptr(new int[100], ArrayDeleter());
  • 标准库提供的删除器
// unique_ptr 有专门的数组版本
unique_ptr<int[]> array_ptr(new int[100]);  // 自动用 delete[]

// shared_ptr 需要手动指定
shared_ptr<int> array_ptr(new int[100], default_delete<int[]>());

4.给自己写的shared_ptr实现定制删除器功能

设计思路

shared_ptr类中添加一个成员变量:由包装器包装的可调用对象_del

std::function<void(T *)> _del = [](T *ptr){
    if(ptr){
        delete ptr;
    } 
};
  • 将所有的删除器都包装成std::function
  • 支持函数指针,lambda表达式,函数对象等各种可调用对象;
  • 同时利用lambda表达式提供默认的delete行为;

增加一个新的构造函数

template<typename D>
shared_ptr(T *ptr, D del) 
: _ptr(ptr)
, _count(new int(1))
, _del(del) 
{}
  • 通过模板参数D来自动推导删除器类型;
  • 没有自定义删除器时,使用默认的delete
  • 编译时类型检查,避免运行时错误;

同时更改析构函数中的delete行为

~shared_ptr(){
    std::cout << "~shared_ptr()" << std::endl;
    if (--(*_count) == 0){
        std::cout << "delete: " << _ptr << std::endl;
        // delete _ptr;
        _del(_ptr);
        delete _count;
    }
}

以及拷贝构造和拷贝赋值中增加删除器的拷贝

// 拷贝构造
shared_ptr(const shared_ptr<T> &s)
:_ptr(s._ptr)
,_count(s._count)
,_del(s._del)        // 拷贝删除器
{
    ++(*_count); 
}

// 拷贝赋值
shared_ptr<T> &operator=(const shared_ptr<T> &s){
    if (_ptr != s._ptr){         
        if (--(*_count) == 0){
            // delete _ptr;
            _del(_ptr);
            delete _count;
        }

        _ptr = s._ptr;
        _count = s._count;
        _del = s._del;  // 拷贝删除器
        ++(*_count);
    }
    return *this;
}

最后用一段代码来进行测试:

struct ArrayDelete{
    void operator()(int *p){
        if(p){
            cout << "array_delete" << endl;
            delete[] p;
        }
    }
};
int main()
{
    MySmartPtr::shared_ptr<int> array_ptr(new int[100], ArrayDelete());
    MySmartPtr::shared_ptr<string> s1(new string("hello"));
    MySmartPtr::shared_ptr<string> s2(s1);
    return 0;
}

在这里插入图片描述

以上就是关于C++智能指针的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐