C++ 智能指针的使用和原理

在C++编程中,内存管理一直是一个棘手的问题。传统的手动内存管理(使用newdelete)容易导致内存泄漏、悬垂指针等问题。为了解决这些问题,C++引入了智能指针的概念。

智能指针的使用场景

传统内存管理的问题

考虑以下代码示例:

void Func() {
    int* array1 = new int[10];
    int* array2 = new int[10];
    try {
        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;
    }
    catch (...) {
        delete[] array1;
        delete[] array2;
        throw;
    }
    delete[] array1;
    delete[] array2;
}

这段代码存在几个问题:

  1. 如果Divide函数抛出异常,必须记得在catch块中释放内存
  2. 如果array2new操作抛出异常,array1的内存会泄漏
  3. 代码冗长且容易出错

智能指针解决方案

使用智能指针可以大大简化代码:

void Func() {
    SmartPtr<int> sp1 = new int[10];
    SmartPtr<int> sp2 = new int[10];
    int len, time;
    cin >> len >> time;
    cout << Divide(len, time) << endl;
}

智能指针会自动管理内存,无论是否发生异常,都能确保内存被正确释放。

RAII与智能指针设计思路

RAII原则

RAII(Resource Acquisition Is Initialization)是一种重要的资源管理思想:

  • 在对象构造时获取资源
  • 在对象生命周期内保持资源有效
  • 在对象析构时释放资源

智能指针的基本实现

一个简单的智能指针实现如下:

template<class T>
class SmartPtr {
public:
    SmartPtr(T* ptr) : _ptr(ptr) {}
    
    ~SmartPtr() {
        delete[] _ptr;
    }
    
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }
    T& operator[](size_t i) { return _ptr[i]; }

private:
    T* _ptr;
};

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

C++标准库提供了几种智能指针,各有特点:

auto_ptr(已废弃)

  • C++98引入
  • 拷贝时把被拷贝对象的资源的管理权转移给拷贝对象
  • 这是一个非常糟糕的设计,因为他会到被拷贝对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使用auto_ptr。

unique_ptr

  • 独占所有权
  • 不支持拷贝,只支持移动
  • 轻量高效,推荐在不需共享时使用
unique_ptr<Date> up1(new Date);
// unique_ptr<Date> up2(up1); // 错误,不支持拷贝
unique_ptr<Date> up3(move(up1)); // ⽀持移动,但是移动后up1也悬空,所以使⽤移动要谨慎

shared_ptr

  • 共享所有权
  • 使用引用计数管理资源
  • 支持拷贝和移动
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1); // 引用计数增加
cout << sp1.use_count(); // 输出2

weak_ptr

  • 不增加引用计数
  • 用于解决shared_ptr的循环引用问题
  • 不能直接访问资源,需通过lock()获取shared_ptr

weak_ptr不具备RAII特性,也无法直接访问资源。根据文档,weak_ptr在构造时只能绑定到shared_ptr,而不能直接绑定资源。这种绑定不会增加shared_ptr的引用计数,从而有效解决了循环引用问题。
weak_ptr没有重载operator*和operator->运算符,因为它不参与资源管理。如果绑定的shared_ptr已释放资源,weak_ptr访问资源将存在风险。weak_ptr提供了expired()方法来检查资源是否失效,use_count()可获取shared_ptr的引用计数。当需要访问资源时,可以调用lock()方法返回一个管理资源的shared_ptr:若资源已释放则返回空对象,否则可安全访问资源。

#include <iostream>
#include <memory>
#include <string>
using namespace std;

int main() {
    // 创建原始shared_ptr并初始化资源
    shared_ptr<string> sp1(new string("原始资源"));
    // 创建第二个shared_ptr共享同一资源
    shared_ptr<string> sp2(sp1);
    
    // 创建weak_ptr观察sp1管理的资源
    weak_ptr<string> wp = sp1;
    
    // 检查weak_ptr是否失效(应返回false)
    cout << "资源是否失效: " << wp.expired() << endl;
    // 输出当前引用计数(应显示2)
    cout << "当前引用计数: " << wp.use_count() << endl;
    
    // 重置sp1指向新资源
    sp1 = make_shared<string>("新资源1");
    cout << "sp1重置后是否失效: " << wp.expired() << endl;
    cout << "sp1重置后引用计数: " << wp.use_count() << endl;
    
    // 重置sp2指向新资源
    sp2 = make_shared<string>("新资源2");
    cout << "sp2重置后是否失效: " << wp.expired() << endl;
    cout << "sp2重置后引用计数: " << wp.use_count() << endl;
    
    // 重新绑定weak_ptr到sp1
    wp = sp1;
    // 通过lock获取可用的shared_ptr
    auto sp3 = wp.lock();
    
    // 验证新绑定状态
    cout << "重新绑定后是否失效: " << wp.expired() << endl;
    cout << "重新绑定后引用计数: " << wp.use_count() << endl;
    
    // 通过lock返回的shared_ptr修改资源
    if(sp3) {
        *sp3 += "###修改后缀";
        cout << "修改后资源内容: " << *sp1 << endl;
    }
    
    return 0;
}

