【C++】智能指针

目录

    • [1. 为什么必须抛弃裸指针?](#1. 为什么必须抛弃裸指针?)
    • [2. 智能指针发展](#2. 智能指针发展)
      • [2.1 auto_ptr (C++98)](#2.1 auto_ptr (C++98))
      • [2.2 unique_ptr (C++11)](#2.2 unique_ptr (C++11))
      • [2.3 shared_ptr (C++11)](#2.3 shared_ptr (C++11))
    • [3. 深度解析 shared_ptr](#3. 深度解析 shared_ptr)
      • [3.1 内存优化:make_shared 与 直接 new 的区别](#3.1 内存优化:make_shared 与 直接 new 的区别)
      • [3.2 定制删除器(Deleter)的必要性](#3.2 定制删除器(Deleter)的必要性)
    • [4. 线程安全陷阱](#4. 线程安全陷阱)
    • [5. 循环引用灾难与 weak_ptr](#5. 循环引用灾难与 weak_ptr)
      • [5.1 循环引用是如何发生的?](#5.1 循环引用是如何发生的?)
      • [5.2 weak_ptr 解决循环引用](#5.2 weak_ptr 解决循环引用)
    • [6. 总结](#6. 总结)

在现代 C++ 开发中,内存管理是核心痛点。结合 CSDN 优质技术博客的深度解析与底层代码拆解,本文将全面剖析 C++ 智能指针的演进、底层原理、内存布局以及多线程安全陷阱。

1. 为什么必须抛弃裸指针?

传统 C/C++ 内存泄漏通常分为堆内存泄漏 (忘记释放 new/malloc 内存)和系统资源泄漏 (未关闭句柄、套接字等)。除了疏忽导致外,最大的隐患是异常安全问题

cpp 复制代码
void Func() {
    int* p1 = new int;
    // 若后续逻辑(或函数调用)抛出异常,直接跳转至 catch 块,delete p1 将永远无法执行
    cout << div() << endl; 
    delete p1; 
}

工程实践中,事前预防远胜于事后检测。C++ 采用 RAII(Resource Acquisition Is Initialization,资源获取即初始化)思想彻底解决这一问题。其核心机制是:在对象构造时获取并接管资源,在生命周期内保持有效,在对象析构时由编译器自动调用析构函数释放资源。

2. 智能指针发展

C++ 智能指针经历了从 C++98 到 C++11 的重大演进,标准库大量吸收了 Boost 库的核心实现原理。

2.1 auto_ptr (C++98)

auto_ptr 采用"管理权转移"机制:拷贝或赋值时,原对象的指针会被强行置空,资源完全交由新对象管理。 原对象内部指针被强行置空。

cpp 复制代码
#include <memory>
using namespace std;
int main() {
    auto_ptr<Date> ap1(new Date);
    // 拷贝时,管理权限转移,ap1 被悬空
    auto_ptr<Date> ap2(ap1); 
    // ap1->_year++; // 致命错误:此时 ap1 为空指针,解引用将导致程序崩溃
    return 0;
}
  • 致命缺陷:拷贝行为隐式剥夺了原对象的所有权,若后续代码无意中访问原对象,会导致对空指针的解引用,直接引发 Core Dump(崩溃)。目前已被诸多公司明令禁止,并在 C++11 后被正式废弃。

2.2 unique_ptr (C++11)

针对 auto_ptr 的痛点,unique_ptr 采用了简单粗暴的防拷贝机制:直接 delete 拷贝构造函数和赋值运算符,强制实现独占所有权语义

cpp 复制代码
#include <memory>
using namespace std;

int main() {
    unique_ptr<Date> up1(new Date);
    
    // unique_ptr<Date> up2(up1); // 编译报错:不支持拷贝
    
    // 若业务确实需要转移管理权,必须显式使用 std::move
    unique_ptr<Date> up3 = std::move(up1); 
    // 此时 up1 为空,资源完全由 up3 独占接管
    
    return 0;
}
  • 特性:由于没有额外的维护开销,它与裸指针的性能几乎一致(零开销抽象)。
  • 转移所有权 :必须显式调用 std::move() 才能将管理权转移给另一个 unique_ptr,语义极其清晰。

2.3 shared_ptr (C++11)

shared_ptr 允许多个智能指针管理同一块内存资源。它在内部维护了一个引用计数 ,新对象拷贝时计数 +1,对象销毁析构时计数 -1。当计数归零时,说明自己是最后一个使用者,触发 delete 释放底层资源。 并允许传入定制删除器以管理非 new 申请的资源

cpp 复制代码
#include <memory>
#include <iostream>
using namespace std;

// 自定义文件句柄删除器
class Fclose {
public:
    void operator()(FILE* ptr) {
        cout << "fclose:" << ptr << endl;
        fclose(ptr);
    }
};

int main() {
    // 1. 基础共享与引用计数测试
    shared_ptr<Date> sp1(new Date);
    shared_ptr<Date> sp2(sp1);
    shared_ptr<Date> sp3(sp2);
    cout << sp1.use_count() << endl; // 输出 3

    // 2. 高效初始化:make_shared 一次性分配内存,性能更优且避免内存碎片
    shared_ptr<Date> sp6 = make_shared<Date>(2024, 8, 5);

    // 3. 定制删除器:管理数组与文件
    shared_ptr<Date[]> sp4(new Date[5]); // 内部正确调用 delete[] 释放数组
    shared_ptr<FILE> sp5(fopen("test.cpp", "r"), Fclose()); // 析构时自动调用 fclose
    
    return 0;
}

3. 深度解析 shared_ptr

标准的 shared_ptr 并非只有一个指针那么简单,其内部通常包含两个指针:

  1. 指向所管理对象内存的数据指针
  2. 指向控制块(Control Block)的指针。控制块中存储了**强引用计数(use_count)、弱引用计数(weak_count)以及自定义删除器(Deleter)**等。

结合提供的底层模拟代码,分析核心机制:

cpp 复制代码
namespace bit {
    template<class T>
    class shared_ptr {
    public:
        // 构造:分配控制块(此处用 _pcount 模拟)
        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) {
            ++(*_pcount);
        }

        // 赋值:处理自我赋值,解除旧引用,绑定新引用
        shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
            if (_ptr != sp._ptr) {
                release(); // 递减旧计数,归零则释放
                _ptr = sp._ptr;
                _pcount = sp._pcount;
                ++(*_pcount);
            }
            return *this;
        }

        ~shared_ptr() { release(); }
        T* get() const { return _ptr; }
        int use_count() const { return *_pcount; }

    private:
        void release() {
            if (--(*_pcount) == 0) {
                _del(_ptr); // 调用删除器
                delete _pcount;
                _ptr = nullptr;
                _pcount = nullptr;
            }
        }
        T* _ptr;
        int* _pcount; // 控制块(引用计数)
        std::function<void(T*)> _del = [](T* ptr) { delete ptr; }; // 默认删除器
    };
}

3.1 内存优化:make_shared 与 直接 new 的区别

在标准的 std::shared_ptr<int> p(new int(10)) 初始化中,存在两次内存分配:一次为 new int(10) 分配数据内存,一次为控制块分配内存。

而使用 std::make_shared

  • 优势:只需进行一次内存分配,将数据对象与控制块放置在一块连续内存中。这不仅提升了性能(避免碎片化、提高缓存命中率),还避免了在两次分配之间若抛出异常导致的数据内存泄漏风险。

3.2 定制删除器(Deleter)的必要性

智能指针默认使用 delete 释放内存。但若资源是 new [] 出来的数组、malloc 的堆空间,或者是 fopen 产生的文件句柄,必须使用自定义删除器防止程序崩溃或系统资源泄漏。

cpp 复制代码
// 仿函数删除文件句柄
class Fclose {
public:
    void operator()(FILE* ptr) {
        cout << "fclose:" << ptr << endl;
        fclose(ptr);
    }
};

// 业务应用
bit::shared_ptr<FILE> sp5(fopen("test.cpp", "r"), Fclose());
// 使用 Lambda 表达式释放 malloc 内存
bit::shared_ptr<int> sp6((int*)malloc(40), [](int* ptr) { free(ptr); });

4. 线程安全陷阱

shared_ptr 的线程安全性需要分为两个维度严格界定:

  1. 控制块(引用计数)是线程安全的 :标准库通过原子操作(或加锁机制)保护引用计数的 ++--。因此,多线程同时对同一个 shared_ptr 对象进行拷贝析构是安全的。
  2. 管理的底层对象资源不是线程安全的 :智能指针不管堆上数据的线程安全问题。如果多线程中同时通过 shared_ptr 修改其指向的数据资源(如 copy->_year++),必须由开发者在业务层使用 mutex 加锁同步,否则会导致数据错乱。

5. 循环引用灾难与 weak_ptr

5.1 循环引用是如何发生的?

在构建双向链表或图结构时,若节点内使用 shared_ptr 互指,将引发灾难性后果。

cpp 复制代码
struct ListNode {
    bit::shared_ptr<ListNode> _next;
    bit::shared_ptr<ListNode> _prev;
    ~ListNode() { cout << "~ListNode()" << endl; }
};
// 互相链接后,n1 和 n2 的内部引用计数皆为 2
n1->_next = n2; 
n2->_prev = n1; 

离开作用域时,n1n2 析构,引用计数减为 1。但此时 n1n2._prev 持有,n2n1._next 持有。因为外部对象无法释放,内部成员也不会被析构,双方互相钳制,造成无法挽回的内存泄漏。

5.2 weak_ptr 解决循环引用

weak_ptr 是专门为解决 shared_ptr 循环引用而生的观察者指针。 weak_ptr 不支持 RAII 管理资源,主要用作 shared_ptr 的辅助工具。它不增加引用计数,可完美打破双向链表中的循环引用死锁。

cpp 复制代码
#include <memory>
#include <iostream>
using namespace std;

// 场景一:打破循环引用
struct ListNode {
    int _data;
    // 使用 weak_ptr 替代 shared_ptr 作为成员,避免互相钳制
    weak_ptr<ListNode> _next;
    weak_ptr<ListNode> _prev;
    
    ~ListNode() { cout << "~ListNode()" << endl; }
};

// 场景二:资源状态探测与安全提升
int main() {
    std::weak_ptr<ListNode> wp;
    std::shared_ptr<ListNode> sp;

    {
        std::shared_ptr<ListNode> n1(new ListNode);
        wp = n1; // weak_ptr 观测 shared_ptr,但不增加强引用计数
        cout << wp.expired() << endl; // 输出 0 (false),表示资源存活
        
        n1->_data++;
        
        // 安全访问数据:使用 lock() 尝试将 weak_ptr 提升为 shared_ptr
        sp = wp.lock(); 
    } // 离开作用域,n1 和 sp 析构,资源被彻底释放
    
    cout << wp.expired() << endl; // 输出 1 (true),表示底层资源已被释放

    return 0;
}
  • 原理weak_ptr 指向 shared_ptr 管理的资源,但绝不会增加控制块中的强引用计数(use_count)
  • 底层实现逻辑
cpp 复制代码
namespace bit {
    template<class T>
    class weak_ptr {
    public:
        weak_ptr() = default;
        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; // 不干预强引用计数的递增
    };
}
  • 状态探测与资源访问 :由于 weak_ptr 不保证资源存活,不能直接重载 operator*operator->(不支持 RAII)。使用标准库时,必须通过 wp.expired() 判断资源是否已被释放,并通过 wp.lock() 原子的将 weak_ptr 提升(Promote)为一个有效的 shared_ptr,才能安全访问数据。

6. 总结

  1. 优先使用 std::unique_ptr,只在需要绝对共享所有权时才使用 std::shared_ptr
  2. 尽可能使用 std::make_uniquestd::make_shared 进行初始化,以提升性能并保障异常安全性。
  3. 警惕双向链表、树的父子节点互指以及观察者模式中的循环引用,强制采用 weak_ptr 作为反向指针打破闭环。
相关推荐
春生野草15 小时前
个人笔记——C语言字符串、树
c语言·开发语言·笔记
风中芦苇啊15 小时前
Java EasyExcel 导入通用工具类:自定义注解映射字段 + 反射机制
java·开发语言
PieroPc15 小时前
Python 写的 Windows凭据添加工具 第二版 解决Win11 23H2, 25H2,26H1,共享文件和打印 一部份问题,不是全部!
开发语言·windows·python
家有娇妻张兔兔15 小时前
Java 对接 PLC 主流型号最合适的方案:Apache PLC4X 实战指南
java·开发语言·plc·modbus·数据缓存·西门子s7·apache plc4x
The_Ticker16 小时前
大宗商品行情API接入教程
开发语言·python·websocket·程序人生·区块链
Infedium16 小时前
Qt嵌入式开发核心亮点:线程安全硬件交互技术
开发语言·qt
阿里嘎多学长16 小时前
2026-07-30 GitHub 热点项目精选
开发语言·程序员·github·代码托管
摇滚侠16 小时前
Codebuddy 官网 Codebuddy IntelliJ IDEA 插件 阅读笔记 2
java·笔记·intellij-idea
gugucoding16 小时前
24. 【Java】枚举:更安全的常量
java·开发语言
一只旭宝16 小时前
C++手写shared_ptr共享智能指针|原子引用计数、强弱引用控制块、赋值重载底层深度剖析
开发语言·c++·面试