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

问题分析

在传统C++编程中,手动管理内存容易出现问题:

void Func()
{
    // 1. 如果p1这里new抛异常会如何?
    // 2. 如果p2这里new抛异常会如何?
    // 3. 如果div调用这里又抛异常会如何?
    int* p1 = new int;
    int* p2 = new int;
    
    cout << div() << endl;  // 如果div()抛出异常
    
    delete p1;  // 这些delete可能不会执行
    delete p2;
}

对于以上代码会存在问题:

第一个new失败:如果new int抛出std::bad_alloc,程序直接终止,没有内存泄漏

第二个new失败:如果第一个new成功,第二个new失败,第一个int的内存泄漏

div()函数异常:如果两个new都成功,但div()抛出异常,两个int的内存都会泄漏

我们可以看到,C++异常安全机制不完善,异常抛出时直接寻找对应catch捕获异常,不会自动释放堆内存。

2. 内存泄漏

2.1 什么是内存泄露

  • 内存泄漏不是物理内存消失,而是应用程序分配某段内存后,因设计错误失去对该内存的控制,即申请内存没有释放,或者是系统的其他问题。内存泄漏不是在物理上消失,而是失去了对内存的控制。
  • 程序无法再使用该内存,也无法释放它

具体危害

  • 短期程序影响不大,但长期运行的程序(操作系统、服务器、后台服务)影响严重
  • 内存泄漏累积导致可用内存越来越少
  • 系统响应变慢,频繁交换,最终卡死或崩溃
  • 在嵌入式系统等资源受限环境中更为致命

内存泄漏示例

void MemoryLeaks()
{
    // 1. 明显的忘记释放
    int* p1 = (int*)malloc(sizeof(int));
    int* p2 = new int;
    FILE* p3 = fopen("log.txt","w");
    // 这里直接返回,p1和p2指向的内存泄漏,p3所指的FILE对象也不会释放,造成内存泄露
    
    // 2. 异常安全问题
    int* p4 = new int[10];
    Func(); // 假设Func()可能抛出异常
    
    delete[] p4; // 如果Func抛出异常,则在上面就会退出函数,这行不会执行
}

2.2 内存泄漏都有哪些类型

堆内存泄漏

我们自己通过malloc/calloc/realloc/new分配的内存,因为忘记释放或者程序执行跳转等原因没有释放(free/delete),会造成内存泄漏,这会导致这块内存无法再次被程序使用。

系统资源泄漏

在操作系统中的一些资源失去控制权,由于我们在调用系统调用的时候出现与堆泄露的类似问题,申请了文件描述符、套接字、管道、数据库连接、互斥锁等类型的数据,没有调用专门的释放函数(close、fclose等),导致系统资源耗尽,甚至影响其他程序。

2.3 避免内存泄漏

  1. 编码规范

    谁申请,谁释放,成对编程:每个new对应delete,每个malloc对应free,但仅靠规范无法解决异常安全问题,因为如果遇到异常,代码的执行流则会跳转,可能无法执行到我们是释放的代码行,因此无法完全解决。

  2. RAII思想

    资源生命周期与对象绑定,构造时获取资源,析构时自动释放,完美解决因为异常执行流跳转而无法释放内存的问题。当函数结束时,对象需要销毁,就可以顺带进行delet/free释放空间。

    智能指针就是具体实现。自动管理内存生命周期,对象生命周期销毁时会自动调用析构函数,即采用RALL思想专门设计的工具。

  3. 内存管理库

    使用自定义内存管理库,内置内存泄漏检测和调试功能

因此,内存泄露非常常见,一般会有2钟解决方案:1.提前预防,比如使用智能指针,2.泄露了查错,如使用内存泄露检测工具。

3. 智能指针的使用及原理

3.1 RAII思想

RAII(Resource Acquisition Is Initialization),直接翻译为 资源获取即初始化,真正意思为获取资源的时候初始化一个对象,这就是RALL思想。

利用RALL的思想,可以做到资源获取即初始化,利用对象的生命周期管理资源

在C++语言中,具体实现为构造函数中获取资源,析构函数中释放资源,资源在对象生命周期内保持有效。我的Linux线程池文章中采用的RALL风格的锁就是一个典型的应用实例。

RALL思想的优点就是:实现资源自动管理,无需手动释放。即使发生异常,在销毁变量的时候也会调用析构自动回收空间。资源的生命周期与对象生命周期是一致的,避免了内存泄露问题。

