C++ 智能指针

文章目录

C++ 智能指针

为什么需要智能指针?

我们看下面这个代码有什么问题,提示请看Func函数接口:

cpp 复制代码
#include <iostream>
#include <stdexcept>
#include <exception>

int div()
{
    int a,b;
    std::cin >> a >> b;

    if(b == 0)
    {
        throw std::invalid_argument("除0错误");
    }

    return a / b;
}

void Func()
{
    int *p1 = new int(1);
    int *p2 = new int(2);

    std::cout << div() << std::endl;

    delete p1;
    delete p2;
}

int main()
{

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

通过我们对C++的学习和了解,我们分析一下上面代码的问题:

  • p1 new的时候抛异常,没造成内存泄漏。因为p1申请空间未成功;
  • p2 new的时候抛异常,造成p1的内存泄漏。因为抛异常后p1的delete操作无法执行;
  • div抛异常,造成p1、p2的内存泄漏。因为抛异常后p1、p2的delete操作都无法执行。

内存泄漏

什么是内存泄漏,内存泄漏的危害

  • 内存泄漏:程序申请了内存,但因为设计错误或异常路径,失去了对该内存的控制,导致无法释放。不是内存物理消失,而是这部分内存无法再被使用。
  • 危害:长期运行的程序,如操作系统、后台服务,内存泄漏会导致响应越来越慢,最终卡死。
cpp 复制代码
void Func()
{
    int *p1 = new int(1);
    int *p2 = new int(2);

    std::cout << div() << std::endl;

    delete p1;
    delete p2;
}

抛异常之后就会导致申请的内存无法释放。

内存泄漏分类

  1. 堆内存泄漏:malloc/calloc/realloc/new 申请,未用 free/delete 释放。
  2. 系统资源泄漏:套接字、文件描述符、管道、锁等未释放,可能导致系统性能下降或不稳定。

如何检测内存泄漏

在linux下内存泄漏检测:linux下几款内存泄漏检测工具

在windows下使用第三方工具:VLD工具说明

其他工具:内存泄漏工具比较

如何避免内存泄漏

  1. 良好编码规范,申请与释放匹配。但异常场景仍可能出问题。
  2. 采用 RAII 思想或智能指针管理资源。
  3. 公司内部私有内存管理库。
  4. 出问题后用泄漏检测工具事后查错。

总结一下:

内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。

智能指针的使用及原理

RAII

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

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

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

使用RAII思想我们重新设计一些刚才那个代码:

cpp 复制代码
template <class T>
class SmartPtr{
public:
    SmartPtr(T* ptr = nullptr) : _ptr(ptr) {}

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

    // 简化一下,禁止拷贝
    SmartPtr(const SmartPtr&) = delete;
    SmartPtr& operator=(const SmartPtr&) = delete;
private:
    T* _ptr;
};

int div()
{
    int a,b;
    std::cin >> a >> b;

    if(b == 0)
    {
        throw std::invalid_argument("除0错误");
    }

    return a / b;
}

void Func()
{
    // int *p1 = new int(1);
    // int *p2 = new int(2);
    SmartPtr<int> p1(new int(1));
    SmartPtr<int> p2(new int(2));

    std::cout << div() << std::endl;

    // delete p1;
    // delete p2;
}

int main()
{

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

这样当b输入0时就不会导致内存泄漏了,因为抛异常的时候p2、p1 依次析构,资源自动释放。

智能指针的原理

但是仅有 RAII 还不够,上述的SmartPtr 不能称为智能指针。因为智能指针还需要重载:

  • operator*:解引用;
  • operator->:访问成员。

因此AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。

cpp 复制代码
template <class T>
class SmartPtr{
public:
    SmartPtr(T* ptr = nullptr) : _ptr(ptr) {}

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

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

    // 简化一下,禁止拷贝
    SmartPtr(const SmartPtr&) = delete;
    SmartPtr& operator=(const SmartPtr&) = delete;
private:
    T* _ptr;
};

struct Date {
    int _year = 0;
    int _month = 0;
    int _day = 0;
};

int main()
{
    SmartPtr<int> sp1(new int(0));
    *sp1 = 10;
    std::cout << *sp1 << std::endl;

    SmartPtr<Date> sparray(new Date);
    // 等价于 sparray.operator->()->_year = 2018;
    sparray->_year = 1949;
    sparray->_month = 10;
    sparray->_day = 1;

    std::cout << sparray->_year << "-" << sparray->_month << "-" << sparray->_day << std::endl;
    return 0;
}

智能指针我们可以简单总结为:

  • RAII 特性:构造接管资源,析构释放资源;
  • 重载 operator* 和 operator->:具有像指针一样的行为。

std::auto_ptr

std::auto_ptr介绍文档

C++98 提供了 auto_ptr。它的实现思想是管理权转移:

  • 拷贝构造时,把原对象的指针转移给新对象,原对象置空;
  • 赋值时,先释放当前对象资源,再转移对方资源,对方置空。

缺陷:

  1. 拷贝后原对象变成空指针,继续使用会空指针解引用;
  2. 语义反直觉:拷贝本应不改变源对象,但 auto_ptr 会改变;
  3. 不能很好用于 STL 容器,容器拷贝可能导致资源所有权被转移,源对象悬空/为空;
  4. C++11 弃用,C++17 移除。

auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份xhs::auto_ptr来了解它的原理

cpp 复制代码
namespace xhs{
    template<class T>
    class auto_ptr{
    public:
        auto_ptr(T* ptr = nullptr)
        :_ptr(ptr)
        {}

        auto_ptr(auto_ptr<T>& ap)
        :_ptr(ap._ptr)
        {
            ap._ptr = nullptr;
        }

        auto_ptr<T>& operator=(auto_ptr<T>& ap)
        {
            if(this != &ap)
            {
                delete _ptr;
                _ptr = ap._ptr;
                ap._ptr = nullptr;
            }

            return *this;
        }

        ~auto_ptr()
        {
            if(_ptr)
            {
                std::cout << "delete:" << _ptr << std::endl;
                
                delete _ptr;
            }
        }
        T& operator*() { return *_ptr; }
        T* operator->() { return _ptr; }
    private:
    T* _ptr;
    };
}

int main()
{
    xhs::auto_ptr<int> ap1(new int(10));
    xhs::auto_ptr<int> ap2(ap1);

    std::cout << (ap1.get() == nullptr) << std::endl; // 1,ap1 已被置空
    std::cout << *ap2 << std::endl;                   // 10

    xhs::auto_ptr<int> ap3(new int(20));
    ap3 = ap2; // 释放 ap3 原来的 20,转移 ap2 的资源

    std::cout << (ap2.get() == nullptr) << std::endl; // 1
    std::cout << *ap3 << std::endl;                   // 10

    return 0;
}

std::unique_ptr

std::unique_ptr介绍文档

C++11中开始提供更靠谱的unique_ptr

  • 同一时刻只能有一个 unique_ptr 管理某资源;
  • 禁止拷贝构造和拷贝赋值;
  • 支持移动构造和移动赋值,转移所有权后源对象为空;
  • 对应 boost 的 scoped_ptr 思路,但 unique_ptr 支持移动语义。

为什么禁止拷贝?如果两个 unique_ptr 指向同一资源,析构时会 delete 两次,导致 double free。独占所有权从类型系统上避免这个问题。

unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份xhs::unique_ptr来了解它的原理

cpp 复制代码
namespace xhs
{
    template <class T>
    class unique_ptr
    {
    public:
        unique_ptr(T *ptr = nullptr)
            : _ptr(ptr)
        {
        }

        ~unique_ptr()
        {
            delete _ptr;
        }

        unique_ptr(const unique_ptr<T> &) = delete;
        unique_ptr<T> &operator=(const unique_ptr<T> &) = delete;

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

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

    private:
        T *_ptr;
    };
}

int main() {
    xhs::unique_ptr<int> up1(new int(10));

    // xhs::unique_ptr<int> up2(up1); // 编译错误:禁止拷贝

    std::unique_ptr<int> up3(new int(30));
    // std::unique_ptr<int> up4(up3); // 编译错误:禁止拷贝             

    return 0;
}

std::shared_ptr

std::shared_ptr介绍文档

shared_ptr 支持多个智能指针共享同一资源,原理是引用计数:

  1. 每个资源维护一份计数,记录被几个 shared_ptr 共享;
  2. 拷贝 shared_ptr 时计数加一;
  3. 析构时计数减一;
  4. 计数为 0 时释放资源;
  5. 计数不为 0 时不能释放,否则其他对象变野指针。

shared_ptr就像最后离开教室的人负责锁门。当最后一个 shared_ptr 析构时才释放资源。

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。下面我们简单实现一下xhs::shared_ptr。

cpp 复制代码
namespace xhs
{

    template <class T>
    class shared_ptr
    {
        struct ControlBlock
        {
            std::atomic<int> count;
            ControlBlock() : count(1) {}
        };

        mutable std::mutex _mtx; // 保护 _ptr / _ctrl 的读写
        T *_ptr;
        ControlBlock *_ctrl;

        // 调用者必须持有 _mtx(当前对象的锁)
        void add_ref_locked()
        {
            if (_ctrl)
                _ctrl->count.fetch_add(1, std::memory_order_relaxed);
        }

        // 不持锁的静态释放函数,便于在锁外调用
        static void release(T *ptr, ControlBlock *ctrl)
        {
            if (ctrl && ctrl->count.fetch_sub(1, std::memory_order_acq_rel) == 1)
            {
                delete ptr;
                delete ctrl;
            }
        }

    public:
        explicit shared_ptr(T *ptr = nullptr)
            : _ptr(ptr), _ctrl(ptr ? new ControlBlock() : nullptr) {}

        shared_ptr(const shared_ptr<T> &sp)
        {
            // 锁住源对象,读取它当前的 _ptr / _ctrl
            std::lock_guard<std::mutex> lk(sp._mtx);
            _ptr = sp._ptr;
            _ctrl = sp._ctrl;
            add_ref_locked();
        }

        shared_ptr<T> &operator=(const shared_ptr<T> &sp)
        {
            if (this == &sp)
                return *this;

            T *old_ptr = nullptr;
            ControlBlock *old_ctrl = nullptr;
            {
                // 同时锁住两边,scoped_lock 内部用 std::lock 避免死锁
                std::scoped_lock lk(_mtx, sp._mtx);

                // 摘下旧资源
                old_ptr = _ptr;
                old_ctrl = _ctrl;
                _ptr = nullptr;
                _ctrl = nullptr;

                // 挂上新资源
                _ptr = sp._ptr;
                _ctrl = sp._ctrl;
                add_ref_locked();
            }
            // 锁外释放旧资源,避免持锁期间 delete 引发死锁
            release(old_ptr, old_ctrl);
            return *this;
        }

        ~shared_ptr()
        {
            T *ptr = nullptr;
            ControlBlock *ctrl = nullptr;
            {
                std::lock_guard<std::mutex> lk(_mtx);
                ptr = _ptr;
                ctrl = _ctrl;
                _ptr = nullptr;
                _ctrl = nullptr;
            }
            // 锁外释放
            release(ptr, ctrl);
        }

        T &operator*() const
        {
            std::lock_guard<std::mutex> lk(_mtx);
            return *_ptr;
        }

        T *operator->() const
        {
            std::lock_guard<std::mutex> lk(_mtx);
            return _ptr;
        }

        T *get() const
        {
            std::lock_guard<std::mutex> lk(_mtx);
            return _ptr;
        }

        int use_count() const
        {
            std::lock_guard<std::mutex> lk(_mtx);
            return _ctrl ? _ctrl->count.load(std::memory_order_relaxed) : 0;
        }
    };

} 

// shared_ptr智能指针是线程安全的吗?
// 是的,引用计数的加减是加锁保护的。但是指向资源不是线程安全的

// 指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了
// 引用计数的线程安全问题,是智能指针要处理的
int main()
{
    xhs::shared_ptr<int> sp1(new int(1));
    xhs::shared_ptr<int> sp2(sp1);
    xhs::shared_ptr<int> sp3(sp1);

    std::cout << sp1.use_count() << std::endl; // 3

    xhs::shared_ptr<int> sp4(new int(4));
    sp1 = sp4;

    std::cout << sp1.use_count() << " "
         << sp2.use_count() << " "
         << sp3.use_count() << std::endl; // 2 2 2

    *sp2 = 3;
    std::cout << *sp2 << std::endl; // 3

    return 0;
}

std::shared_ptr的线程安全问题

通过下面的程序我们来测试shared_ptr的线程安全问题。需要注意的是shared_ptr的线程安全分为两方面:

  1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以智能指针中引用计数++、--是需要加锁的,也就是说引用计数的操作是线程安全的。
  2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
cpp 复制代码
struct Date
{
    int year = 0;
    int month = 0;
    int day = 0;
};

void SharedPtrFunc(xhs::shared_ptr<Date> &sp, std::size_t n, std::mutex &mtx)
{
    std::cout << sp.get() << std::endl; // get() 内部已加锁

    for (std::size_t i = 0; i < n; ++i)
    {
        xhs::shared_ptr<Date> copy(sp); // 拷贝构造内部锁住 sp
			
        {
            std::lock_guard<std::mutex> lk(mtx);
            sp->year++;
            sp->month++;
            sp->day++;
        }
    }
}

int main()
{
    xhs::shared_ptr<Date> p(new Date);
    const std::size_t n = 100000;
    std::mutex mtx;

    std::thread t1(SharedPtrFunc, std::ref(p), n, std::ref(mtx));
    std::thread t2(SharedPtrFunc, std::ref(p), n, std::ref(mtx));

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

    std::cout << std::dec; // 打印前强制十进制
    std::cout << p->year << " " << p->month << " " << p->day << std::endl;
    std::cout << p.use_count() << std::endl;

    return 0;
}

这里锁的写法很精妙,因为lock_guard它是自动解锁的我们这里避免忘记解锁就给它单独搞一个域这样lock_guard运行完出域的时候就自动解锁了不需要我们手动来解锁

std::shared_ptr的循环引用

cpp 复制代码
struct ListNode
{
    int _data;

    std::shared_ptr<ListNode> _prev;
    std::shared_ptr<ListNode> _next;

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

int main()
{
    std::shared_ptr<ListNode> node1(new ListNode);
    std::shared_ptr<ListNode> node2(new ListNode);
    
    std::cout << node1.use_count() << std::endl;
    std::cout << node2.use_count() << std::endl;

    node1->_next = node2;
    node2->_prev = node1;

    std::cout << node1.use_count() << std::endl;
    std::cout << node2.use_count() << std::endl;

    return 0;
}

我们分析一下上述代码:

  1. node1、node2 分别指向两个节点,引用计数为 1;
  2. node1->_next = node2,node2->_prev = node1,引用计数变成 2;
  3. 作用域结束,node1、node2 析构,引用计数减到 1;
  4. 但 node1 的 _next 还指向 node2,node2 的 _prev 还指向 node1;
  5. node2 要释放,需要 node1->_next 析构;node1 要释放,需要 node2->_prev 析构;
  6. 谁也不会释放,形成循环引用,资源泄漏。

那么如何解决上面这个问题呢?

只需要我们将shared_ptr换成weak_ptr就可以解决上述问题了,weak_ptr就是C++用来解决shared_ptr的循环引用问题的。

cpp 复制代码
struct ListNode
{
    int _data;

    std::weak_ptr<ListNode> _prev;
    std::weak_ptr<ListNode> _next;

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

int main()
{
    std::shared_ptr<ListNode> node1(new ListNode);
    std::shared_ptr<ListNode> node2(new ListNode);
    
    std::cout << node1.use_count() << std::endl;
    std::cout << node2.use_count() << std::endl;

    node1->_next = node2;
    node2->_prev = node1;

    std::cout << node1.use_count() << std::endl;
    std::cout << node2.use_count() << std::endl;

    return 0;
}

为什么weak_ptr能解决循环引用计数问题?

  • 因为weak_ptr是弱引用,不增加强引用计数,不参与所有权。

那么shared_ptr会造成资源泄漏,为什么不把所有地方都换成weak_ptr呢?

  • 因为weak_ptr 不拥有资源,不能保证对象一直存活,只适合打破循环或观察场景。实际上weak_ptr底层上是通过调用shread_ptr来访问对象的。
  • 那么为什么weak_ptr能解决shared_ptr上的问题呢?明明都是用的同一套代码,因为weak_ptr它不动shared_ptr的引用计数而是底层自己管理这自己的那一套,它的那个计数只是说明weak_ptr能够访问的对象,并不是持有对象。

这里的weak_ptr实现起来比较复杂,所以这里就不提供模拟实现的代码,感兴趣的可以去问ai,让ai来生成对应的代码。

如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题(只做了解)

cpp 复制代码
template<class T>
struct FreeFunc {
    void operator()(T* ptr)
    {
        std::cout << "free:" << ptr << std::endl;
        free(ptr);
    }
};

template<class T>
struct DeleteArrayFunc {
    void operator()(T* ptr)
    {
        std::cout << "delete[]" << ptr << std::endl;
        delete[] ptr;
    }
};

// 原图中 sp4 使用了 A,但未定义,这里补上一个空结构体保证编译通过
struct A {};

int main()
{
    FreeFunc<int> freeFunc;
    std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);

    DeleteArrayFunc<int> deleteArrayFunc;
    std::shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);

    std::shared_ptr<A> sp4(new A[10], [](A* p){delete[] p; });

    std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p){fclose(p); });

    return 0;
}

