Smart Pointer¶
C++ 中的智能指针(Smart Pointers)是 C++11 标准引入的重要特性,主要用于自动、安全地管理动态分配的内存。它们包含在 <memory> 头文件中
智能指针的核心思想是 RAII(Resource Acquisition Is Initialization,资源获取即初始化):将动态分配的内存封装在一个局部对象中,利用局部对象在离开作用域时自动调用析构函数的特性,来实现内存的自动释放。这能有效防止内存泄漏(Memory Leak)和悬垂指针(Dangling Pointer)
1 std::unique_ptr¶
std::unique_ptr 是一种独占性的智能指针。这意味着在任何时刻,一个对象只能被一个 unique_ptr 所拥有
- 不可拷贝:你不能直接将一个
unique_ptr赋值或拷贝给另一个unique_ptr(拷贝构造函数和拷贝赋值运算符被禁用) - 可移动:可以使用
std::move将所有权从一个unique_ptr转移给另一个。转移后,原来的指针变为空(nullptr)
推荐使用 std::make_unique(C++14 引入)来创建
2 std::shared_ptr¶
std::shared_ptr 允许多个智能指针共享同一个对象的所有权
- 内部包含一个引用计数器(Reference Count)
- 每次对
shared_ptr进行拷贝或赋值操作时,引用计数加 1 - 每次有
shared_ptr被销毁(离开作用域)或重新赋值时,引用计数减 1 - 当引用计数降为 0 时,对象才会被自动销毁
推荐使用 std::make_shared 来创建。它不仅使得代码更简洁,还能将对象实例和控制块(包含引用计数等信息)合并为一次内存分配,性能更好且能防止某些极端情况下的内存泄漏
make_shared 一次分配
当你使用 shared_ptr 管理一个对象时,底层其实需要维护两块信息:
- 对象实例本身:也就是你真正想使用的数据(比如一个
int,或者一个MyClass对象) -
控制块(Control Block):它是
shared_ptr用来实现共享和计数的东西。控制块里包含:- 强引用计数(有多少个
shared_ptr指向对象) - 弱引用计数(有多少个
weak_ptr观察对象) - 自定义删除器(如果有的话)等其他信息
- 强引用计数(有多少个
如果你这样写代码:
实际上发生了两步:
new MyClass()在堆内存中开辟了一块空间,用来存放MyClass对象。这是第一次内存分配shared_ptr的构造函数收到这个裸指针后,为了管理它,又在堆内存中单独开辟了另一块空间,用来存放“控制块”。这是第二次内存分配
两者的内存布局时分散的
如果你这样写代码:
std::make_shared 会在底层进行优化。它会先计算好“对象的大小”加上“控制块的大小”,然后向系统申请一整块足够大的连续内存,把控制块和对象挨着放在一起。两者的内存布局时连续的
- 性能更高(省时间):在堆上分配内存(调用
new或malloc)是一个相对耗时的操作系统操作。把两次分配减为一次分配,直接减少了一半的内存分配开销 - 内存更连续,缓存命中率高(省空间、速度快):控制块和对象在内存中是挨着的。当 CPU 读取
shared_ptr的控制块检查引用计数时,利用局部性原理,对象本身大概率也会被顺便加载到 CPU 的高速缓存(Cache)中,后续访问对象数据的速度会非常快 - 异常安全(防止内存泄漏):在早期的 C++11/14 中,如果写一个函数:
func(std::shared_ptr<T>(new T()), doSomethingElse());。如果new T()成功了,但在构造shared_ptr之前,doSomethingElse()抛出了异常,那么new T()申请的内存就会泄漏,因为没有指针接管它。而make_shared把分配和接管做成了一个不可分割的整体动作,消除了这个风险
自定义删除器
智能指针默认使用 delete 释放资源,但有时我们管理的不一定是普通内存(例如文件句柄 FILE*、网络套接字)。此时可以传入自定义删除器:
shared_ptr 是线程安全的吗
- 控制块的引用计数是线程安全的:底层通常使用原子操作(
std::atomic)实现计数的增减,因此多个线程同时拷贝、析构同一个shared_ptr是安全的 - 指针指向的对象的读写不是线程安全的:多个线程通过
shared_ptr并发修改其指向的底层对象时,如果没有加锁同步,会引发数据竞争(Data Race)。此外,对同一个shared_ptr实例本身进行并发读写操作(如一个线程赋值,另一个线程读取)也是不安全的,需要加锁
3 std::weak_ptr¶
std::weak_ptr 是为了配合 shared_ptr 而设计的。它提供对象的“非拥有”(观察者)访问权
- 不增加引用计数:将一个
shared_ptr赋值给weak_ptr不会改变对象的生命周期 - 不能直接访问:
weak_ptr没有重载*和->操作符。要想访问对象,必须先通过lock()方法尝试将其提升为一个有效的shared_ptr - 核心作用:解决
shared_ptr带来的循环引用(Circular Reference)问题
循环引用
如果对象 A 包含指向对象 B 的 shared_ptr,而对象 B 也包含指向对象 A 的 shared_ptr,那么它们的引用计数永远不会降为 0,从而导致内存泄漏。此时应将其中一个指针改为 weak_ptr 来打破循环
lock()
要想使用被 weak_ptr 指向的对象的方法,你必须先使用 lock() 方法。lock() 的作用是:检查观察的对象是否还存活
- 如果对象还存活:它会返回一个有效的
std::shared_ptr(同时会临时将引用计数加 1,保证在你使用期间对象绝对不会被其他地方销毁) - 如果对象已经销毁:它会返回一个空的
std::shared_ptr
std::auto_ptr 为什么被 C++11 废弃最终在 C++17 移除
auto_ptr 是早期的独占式智能指针。它的拷贝操作具有破坏性(会隐式地转移所有权)
auto_ptr<int> p2 = p1; 编译能通过,但赋值后,p1 会莫名其妙变成空指针。如果把 auto_ptr 放进 STL 容器(如 vector)并调用算法(如 sort 内部会产生拷贝),会导致严重崩溃危险
C++11 引入右值引用和移动语义后,用 unique_ptr 的 std::move 明确区分了拷贝和转移的概念,从而完美替代了它