如果需要使用资源,只需在类中提供相关方法即可。

代码示例:

template<class T>
class SmartPtr {
public:
    SmartPtr(T* ptr = nullptr) : _ptr(ptr) 
    {
        // 构造时获取资源
    }
    
    ~SmartPtr()
    {
        if(_ptr) {
            delete[] _ptr;  // 析构时自动释放
            _ptr = nullptr;
        }
    }
    
private:
    T* _ptr;
};

void MemoryLeaks()
{
    // 1. 明显的忘记释放
    //int* p1 = (int*)malloc(sizeof(int));
    //int* p2 = new int;
    SmartPtr<int> sp1((int*)malloc(sizeof(int)));
    SmartPtr<int> sp2(new int);   
    // 这里将开辟的资源交给sp1和sp2对象管理,无需手动释放,对象出作用域自动销毁。
    
    // 2. 异常安全问题
    //int* p3 = new int[10];
    SmartPtr<int> sp3(new int[10]); 
    Func(); // 假设Func()可能抛出异常
    // 如果Func抛出异常,那也需要销毁对象,会自动释放内存。
}
int main()
{
    MemoryLeaks();
    return 0;
}

3.2 智能指针的原理

智能指针需要具备两个核心特性:

  • 管理资源的生命周期
  • 自动释放资源

也需要具备和普通指针一样的行为:

  • 通过重载运算符模拟指针操作
  • operator*:解引用
  • operator->:成员访问

3.3 std::auto_ptr(C++98)

C++98提供了智能指针,即为auto_ptr,他除了解决了自动管理生命周期的问题外,还解决了另外一个问题:智能指针的赋值问题。

问题引入

如果只是单纯的实现上面代码示例中SmartPtr,就会出现赋值问题。

普通指针的赋值,其意为两个指针指向一块空间共同管理。

而我们所写的简易智能指针,再赋值的时候会遇到深浅拷贝问题。首先不能深拷贝,因为我们写这个类的目的是帮助我们自动管理,管理其生命周期,深拷贝会开辟空间,不符合我们的要求。但是浅拷贝也会出现问题,两个指针指向一块空间,但是在析构的时候两个对象都会销毁,会导致重复释放的问题

在这里插入图片描述

而auto_ptr就是解决了这个问题。

核心思想为管理权转移

  • 拷贝构造或赋值时,将资源所有权转移给新对象
  • 原对象变为空指针

简易的模拟实现:

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

		auto_ptr(auto_ptr<T>& a) 
		{
			_ptr = a.relese();
		}
		T* get()
		{
			return _ptr;
		}

		T* relese()
		{
			T* ret = _ptr;
			_ptr = nullptr;
			return ret;
		}

		void reset(T* ptr = nullptr)
		{
			delete _ptr;
			_ptr = ptr;
		}

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

		T* operator->()
		{
			return _ptr;
		}
		auto_ptr<T>& operator= (auto_ptr<T>& p)
		{
			_ptr = p.relese();
			return *this;
		}
		~auto_ptr()
		{
			if(_ptr != nullptr)
			delete _ptr;
		}
	private:
		T* _ptr;
	};

缺陷分析:

std::auto_ptr<int> sp1(new int);
std::auto_ptr<int> sp2(sp1);  // 管理权转移

// sp1现在为空指针,访问会导致未定义行为
*sp2 = 10;
cout << *sp2 << endl;  // 正常
cout << *sp1 << endl;  // 运行时错误!sp1已是空指针

该设计具有明前缺陷,悬空指针问题,因此我们尽量不要使用该指针。

auto_ptr是失败的设计,已被C++11弃用,甚至有些公司明确禁止使用。

3.4 std::unique_ptr

C++11新添的智能指针,唯一型智能指针,不可拷贝,赋值

核心思想:防拷贝

  • 独占式所有权,一个资源只能被一个unique_ptr拥有
  • 通过删除拷贝构造函数和赋值运算符实现防拷贝
// 默认删除器模板 - 用于普通指针
template<class T>
struct default_delete {
    void operator()(T* ptr)
    {
        delete ptr;  // 使用 delete 释放单个对象
    }
};

// 默认删除器的特化版本 - 用于数组指针
template<class T>
struct default_delete<T[]> {
    void operator()(T* ptr)
    {
        delete[] ptr;  // 使用 delete[] 释放数组
    }
};

