【C++】智能指针族谱:auto_ptr、unique_ptr、shared_ptr

C++标准库提供了多种智能指针,核心区别在于如何处理拷贝行为。这直接决定了它们在什么场景下能用、什么场景下会出问题。

目录

[1. auto_ptr:被时代抛弃的设计](#1. auto_ptr:被时代抛弃的设计)

[2. unique_ptr:独占,只移不拷](#2. unique_ptr:独占,只移不拷)

[3. shared_ptr:共享所有权与引用计数](#3. shared_ptr:共享所有权与引用计数)

[4. 模拟实现:引用计数的本质](#4. 模拟实现:引用计数的本质)


1. auto_ptr:被时代抛弃的设计

auto_ptr是C++98的产物,思路是"拷贝即转移管理权"。执行auto_ptr<T> ap2(ap1)之后,ap1内部的指针变成nullptr,资源归ap2。后续如果访问ap1,就是空指针行为,轻则崩溃,重则更难排查。

cpp

复制代码
auto_ptr<Date> ap1(new Date);
auto_ptr<Date> ap2(ap1);
// ap1已经悬空
// ap1->_year++;  // 未定义行为

这个设计违背直觉------看起来是拷贝,却暗中修改了源对象。C++11引入移动语义后,auto_ptr被标记为废弃,现代代码不应该再用。

2. unique_ptr:独占,只移不拷

unique_ptr把"独占"语义用语言机制明确表达出来:禁止拷贝,只支持移动 。拷贝构造和拷贝赋值被= delete了,移动构造和移动赋值则转移所有权,源指针同时置空。

cpp

复制代码
unique_ptr<Date> up1(new Date);
// unique_ptr<Date> up2(up1);   // 编译错误
unique_ptr<Date> up2(move(up1));  // OK,但up1悬空了

移动之后up1也不可再访问,但至少移动需要显式写move,让使用者在代码层面意识到所有权在转移。这种设计明确、安全。对于不需要共享资源的场景,unique_ptr是首选:零额外开销,独占语义清晰,也可以平稳转换成shared_ptr

unique_ptr构造用explicit修饰,防止裸指针隐式转换,这是为了避免不经意间把资源交给智能指针而用户无感知。

自定义删除器方面,unique_ptr把它作为模板参数

cpp

复制代码
// 函数指针做删除器
unique_ptr<Date, void(*)(Date*)> up(new Date[5], [](Date* p){ delete[] p; });

// 仿函数做删除器
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);

因为删除器类型是模板参数的一部分,不同删除器的unique_ptr是不同类型。这一点在使用时需要注意:作为函数参数传递时,类型会比较严格。

另外,unique_ptrshared_ptr都对operator new[]做了特化,可以直接unique_ptr<Date[]> up(new Date[5]),析构时会调delete[],不需要指定删除器。

3. shared_ptr:共享所有权与引用计数

当多个所有者需要共享同一份资源时,shared_ptr上场。它的核心机制是引用计数 :每份资源有一块独立的内存记录当前有多少个shared_ptr指向它。拷贝时计数加一,析构时计数减一,减到零时释放资源。

cpp

复制代码
shared_ptr<Date> sp1(new Date(2024, 9, 11));
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3 = sp1;
cout << sp1.use_count() << endl;  // 3

引用计数存在堆上 ,与管理的资源独立。为什么不能用静态成员?因为一份资源对应一个引用计数,不同的资源对象必须有不同的计数,静态成员是所有对象共享的,根本做不到。所以shared_ptr构造时会new int(1),然后所有拷贝构造和拷贝赋值都操作这个堆上的计数。

shared_ptr的自定义删除器通过构造函数参数 传递,并非模板参数,这一点与unique_ptr不同:

cpp

复制代码
shared_ptr<FILE> sp(fopen("test.cpp", "r"), [](FILE* f) { fclose(f); });

此外,shared_ptr提供了make_shared<T>(args...),直接用构造资源所需的参数来分配和构造,在堆上一次分配同时包含对象和引用计数控制块,比先new再传给shared_ptr构造函数少一次内存分配,也避免了部分异常安全问题。

4. 模拟实现:引用计数的本质

以下是shared_ptr核心骨架的简化实现,关键点在于引用计数是堆上的int*,以及拷贝、赋值操作对计数的正确维护:

cpp

复制代码
template<class T>
class shared_ptr {
public:
    explicit shared_ptr(T* ptr = nullptr)
        : _ptr(ptr), _pcount(new int(1)) {}

    template<class D>
    shared_ptr(T* ptr, D del)
        : _ptr(ptr), _pcount(new int(1)), _del(del) {}

    shared_ptr(const shared_ptr<T>& sp)
        : _ptr(sp._ptr), _pcount(sp._pcount), _del(sp._del) {
        ++(*_pcount);
    }

    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:
    void release() {
        if (--(*_pcount) == 0) {
            _del(_ptr);
            delete _pcount;
            _ptr = nullptr;
            _pcount = nullptr;
        }
    }

    T* _ptr;
    int* _pcount;
    function<void(T*)> _del = [](T* ptr) { delete ptr; };
};

release()是核心逻辑:每次计数减一,归零时代表当前对象是最后一个管理者,负责释放资源和计数本身。_del默认为delete,但可以通过构造时传入的删除器覆盖,实现文件句柄等非内存资源的统一管理。拷贝赋值先处理自己的release再指向新对象,顺序不能错------先加新计数再减老计数可以处理自身赋值自身的边界情况,但这里通过_ptr != sp._ptr的检查来避免。

相关推荐
ZC跨境爬虫1 小时前
跟着 MDN 学 HTML day_35:(深入解析 CharacterData 抽象接口)
java·前端·ui·html·edge浏览器·媒体
Brilliantwxx1 小时前
【C++】Stack和Queue(初认识和算法题OJ)
开发语言·c++·笔记·算法
录大大i1 小时前
javaWeb中使用AES256+RSA网络数据加密
java·网络·网络安全
洛水水1 小时前
B树与B+树详解
数据结构·b树
ch.ju1 小时前
Java Programming Chapter 3——If the array is out of range
java·开发语言
康小汪1 小时前
IntelliJ IDEA 安装教程(Windows 版)
java·windows·intellij-idea
枫叶丹41 小时前
【HarmonyOS 6.0】Desktop Extension Kit 正式接棒原状态栏服务,API 引用路径全面更新
开发语言·华为·harmonyos
fffzd1 小时前
C++入门(二)
开发语言·c++·算法·函数重载·引用·inline内联函数·nullptr