【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的检查来避免。

相关推荐
一个王同学2 小时前
从零到一 | CV转多模态大模型 | week17 | LLM 推理优化 & vLLM 详解
人工智能·深度学习·算法·机器学习·计算机视觉·vllm
jimy12 小时前
C语言模拟对象、方法:“函数指针+结构体“复用函数指针指向的函数体
c语言·开发语言
程序员张32 小时前
SpringBoot集成BCrypt密码加密库
java·spring boot·后端
闲猫2 小时前
Spring AI Agentic 模式(第1部分):Agent Skills——模块化、可复用的能力
java·人工智能·spring
源图客2 小时前
云途物流API开发-鉴权认证(Java)
java·网络·python
天外天-亮3 小时前
HBuilder X 使用 uview-plus 方式
开发语言·javascript·hbuilder x·uview-plus
liguojun20253 小时前
篮球馆自动计时收费系统:从规则配置到自动结算的全流程拆解
java·大数据·运维·人工智能·物联网·1024程序员节
木木子223 小时前
[特殊字符] 音乐播放器——状态驱动的多媒体控制
android·开发语言·华为·php·harmonyos
旖-旎3 小时前
《LeetCode 53 最大子数组和 || LeetCode 918 环形子数组的最大和》
c++·算法·leetcode·动态规划
952363 小时前
RabbitMQ-基础操作
java·spring boot·分布式·后端·spring·rabbitmq