完

相关推荐
数据狐(Datafox)36 分钟前
一文读懂京东商品详情API:返回字段解读与 JSON 样例
java·开发语言·数据库·windows·json
进化中的刀盾兽1 小时前
从零实现 C++ AI 大模型接入 SDK(十):SessionManager 会话管理与多轮对话实现
c++
小成202303202651 小时前
C++~02运算符重载
开发语言·c++
emfuture1 小时前
龙芯2K300 PLC 在拉丝机收线系统改造中的应用:硬件选型、张力控制与国产化替代实践
java·开发语言·智能制造·plc·国产化替代
geovindu1 小时前
rust: Mediator Pattern
开发语言·后端·设计模式·rust·中介者模式·行为模式
传奇开心果编程1 小时前
【Rust入门练中学】第8课:错误处理
开发语言·rust·错误处理·编程语言理论·通用编程语言
郑同学的笔记2 小时前
【c++随笔30】C++ 注册回调函数
开发语言·c++
七夜zippoe2 小时前
启元 Q1 腿部运动学逆解:Python 算站立下蹲关节角
开发语言·python·ai·机器人·腿部运动学
熊猫钓鱼>_>2 小时前
精准碰一碰近场分享与拖拽投送实战:把文件“碰“进另一台设备【鸿蒙心迹】
开发语言·华为·harmonyos·鸿蒙·openharmony