// 前向声明 unique_ptr 类模板
template <class T, class D = default_delete<T>>
class unique_ptr;

// unique_ptr 主模板 - 管理单个对象的独占所有权
template <class T, class D >
class unique_ptr {
public:
    // 构造函数:接受原始指针和删除器(默认构造)
    unique_ptr(T* ptr = nullptr, D del = D()) :_ptr(ptr),_del(del)
    {}

    // 移动构造函数:从右值引用转移所有权
    unique_ptr(unique_ptr&& a):_ptr(a._ptr),_del(move(a._del))
    {
        a._ptr = nullptr;  // 将源对象的指针置空
    }
    
    // 移动赋值运算符:转移所有权
    unique_ptr& operator= (unique_ptr&& a)
    {
        if (&a != this)  // 修正:应该比较地址,原代码 a != *this 不正确
        {
            reset(a.release());  // 释放当前资源并接管新资源
            _del = move(a._del); // 转移删除器
        }
        return *this;  
    }

    // 禁用拷贝构造函数
    unique_ptr(unique_ptr& a) = delete;
    
    // 获取原始指针(不释放所有权)
    T* get()
    {
        return _ptr;
    }

    // 交换两个 unique_ptr 的内容
    void swap(unique_ptr& a)
    {
        std::swap(_ptr, a._ptr);  
        std::swap(_del, a._del);  
    }
    
    // 布尔转换运算符:检查是否持有有效指针
    explicit operator bool()const  
    {
        return _ptr != nullptr;
    }

    // 释放所有权,返回原始指针(调用者负责管理)
    T* release()
    {
        T* ret = _ptr;
        _ptr = nullptr;
        return ret;
    }

    // 重置指针:先删除当前对象,再接管新指针
    void reset(T* ptr = nullptr)
    {
        _del(_ptr);  // 使用删除器释放当前资源
        _ptr = ptr;  // 接管新指针
    }

    // 解引用运算符:访问指向的对象
    T& operator*() const
    {
        return *_ptr;
    }

    // 箭头运算符:通过指针访问成员
    T* operator->() const 
    {
        return _ptr;
    }
    
    // 下标运算符:用于数组访问
    T& operator[](int pos)const
    {
        return _ptr[pos];
    }
    
    // 禁用拷贝赋值运算符
    unique_ptr& operator= (unique_ptr& p) = delete;
    
    // 析构函数:自动释放管理的资源
    ~unique_ptr()
    {
        if (_ptr != nullptr)
            _del(_ptr);  // 使用删除器释放资源
    }
    
private:
    T* _ptr;  // 管理的原始指针
    D _del;   // 删除器对象
};

// unique_ptr 的特化版本 - 用于管理动态数组
template <class T, class D>
class unique_ptr<T[], D> {
public:
    // 构造函数:接受数组指针和删除器
    unique_ptr(T* ptr, D del = D() ) :_ptr(ptr), _del(del)
    {
    }
    
    // 移动构造函数:转移数组所有权
    unique_ptr(unique_ptr&& a) :_ptr(a._ptr), _del(move(a._del))
    {
        a._ptr = nullptr;  // 将源对象指针置空
    }
    
    // 禁用拷贝构造函数
    unique_ptr(unique_ptr& a) = delete;

    // 获取原始指针(不释放所有权)
    T* get() const  
    {
        return _ptr;
    }

    // 布尔转换运算符:检查是否持有有效指针
    explicit operator bool()const
    {
        return _ptr != nullptr;
    }

    // 移动赋值运算符:转移数组所有权
    unique_ptr& operator= (unique_ptr&& a) {
        if (&a != this)  // 修正:应该比较地址
        {
            reset(a.release());  // 释放当前数组并接管新数组
            _del = move(a._del); // 转移删除器
        }
        return *this;  // 注意:原代码缺少返回值
    }

    // 交换两个 unique_ptr 的内容
    void swap(unique_ptr& a)
    {
        std::swap(_ptr, a._ptr);  // 修正:应该使用 std::swap
        std::swap(_del, a._del);  // 修正:应该使用 std::swap
    }

    // 释放数组所有权,返回原始指针
    T* release()
    {
        T* ret = _ptr;
        _ptr = nullptr;
        return ret;
    }