其他要点

  • 智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。智能指针支持在构造时给一个删除器,所谓删除器本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。因为new[]经常使用,所以为了简洁一点,unique_ptr和shared_ptr都特化了一份[]的版本,使用时 unique_ptr<Date[]> up1(new Date[5]);shared_ptr<Date[]> sp1(new Date[5]);就可以管理new[]的资源。
  • shared_ptr除了支持用指向资源的指针构造,还支持make_shared用初始化资源对象的值直接构造,对比用new构造make_shared可以让引用计数和指针的地址联系更紧密,减少碎片地址。
  • shared_ptrunique_ptr都支持了operator bool的类型转换,如果智能指针对象是一个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。
  • shared_ptrunique_ptr都得构造函数都使用explicit修饰,防止普通指针隐式类型转换成智能指针对象。

智能指针的原理

unique_ptr

原理简单,直接看代码

template<class T>
class unique_ptr
{
public:
	explicit unique_ptr(T* ptr)//explicit防止隐式类型转换转换
		:_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;
};

shared_ptr

在这里插入图片描述

shared_ptr<Date> sp1(new Date);
 // ⽀持拷⻉ 
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);

重点关注shared_ptr的设计原理,特别是引用计数机制的设计。由于每个资源都需要独立的引用计数,使用静态成员方式无法实现这一需求,必须采用堆上动态分配的方式。每当构造一个智能指针对象时,不仅要管理资源,还要new一个对应的引用计数。当多个shared_ptr指向同一资源时,引用计数会递增;当shared_ptr对象析构时,引用计数递减。当引用计数减至0,说明当前析构的shared_ptr是最后一个管理该资源的对象,此时才会真正释放资源。
在面试笔试中,模拟实现shared_ptr经常出现,在没有明确要求的情况下,建议实现以下代码的内容,如果还要求实现删除器功能,则加上其中注释的代码即可。

template<class T>
class shared_ptr {
public:
    // 构造函数,初始化智能指针并设置引用计数为1
    // 参数ptr为指向动态分配对象的指针,默认nullptr
    explicit shared_ptr(T* ptr = nullptr)
        : _ptr(ptr)
        , _pcount(new int(1))  // 动态分配引用计数,初始化为1
    {}

    // 拷贝构造函数,增加引用计数
    // 参数sp为另一个shared_ptr对象
    shared_ptr(const shared_ptr<T>& sp)
        :_ptr(sp._ptr)
        , _pcount(sp._pcount)
        , _del(sp._del)
    {
        ++(*_pcount);  // 引用计数加1
    }

    // 释放资源函数,减少引用计数并在必要时销毁对象
    void release()
    {
        if (--(*_pcount) == 0)  // 引用计数减1
        {
            // 当引用计数为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;            // 指向引用计数器的指针
    // function<void(T*)> _del = [](T* ptr) {delete ptr; };  // 删除器,默认使用delete
};

shared_ptr循环引用问题

如下图所示场景,n1和n2析构后,两个节点的引用计数都降为1

  • 右侧节点何时释放?它由左侧节点的_next指针管理,当_next析构时,右侧节点即被释放
  • _next何时析构?作为左侧节点的成员变量,当左侧节点释放时,_next随之析构
  • 左侧节点何时释放?它由右侧节点的_prev指针管理,_prev析构后,左侧节点即被释放
  • _prev何时析构?作为右侧节点的成员变量,当右侧节点释放时,_prev随之析构

这样就形成了相互制约的循环引用,导致内存无法释放。将ListNode结构体中的_next_prev改为weak_ptr,由于weak_ptr绑定到shared_ptr时不会增加引用计数,且不参与资源释放管理,成功打破了循环引用,解决了内存泄漏问题。

在这里插入图片描述

struct ListNode {
    int data;
    shared_ptr<ListNode> next;
    shared_ptr<ListNode> prev;
    ~ListNode() { cout << "~ListNode()" << endl; }
};

int main() {
    shared_ptr<ListNode> n1(new ListNode);
    shared_ptr<ListNode> n2(new ListNode);
    n1->next = n2;
    n2->prev = n1; // 循环引用,内存泄漏
}

解决方案:使用weak_ptr打破循环引用

struct ListNode {
    int data;
    weak_ptr<ListNode> next;
    weak_ptr<ListNode> prev;
};

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;
};

智能指针的高级用法

自定义删除器

智能指针支持自定义删除器,用于管理非new分配的资源:

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

// 仿函数删除器
shared_ptr<FILE> sp5(fopen("test.txt", "r"), Fclose());

// lambda删除器
shared_ptr<FILE> sp6(fopen("test.txt", "r"), [](FILE* ptr) {
    fclose(ptr);
});

make_shared

更高效的shared_ptr创建方式:

auto sp = make_shared<Date>(2024, 9, 11);

线程安全问题

  • shared_ptr的引用计数是线程安全的
  • 但指向的对象本身不是线程安全的
  • 需要额外同步机制保护共享数据

内存泄漏

什么是内存泄漏

程序未能释放不再使用的内存,导致可用内存逐渐减少。

如何避免内存泄漏

  1. 使用智能指针管理资源
  2. 遵循RAII原则
  3. 使用内存检测工具定期检查

总结

智能指针是C++中强大的资源管理工具,它们基于RAII原则,能够自动管理资源生命周期,显著减少内存泄漏和资源管理错误。理解各种智能指针的特点和适用场景,能够帮助我们编写更安全、更健壮的C++代码。

在实际开发中:

  • 优先使用unique_ptr,除非需要共享所有权
  • 需要共享所有权时使用shared_ptr
  • 遇到循环引用时使用weak_ptr
  • 对于非内存资源,使用自定义删除器
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值