⭐上篇文章:44 C++智能指针详解:安全高效管理内存-CSDN博客

⭐代码仓库:橘子真甜 (yzc-YZC) - Gitee.com

目录

一. 使用类封装一个智能指针

二. 智能指针解决多次析构的方式

2.1 早期版本 auto_ptr

2.2 unique_ptr

2.3 shared_ptr⭐

2.4 weak_ptr

三. 内存泄漏与RAII

3.1 内存泄漏

3.2 RAII思想


一. 使用类封装一个智能指针

        智能指针是基于RAII思想构建的指针,需要在构造时候创建资源,析构的时候释放资源。这样才能保证不会有内存的泄漏。

        还需要使用模板保证能够存储不同类型的变量

        基本代码结构如下:在构造函数中初始化资源,析构函数中自动销毁资源。通过类的管理实现资源的开辟和销毁

#pragma once

template <class T>
class mySmartPtr
{
public:
    mySmartPtr(const T *ptr)
        : _ptr(ptr) {}

    ~mySmartPtr()
    {
        if (_ptr != nullptr)
            delete _ptr;
        _ptr = nullptr
    }

private:
    T *_ptr;
};

但是我们还不能使用,因为没有重载operator* 和 operator->

改进后的代码如下:

#pragma once

template <class T>
class mySmartPtr
{
public:
    mySmartPtr(const T *ptr)
        : _ptr(ptr) {}

    ~mySmartPtr()
    {
        if (_ptr != nullptr)
            delete _ptr;
        _ptr = nullptr;
    }

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

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

private:
    T *_ptr;
};

重新测试主程序,运行结果如下。

        此时仍然有问题,假如有两个智能指针管理同一个对象。这样同一块资源就会被析构两次,这样就会非法访问内存。

二. 智能指针解决多次析构的方式

2.1 早期版本 auto_ptr

        上篇文章提到:auto_ptr是通过转移管理权来防止多次析构的,不过现在已经被弃用了。因为有更好的方式,auto_ptr转移管理权会容易导致原指针变为野指针

        不过auto_ptr实现的思路还是可以学习的!转移管理权首先要考虑转移的指针是否为空指针

拷贝构造:直接让转移的指针指向我管理的空间即可,然后让自己置空。

调用 p1 = p2 即赋值的时候:则需要释放转移指针的资源并置空,然后执行上面操作。

是不是有点抽象?可以看下面的图画

代码如下:

template <class T>
class myAutoPtr
{
public:
    myAutoPtr(T *ptr)
        : _ptr(ptr) {}

    ~mySharedPtr()
    {
        //直接调用release释放自己即可
       release();
    }


    // 拷贝构造函数
    myAutoPtr(myAutoPtr<T> &aptr)
        : _ptr(aptr._ptr)
    {
        aptr._ptr = nullptr;
    }

    // operator重载
    myAutoPtr<T> &operator=(myAutoPtr<T> &aptr)
    {
        if (this != aptr)
        {
            if(aptr != nullptr)
            {
                delete _ptr;
                _ptr = aptr._ptr;
                aptr._ptr = nullptr;
            }
        }
        return *this;
    }

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

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

private:
    T *_ptr;
};

2.2 unique_ptr

        unique_ptr通过防止拷贝来保证只有一个智能指针对象管理一个对象资源,这样就不会造成多次析构的问题了!解决方法简单粗暴,但是好用。

代码如下:

template <class T>
class myUniquePtr
{
public:
    myUniquePtr(T *ptr)
        : _ptr(ptr) {}

    ~myUniquePtr()
    {
        if (_ptr != nullptr)
            delete _ptr;
        _ptr = nullptr;
    }
    // 禁用拷贝构造和赋值重载
    myUniquePtr(myUniquePtr<T> &mup) = delete;
    myUniquePtr<T> &operator=(myUniquePtr<T> &mup) = delete;

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

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

private:
    T *_ptr;
};

2.3 shared_ptr⭐

        共享指针通过引用计数这个变量来保证多次析构的问题,每有一个指针指向当前对象,引用计数就++。当引用计数为0,即没有指针指向这个对象的时候就销毁资源。

        不过这样也有了新的问题!如何保证引用计数的线程安全问题?

        C++11内部是通过原子变量的方式来保证的!原子变量作为引用计数,原子变量++ --操作都是线程安全的

        这里使用mutex互斥锁的方式来实现保护shared_ptr中的引用计数

        如果使用mutex保护引用计数,需要如何定义count呢?

        int count 不能这样,因为每一个指针都有自己的count。static int count也不行,因为这样会导致不同的类型有相同的count。int & 也不行,因为管理比较复杂。

        使用 int *在堆中创建资源是最好管理的!

代码如下:

部分函数:

    //增加引用计数
    void add_ref_count()
    {
        //注意加锁
        std::unique_lock<std::mutex> ul(_lock);
        (*_count)++;
    }
    // 释放自己的资源,引用计数--,为0释放所有资源,不为0,啥都不做
    void release()
    {
        bool flag = false;
        {
            std::unique_lock<std::mutex> ul(_lock);
            if (--(*_count) == 0)
            {
                // 说明引用计数为0,需要释放指向的对象
                if (_ptr != nullptr)
                    delete _ptr;
                _ptr = nullptr;
                // 同时需要释放引用计数
                delete _count;
                _count = nullptr;

                // 处于加锁状态,不可释放锁的资源
                // 通过标志位来释放锁资源
                flag = true;
            }
        }

        // 释放锁资源
        if (flag)
            delete _lock;
    }

类代码如下:

注意:拷贝和赋值的时候需要将所有成员变量都拷贝

release时候需要注意_ptr是否为空,如果为空就不需要释放数据

如果赋值的指针有着自己管理的资源,需要先释放自己资源然后再管理新资源

template <class T>
class mySharedPtr
{
public:
    mySharedPtr(T *ptr)
        : _ptr(ptr), _count(new int), _lock(new std::mutex) {}

    ~mySharedPtr()
    {
        release();
    }

    mySharedPtr(mySharedPtr<T> &mup)
        : _ptr(mup._ptr), _count(mup._count),_lock(mup._lock) // 初始化ptr和count保证二者一致
    {
        // 增加引用计数
        add_ref_count();
    }

    mySharedPtr<T> &operator=(mySharedPtr<T> &mup)
    {
        if (this != &mup)
        {
            // 如果自己有管理的对象的话,需要释放自己的资源
            release();

            // 更新所有的资源
            _ptr = mup._ptr;
            _count = mup._count;
            _lock = mup._lock;

            // 增加引用计数
            add_ref_count();
        }
        return *this;
    }

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

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

private:
    // 增加引用计数
    void add_ref_count()
    {
        // 注意加锁
        std::unique_lock<std::mutex> ul(*_lock);
        (*_count)++;
    }

    // 释放自己的资源,引用计数--,为0释放所有资源,不为0,啥都不做
    void release()
    {
        bool flag = false;
        {
            std::unique_lock<std::mutex> ul(*_lock);
            if (--(*_count) == 0 && _ptr != nullptr)
            {
                // 说明引用计数为0,需要释放指向的对象
                if (_ptr != nullptr)
                    delete _ptr;
                _ptr = nullptr;

                // 同时需要释放引用计数
                delete _count;
                _count = nullptr;

                // 处于加锁状态,不可释放锁的资源
                // 通过标志位来释放锁资源
                flag = true;
            }
        }

        // 释放锁资源
        if (flag)
            delete _lock;
    }

private:
    T *_ptr;
    int *_count;       // 共享计数
    std::mutex *_lock; // 互斥锁
};

测试代码与运行结果

#include <iostream>
#include "smartptr.hpp"

int main()
{
    int *a = new int(2);
    mySharedPtr<int> ptr1(a);
    mySharedPtr<int> ptr2(ptr1);
    mySharedPtr<int> ptr3(new int(8));
    ptr3 = ptr1;

    std::cout << "*ptr1:" << *ptr1 << std::endl;
    std::cout << "*ptr2:" << *ptr2 << std::endl;
    std::cout << "*ptr3:" << *ptr3 << std::endl;
    *ptr1 = 12345;
    std::cout << "*ptr1:" << *ptr1 << std::endl;
    std::cout << "*ptr2:" << *ptr2 << std::endl;
    std::cout << "*ptr3:" << *ptr3 << std::endl;
}

可以看到,这些指针管理同一个对象,并且没有

2.4 weak_ptr

        weak_ptr是用于解决shared_ptr循环引用的问题。在上篇文章中已经详细说明,这里就不过多介绍和实现了。

三. 内存泄漏与RAII

3.1 内存泄漏

        内存泄漏是由于某进程由于代码失误或者其他错误操作造成的。比如 动态开辟了资源,使用完毕了却不释放,一直占用内存却不释放。又比如,父子进程中,子进程退出后,父进程不去回收子进程的资源,这样也会造成内存泄漏等。

        这些占用内存又没有使用资源是没有用的,如果一个系统长时间运行,并且有导致内存泄漏的代码。最后整个系统会由于内存泄漏卡死。

        解决内存泄漏需要从三个方面来说:

1 规范写代码,申请资源需要释放

2 使用如智能指针这样的技术来保证无用的资源被释放

3 代码运行后可以使用一些工具如 valgrind

3.2 RAII思想

RAII思想是:资源获取即初始化,资源可以自动销毁。用于减少内存泄漏

比如智能指针就是 在构造函数中完成资源初始化,当使用完毕后通过析构函数自动销毁

又比如 lock_guard unique_guard等锁,初始化时候进行加锁,解锁之后进行销毁

Logo

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

更多推荐