    // 重置数组指针:先删除当前数组,再接管新数组
    void reset(T* ptr = nullptr)
    {
        if(_ptr != nullptr)
            _del(_ptr);  // 使用删除器释放当前数组
        _ptr = ptr;      // 接管新数组指针
    }

    // 下标运算符:访问数组元素
    T& operator[](int pos)const
    {
        return _ptr[pos];
    }
    
    // 禁用拷贝赋值运算符
    unique_ptr& operator= (unique_ptr& p) = delete;

    // 析构函数:自动释放管理的数组
    ~unique_ptr()
    {
        if(_ptr != nullptr)
            _del(_ptr);  // 使用删除器释放数组
    }
    
private:
    T* _ptr;  // 管理的数组指针
    D _del;   // 删除器对象
};

使用特点

  • 轻量级,几乎无额外开销
  • 编译期防拷贝,安全可靠
  • 支持移动语义(C++11)

3.5 std::shared_ptr

C++11新添智能指针,支持赋值,拷贝

设计思想:引用计数

  • 多个shared_ptr可以共享同一个资源
  • 通过引用计数跟踪资源被多少个shared_ptr共享
  • 当最后一个shared_ptr销毁时释放资源
template <class T>
class shared_ptr {
public:
    // 默认构造函数:接受原始指针,初始化引用计数为1,使用默认删除器
    shared_ptr(T* ptr = nullptr) 
        :_ptr(ptr), 
         _size(new int{ 1 }), 
         _del([](T* ptr) {delete ptr; }), 
         _mutex(new std::mutex)
    {
    }

    // 带自定义删除器的构造函数
    template< class D >
    shared_ptr(T* ptr, D del) 
        :_ptr(ptr),
         _del(del), 
         _size(new int{1}),
         _mutex(new std::mutex)  
    {}
    
    // 增加引用计数(线程安全)
    void addref()
    {
        std::lock_guard<std::mutex> lock(*_mutex);  
        (*_size)++;
    }

    // 减少引用计数,如果为0则释放资源(线程安全)
    void release()
    {
      std::lock_guard<std::mutex> lock(*_mutex);
		if ( _ptr && --(*_size) == 0 )
		{
			_del(_ptr);
			delete _size;
		}
    }
    
    // 拷贝构造函数:共享所有权,增加引用计数
    shared_ptr(shared_ptr& a)
        :_ptr(a._ptr),
         _size(a._size),
         _del(a._del),
         _mutex(a._mutex)
    {
        addref();
    }
    
    // 移动构造函数:转移所有权,不增加引用计数
    shared_ptr(shared_ptr&& a) noexcept
    {
        swap(a);
    }
    
    // 布尔转换运算符:检查是否持有有效指针
    explicit operator bool()const
    {
        return _ptr != nullptr;
    }

    // 获取原始指针(不释放所有权)
    T* get()
    {
        return _ptr;
    }

    // 重置指针:释放当前对象,接管新指针
    void reset(T* ptr = nullptr)
    {
        *this = shared_ptr<T>(ptr);  // 创建临时对象并移动赋值
    }

    // 解引用运算符:访问指向的对象
    T& operator*()
    {
        return *_ptr;
    }

    // 箭头运算符:通过指针访问成员
    T* operator->()
    {
        return _ptr;
    }

    // 交换两个 shared_ptr 的内容
    void swap(shared_ptr& p)
    {
        std::swap(_ptr, p._ptr);
        std::swap(_size, p._size);
        std::swap(_del, p._del);
        std::swap(_mutex, p._mutex);
    }

    // 检查是否是唯一所有者
    bool unique() const
    {
        std::lock_guard<std::mutex> lock(*_mutex);
        return *_size == 1;
    }
    
    // 下标运算符:用于数组访问
    T& operator[](int pos)
    {
        return _ptr[pos];
    }
    
    // 获取当前引用计数
    int use_count() const
    {
        std::lock_guard<std::mutex> lock(*_mutex);
        return *_size;  
    }

    // 拷贝赋值运算符:共享所有权
    shared_ptr& operator= (shared_ptr& p)
    {
        if (this != &p)  // 防止自赋值
        {
            release();  // 释放当前资源
            _ptr = p._ptr;
            _size = p._size;
            _del = p._del;
            _mutex = p._mutex;
            addref();  // 增加新资源的引用计数
        }
        return *this;
    }

    // 移动赋值运算符:转移所有权
    shared_ptr& operator= (shared_ptr&& p)
    {
        if (this != &p)  // 防止自赋值
        {
            swap(p);  // 交换资源,原资源会在p析构时释放
        }
        return *this;
    }
    
    // 析构函数:减少引用计数,必要时释放资源
    ~shared_ptr()
    {
        release();
    }
    
private:
    T* _ptr;                       // 管理的原始指针
    int* _size;                    // 引用计数指针
    std::function<void(T*)> _del;  // 删除器函数对象
    std::mutex* _mutex;            // 互斥锁指针,用于线程安全
};

引用计数原理

初始状态:shared_ptr sp1(new int)
引用计数:1

拷贝构造:shared_ptr sp2(sp1)
引用计数:2

sp1析构:引用计数减为1
sp2析构:引用计数减为0,释放资源

3.6 线程安全问题详解

两个方面

  1. 引用计数操作:线程安全(通过加锁保护)
  2. 指向的资源访问:非线程安全(需要用户自己同步)
struct Date {
    int _year = 0;
    int _month = 0;
    int _day = 0;
};

void SharePtrFunc(bit::shared_ptr<Date>& sp, size_t n, mutex& mtx) {
    for (size_t i = 0; i < n; ++i) {
        // 智能指针拷贝:引用计数++(线程安全)
        bit::shared_ptr<Date> copy(sp);
        
        // 访问资源:需要用户自己加锁(非线程安全)
        {
            unique_lock<mutex> lk(mtx);
            copy->_year++;
            copy->_month++;
            copy->_day++;
        }
    }
}

3.7 循环引用问题及解决方案

类似于死锁,互相拿着对方的释放条件

问题描述

struct ListNode {
    int _data;
    shared_ptr<ListNode> _prev;
    shared_ptr<ListNode> _next;
    
    ~ListNode() { cout << "~ListNode()" << endl; }
};

int main() {
    shared_ptr<ListNode> node1(new ListNode);
    shared_ptr<ListNode> node2(new ListNode);
    
    node1->_next = node2;  // node2引用计数+1
    node2->_prev = node1;  // node1引用计数+1
    
    // 退出时:
    // node1析构:引用计数从2减为1(因为node2->_prev还指向它)
    // node2析构:引用计数从2减为1(因为node1->_next还指向它)
    // 两者都无法释放,内存泄漏!
}

正常当没有循环引用时,即只有单向的指向:

图片字有点小,可以点击放大观看

在这里插入图片描述

在两个指针生命周期结束,资源释放过程如下:

在这里插入图片描述

在这里插入图片描述

当node1->_next = node2,并且node2-> _ next = node1,此时就会有循环引用

循环引用示意图:

在这里插入图片描述

在这里插入图片描述

​ 循环引用会导致他们各自的引用计数都是2,在释放的时候都减1不为0不会释放资源,造成内存泄露,此时只有一方的引用计数将为0,才可释放,但是互相引用,就类似于死锁,互相拿着对方的条件,无法推进。

解决方案:std::weak_ptr

不支持RALL,专门辅助shared_ptr来破解循环引用

本质是赋值或者拷贝时,只指向资源,不增加shared_ptr的引用计数

struct ListNode {
    int _data;
    weak_ptr<ListNode> _prev;    // 使用weak_ptr打破循环
    weak_ptr<ListNode> _next;
    
    ~ListNode() { cout << "~ListNode()" << endl; }
};

weak_ptr特点

  • 不增加引用计数
  • 不控制对象生命周期
  • 可以通过lock()获取shared_ptr临时访问资源

3.8 自定义删除器

  • 非new分配的内存(malloc、fopen等)
  • 数组对象(需要delete[])
  • 需要特殊清理逻辑的资源
// 仿函数删除器
template<class T>
struct FreeFunc {
    void operator()(T* ptr) {
        cout << "free:" << ptr << endl;
        free(ptr);
    }
};

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

int main() {
    // 使用自定义删除器
    FreeFunc<int> freeFunc;
    std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);
    
    DeleteArrayFunc<int> deleteArrayFunc;
    std::shared_ptr<int> sp2(new int[10], deleteArrayFunc);
    
    // lambda表达式删除器
    std::shared_ptr<FILE> sp3(fopen("test.txt", "w"), 
                              [](FILE* p) { fclose(p); });
    
    std::shared_ptr<int> sp4(new int[10], [](int* p) { delete[] p; });
}
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