智能指针详解
深入解析 C++ 智能指针 unique_ptr、shared_ptr、weak_ptr 的所有权语义、引用计数模型、形式化定义、工程实践与跨语言对比,覆盖 RAII、自定义删除器、循环引用破解与生产级模式。
第 1 章 学习目标与导论
1.1 本章在 C++ 知识体系中的位置
智能指针(smart pointer,由 David Klappholz 与 Peter Wegner 于 1987 年在面向对象编程文献中首次提出,“smart” 隐喻其具备自动管理资源的”智能”行为,与”哑指针(dumb pointer)“即裸指针相对)是现代 C++ 资源管理的核心抽象。它位于 C++ 知识体系的”所有权层”,向上承接 cpp/指针 中的裸指针机制,向下衔接 cpp/RAII资源管理、cpp/右值引用与移动语义、cpp/模板元编程 等现代 C++ 关键特性。
学习本章前,读者应当已经掌握:
cpp/概述与现代标准:C++11/14/17/20 的标准演进cpp/基础语法:变量声明、作用域、控制流cpp/数据类型详解:基本类型、模板基础cpp/指针:裸指针的内存模型与算术运算cpp/右值引用与移动语义:std::move、移动构造、值类别
掌握本章后,读者将能够阅读 cpp/RAII资源管理、cpp/模板元编程、cpp/多线程编程 等高级主题,并具备在生产代码中正确选择与组合智能指针的能力。
1.2 学习目标
本章遵循 Bloom 分类法,按认知层级递进组织学习目标:
- 记忆(Remember):复述
unique_ptr、shared_ptr、weak_ptr三种智能指针的所有权语义差异与引用计数状态机模型,识别make_unique、make_shared、enable_shared_from_this等核心 API。 - 理解(Understand):解释 RAII(Resource Acquisition Is Initialization,Bjarne Stroustrup 于 1980s 提出,“资源获取即初始化”)原则与 C++ 异常安全机制在智能指针设计中的形式化表达。
- 应用(Apply):使用
make_unique/make_shared工厂函数构建异常安全的资源管理代码,编写自定义删除器(custom deleter)管理非内存资源。 - 分析(Analyze):对比三种智能指针的所有权(ownership,借用日常语义”负责管理某资源生命周期”的责任归属)语义,分析循环引用导致内存泄漏的代数性质与
weak_ptr破解环路的数学论证。 - 评估(Evaluate):评估不同智能指针在多线程、ABI 稳定性、性能开销场景下的取舍,选择合适的所有权策略。
- 创造(Create):设计 Pimpl、工厂、依赖注入、对象池等基于智能指针的生产级架构,并保证异常安全与线程安全。
1.3 阅读建议
- 零基础读者:先通读第 2、3、4 章建立直观认识,再回看第 5、6 章深入引用计数;
- 有 C++03 经验读者:重点关注第 2 章历史动机与第 4-7 章现代 API;
- 进阶读者:直接研读第 8-12 章的工程实践与开源项目案例。
第 2 章 历史动机与演进
2.1 1980s:RAII 范式的诞生
C++ 的资源管理哲学根植于 Bjarne Stroustrup 在 1980s 提出的 RAII 原则。RAII 的核心思想是:将资源的获取绑定到对象的构造,资源的释放绑定到对象的析构。当对象离开作用域时,析构函数由编译器自动调用,无论退出路径是正常返回、提前 return 还是异常栈展开。
RAII 的动机来自 C 语言手动管理资源的痛点。考虑 C 风格的资源管理:
/* C 风格:每条错误路径都必须显式释放资源 */
int process(const char* path) {
FILE* f = fopen(path, "r");
if (!f) return -1;
Buffer* buf = allocate_buffer(1024);
if (!buf) {
fclose(f);
return -2;
}
if (read_data(f, buf) < 0) {
free_buffer(buf);
fclose(f);
return -3;
}
/* ... 业务逻辑 ... */
if (error) {
free_buffer(buf); /* 必须重复释放 */
fclose(f);
return -4;
}
free_buffer(buf);
fclose(f);
return 0;
}
每条错误路径都需要重复释放资源,极易遗漏。RAII 通过析构函数自动释放,从根本上消除这类问题:
#include <memory>
#include <cstdio>
#include <iostream>
// RAII 封装 FILE*
class FileGuard {
FILE* fp_;
public:
explicit FileGuard(const char* path, const char* mode)
: fp_(std::fopen(path, mode)) {
if (!fp_) throw std::runtime_error("无法打开文件");
}
~FileGuard() { if (fp_) std::fclose(fp_); }
FileGuard(const FileGuard&) = delete;
FileGuard& operator=(const FileGuard&) = delete;
FILE* get() const { return fp_; }
};
int main() {
FileGuard f("test.txt", "w");
std::fprintf(f.get(), "hello RAII\n");
// 离开作用域自动 fclose,即使抛异常也安全
return 0;
}
2.2 1990s:Boehm GC 与 auto_ptr 的折中
1990s 早期,C++ 社区曾就”是否引入垃圾回收”展开激烈讨论。Hans-J. Boehm 于 1993 年实现了保守式垃圾回收器(Boehm GC),但 Stroustrup 坚持认为:C++ 应保留确定性的资源管理(deterministic resource management),而非引入非确定性的 GC。这一决策最终体现为:
- 1995 年 C++95 标准引入
std::auto_ptr,提供独占所有权的智能指针; - 1998 年 C++98 标准正式将
auto_ptr纳入<memory>头; - 2011 年 C++11 引入
unique_ptr、shared_ptr、weak_ptr; - 2014 年 C++14 引入
std::make_unique(填补 C++11 的遗漏); - 2017 年 C++17 正式移除
auto_ptr,引入shared_ptr<T[]>数组特化。
2.3 1998:auto_ptr 的设计缺陷
std::auto_ptr 的核心缺陷在于复制构造隐式转移所有权:
#include <iostream>
#include <memory>
// 模拟 auto_ptr 行为(C++17 已移除,这里用伪代码演示)
template<typename T>
class AutoPtr {
T* ptr_;
public:
explicit AutoPtr(T* p = nullptr) : ptr_(p) {}
// 破坏性复制:源对象变空
AutoPtr(AutoPtr& other) noexcept : ptr_(other.release()) {}
AutoPtr& operator=(AutoPtr& other) noexcept {
if (this != &other) { delete ptr_; ptr_ = other.release(); }
return *this;
}
~AutoPtr() { delete ptr_; }
T* release() { T* p = ptr_; ptr_ = nullptr; return p; }
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
};
int main() {
AutoPtr<int> p(new int(42));
AutoPtr<int> q = p; // 复制!但 p 已为空
// *p = 100; // UB:p 已为空
std::cout << *q << "\n"; // 输出: 42
return 0;
}
这一行为与值语义直觉严重冲突,更致命的是不满足 STL 容器的 CopyConstructible 要求:
#include <vector>
#include <algorithm>
#include <memory>
// 假设 auto_ptr 仍可用
void buggy() {
std::vector<std::auto_ptr<int>> v;
v.push_back(std::auto_ptr<int>(new int(1)));
v.push_back(std::auto_ptr<int>(new int(2)));
// sort 内部通过复制实现元素交换
// 复制后源变空,元素悄然丢失
std::sort(v.begin(), v.end(),
[](const std::auto_ptr<int>& a, const std::auto_ptr<int>& b) {
return *a < *b;
});
// v 中部分元素可能已为空指针
}
2.4 2001:Boost 智能指针的影响
2001 年 Boost 1.21.0 引入了 boost::shared_ptr、boost::weak_ptr、boost::intrusive_ptr,由 Peter Dimov 等人设计。Boost 智能指针成为 C++11 标准智能指针的直接蓝本:
| Boost 引入 | C++ 标准化 | 差异 |
|---|---|---|
boost::scoped_ptr | std::unique_ptr(部分语义) | unique_ptr 支持移动 |
boost::shared_ptr | std::shared_ptr | 几乎完全一致 |
boost::weak_ptr | std::weak_ptr | 几乎完全一致 |
boost::intrusive_ptr | 未标准化 | 仍由 Boost 维护 |
boost::make_shared | std::make_shared | 完全一致 |
Andrei Alexandrescu 在 2001 年出版的《Modern C++ Design》中提出了基于 policy-based design 的 Loki::SmartPtr,允许通过模板参数组合所有权策略(deep copy、reference counting、COM intrusive 等),深刻影响了后续智能指针的设计哲学。
2.5 2011:C++11 的革命性引入
C++11 是智能指针演进史上的里程碑,引入了完整的现代智能指针体系:
| 特性 | 动机 | 替代/补充的旧机制 |
|---|---|---|
std::unique_ptr | 表达独占所有权,零开销抽象 | std::auto_ptr(已废弃) |
std::shared_ptr | 表达共享所有权,引用计数管理 | 手动引用计数、Boost |
std::weak_ptr | 打破 shared_ptr 循环引用 | 无标准方案 |
std::make_shared | 异常安全 + 单次分配 | 裸 new + 两次分配 |
std::enable_shared_from_this | 安全获取 this 的 shared_ptr | 危险的 shared_ptr<T>(this) |
| 右值引用与移动语义 | 显式表达所有权转移 | auto_ptr 的破坏性复制 |
= delete 显式删除 | 禁用拷贝构造 | private 拷贝构造的 hack |
C++11 同时废弃 auto_ptr(标记为 deprecated),C++17 正式将其从标准移除。
2.6 2014:make_unique 的补完
C++11 遗漏了 std::make_unique,导致代码风格不对称:
auto sp = std::make_shared<T>(args); // C++11 OK
// auto up = std::make_unique<T>(args); // C++11 不存在!
std::unique_ptr<T> up(new T(args)); // C++11 只能这样写
C++14(N3471 提案)补完 make_unique,并支持数组形式 make_unique<T[]>(n)。
2.7 2017 与 2023:精细化与稳定
C++17 关键改进:
- 移除
auto_ptr; - 引入
shared_ptr<T[]>数组特化(shared_ptr终于原生支持数组); make_shared支持对齐分配(make_shared<T>与alignas(T)兼容);weak_ptr::lock算法优化。
C++20 进一步:
- 引入
std::bit_cast,便于安全类型转换; - concepts 可约束智能指针模板参数(如
std::movable<T>约束 unique_ptr 的元素); consteval与constexpr增强,部分智能指针操作可在编译期执行。
C++23 引入 std::out_ptr 与 std::inout_ptr(用于与 C 风格 out-parameter API 互操作):
#include <memory>
#include <cstdio>
// C++23:std::out_ptr 简化 C API 互操作
void c_api(FILE** out);
int main() {
auto fp = std::make_unique<std::FILE, decltype(&std::fclose)>(nullptr, &std::fclose);
c_api(std::out_ptr(fp)); // C++23:自动管理 out 参数
return 0;
}
第 3 章 形式化定义与所有权模型
3.1 所有权的形式化定义
为了消除自然语言的歧义,本节以数学形式化方式定义智能指针的语义。该形式化对应 C++ 标准 [smartptr] 子条款。
设 为对象集合, 为 的幂集。设 为强引用计数函数(strong count), 为弱引用计数函数(weak count)。
定义 3.1(所有权语义):所有权关系 满足:
- 独占所有权(unique_ptr):
- 共享所有权(shared_ptr):
- 观察所有权(weak_ptr):,且 不增加
定义 3.2(unique_ptr):类型为 std::unique_ptr<T, D> 的智能指针 是一个四元组 ,其中 为托管对象或空, 为所指类型, 为删除器类型。形式上:
不变量 U1(独占性):在任意时刻,对任意 ,至多存在一个 unique_ptr 满足 。
不变量 U2(移动转移):std::move(u) 将 转换为右值,移动构造/赋值后源对象 ,目标对象获得原 。
定义 3.3(shared_ptr):类型为 std::shared_ptr<T> 的智能指针 是一个三元组 ,其中 为指向控制块(control block,shared_ptr 内部管理引用计数的元数据结构,包含 strong count 与 weak count 两个原子计数器)的指针。
其中 为强引用计数, 为弱引用计数, 为删除器, 为分配器。
不变量 S1(强引用一致性):对任意 与对应控制块 ,。
不变量 S2(弱引用独立性): 当且仅当存在 weak_ptr 引用 ; 不受 shared_ptr 拷贝/析构影响。
定义 3.4(weak_ptr):类型为 std::weak_ptr<T> 的智能指针 是一个二元组 ,仅持有控制块指针,不持有对象指针。
不变量 W1(不增加强引用):构造 时 ,但 不变。
不变量 W2(控制块存活): 存活;仅当两者均为 0 时控制块被释放。
3.2 引用计数的状态机模型
shared_ptr 的引用计数构成一个有限状态机:
stateDiagram-v2
[*] --> Constructed: make_shared/sc=1
Constructed --> Shared: copy/sc+1
Shared --> Constructed: reset/sc-1
Constructed --> ObjectDestroyed: sc=0/调用析构
ObjectDestroyed --> BlockAlive: wc>0
ObjectDestroyed --> [*]: wc=0/释放控制块
BlockAlive --> [*]: weak_ptr全部析构/wc=0
note right of ObjectDestroyed
对象析构但控制块存活
weak_ptr::lock 返回空
end note
状态转移规则:
- :
shared_ptr拷贝构造, - :
shared_ptrreset/析构, - :调用对象析构函数
- :释放控制块
- :等待所有 weak_ptr 释放
3.3 引用计数的代数性质
设 为指向同一对象 的 shared_ptr 集合,记 。引用计数满足以下代数性质:
性质 RC1(计数一致性): 在任意时刻成立。
性质 RC2(拷贝原子性): 的拷贝操作等价于原子执行 。
性质 RC3(析构顺序无关性): 的析构顺序不影响 的最终值(0)。
性质 RC4(线程安全):对 的并发读写满足顺序一致性(sequentially consistent)语义,但对象本身的访问需用户自行同步。
3.4 所有权不变量与对象生命周期
C++ 标准 [basic.life] 定义对象的生命周期为”构造完成到析构开始”的时间段。智能指针通过所有权不变量保证:
定理 3.1(生命周期保证):若对象 被 unique_ptr 托管,则 的生命周期 的生命周期。
证明:由不变量 U1, 是唯一持有 的智能指针。 析构时调用删除器 ,触发 的析构。在 存活期间, 必然存活。
定理 3.2(共享生命周期):若对象 被 个 shared_ptr 共享,则 的生命周期 ,且 。
证明:由不变量 S1,。每个 析构时 。当 时调用 的析构函数。因此 在所有 存活期间必然存活,且在最后一个 析构时立即析构(确定性析构)。
第 4 章 unique_ptr:独占所有权
4.1 基础用法
std::unique_ptr 是零开销抽象的智能指针,独占管理对象生命周期。它不可拷贝,仅可移动:
#include <memory>
#include <iostream>
class Widget {
public:
Widget() { std::cout << "Widget 构造\n"; }
~Widget() { std::cout << "Widget 析构\n"; }
void use() { std::cout << "Widget 使用\n"; }
};
int main() {
// 推荐:make_unique 异常安全
auto p = std::make_unique<Widget>();
p->use();
std::cout << "地址: " << p.get() << "\n";
// 输出:
// Widget 构造
// Widget 使用
// 地址: 0x...
// Widget 析构
return 0;
}
4.2 移动语义
unique_ptr 的拷贝构造与拷贝赋值被声明为 = delete,所有权转移必须通过 std::move 显式完成:
#include <memory>
#include <iostream>
int main() {
auto p = std::make_unique<int>(42);
// auto q = p; // 编译错误:拷贝构造被删除
auto q = std::move(p); // 移动构造:p 变空,q 获得所有权
std::cout << "p 是否为空: " << (p == nullptr) << "\n"; // 输出: 1
std::cout << "*q = " << *q << "\n"; // 输出: 42
return 0;
}
4.3 所有权转移状态机
stateDiagram-v2
[*] --> Owned_by_P: make_unique
Owned_by_P --> Owned_by_Q: std::move(p)
note right of Owned_by_Q
p.o = ⊥ (空)
q.o = 原 p.o
end note
Owned_by_Q --> [*]: q 析构
Owned_by_P --> [*]: p 析构(未转移)
4.4 release 与 reset
#include <memory>
#include <iostream>
int main() {
auto p = std::make_unique<int>(42);
// release:放弃所有权,返回裸指针
int* raw = p.release();
std::cout << "p 是否为空: " << (p == nullptr) << "\n"; // 输出: 1
std::cout << "*raw = " << *raw << "\n"; // 输出: 42
delete raw; // 调用者负责释放
// reset:替换托管对象
auto q = std::make_unique<int>(1);
q.reset(new int(100)); // 释放旧对象,接管新对象
std::cout << "*q = " << *q << "\n"; // 输出: 100
q.reset(); // 释放对象,q 变空
std::cout << "q 是否为空: " << (q == nullptr) << "\n"; // 输出: 1
return 0;
}
4.5 数组特化
unique_ptr<T[]> 是数组特化,提供 operator[] 但不提供 operator* 与 operator->:
#include <memory>
#include <iostream>
int main() {
auto arr = std::make_unique<int[]>(5);
for (int i = 0; i < 5; ++i) {
arr[i] = i * i;
}
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << ' '; // 输出: 0 1 4 9 16
}
std::cout << "\n";
// *arr; // 编译错误:数组特化不提供 operator*
// arr->...; // 编译错误:数组特化不提供 operator->
return 0;
}
注意:现代 C++ 推荐使用 std::vector 或 std::array 替代 unique_ptr<T[]>,因为标准容器提供更完整的接口(size、迭代器、范围检查等)。
4.6 自定义删除器
unique_ptr 的删除器作为类型参数的一部分,无运行时开销(编译期绑定):
#include <memory>
#include <cstdio>
#include <iostream>
// 使用 lambda 作为删除器
auto file_closer = [](FILE* f) {
if (f) {
std::fclose(f);
std::cout << "文件已关闭\n";
}
};
using UniqueFile = std::unique_ptr<FILE, decltype(file_closer)>;
UniqueFile openFile(const char* path, const char* mode) {
FILE* f = std::fopen(path, mode);
if (!f) throw std::runtime_error("无法打开文件");
return UniqueFile(f, file_closer);
}
int main() {
auto f = openFile("/tmp/test.txt", "w");
std::fprintf(f.get(), "hello\n");
// 离开作用域自动调用 file_closer
// 输出: 文件已关闭
return 0;
}
#include <memory>
#include <cstdlib>
#include <iostream>
// 函数指针删除器:unique_ptr<T, void(*)(T*)>
std::unique_ptr<int, void(*)(int*)> make_allocated(int v) {
return std::unique_ptr<int, void(*)(int*)>(new int(v), [](int* p) {
std::cout << "delete int\n";
delete p;
});
}
// 仿函数删除器(无状态):零开销
struct FreeDeleter {
void operator()(void* p) const {
std::free(p);
std::cout << "free\n";
}
};
int main() {
auto p = make_allocated(42);
std::cout << *p << "\n"; // 输出: 42
std::unique_ptr<int, FreeDeleter> fp(static_cast<int*>(std::malloc(sizeof(int))));
*fp = 100;
std::cout << *fp << "\n"; // 输出: 100
// 输出: free
return 0;
}
4.7 大小开销
unique_ptr<T> 的大小通常等于裸指针(1 个字):
#include <memory>
#include <iostream>
int main() {
std::cout << "sizeof(int*) = " << sizeof(int*) << "\n";
std::cout << "sizeof(unique_ptr<int>) = "
<< sizeof(std::unique_ptr<int>) << "\n";
// 无状态删除器时大小相同
std::cout << "sizeof(unique_ptr<int, void(*)(int*)>) = "
<< sizeof(std::unique_ptr<int, void(*)(int*)>) << "\n";
// 函数指针删除器多 1 个字
return 0;
}
输出(64 位 Linux):
sizeof(int*) = 8
sizeof(unique_ptr<int>) = 8
sizeof(unique_ptr<int, void(*)(int*)>) = 16
4.8 转换为 shared_ptr
unique_ptr 可隐式转换为 shared_ptr,反向不可:
#include <memory>
#include <iostream>
int main() {
std::unique_ptr<int> up = std::make_unique<int>(42);
std::shared_ptr<int> sp = std::move(up); // 隐式转换
std::cout << "up 是否为空: " << (up == nullptr) << "\n"; // 输出: 1
std::cout << "*sp = " << *sp << "\n"; // 输出: 42
std::cout << "sp use_count = " << sp.use_count() << "\n"; // 输出: 1
// std::unique_ptr<int> up2 = sp; // 编译错误:shared_ptr 不能转 unique_ptr
return 0;
}
4.9 工厂函数返回 unique_ptr
工厂函数返回 unique_ptr 是表达”调用者获得所有权”的最佳方式:
#include <memory>
#include <stdexcept>
#include <iostream>
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
virtual void print() const = 0;
};
class Circle : public Shape {
double r_;
public:
explicit Circle(double r) : r_(r) {}
double area() const override { return 3.14159 * r_ * r_; }
void print() const override {
std::cout << "Circle r=" << r_ << " area=" << area() << "\n";
}
};
class Rectangle : public Shape {
double w_, h_;
public:
Rectangle(double w, double h) : w_(w), h_(h) {}
double area() const override { return w_ * h_; }
void print() const override {
std::cout << "Rectangle " << w_ << "x" << h_
<< " area=" << area() << "\n";
}
};
// 工厂函数:返回 unique_ptr 表达所有权转移
std::unique_ptr<Shape> createShape(const std::string& type,
double a, double b = 0) {
if (type == "circle") return std::make_unique<Circle>(a);
if (type == "rectangle") return std::make_unique<Rectangle>(a, b);
throw std::invalid_argument("未知形状: " + type);
}
int main() {
auto s1 = createShape("circle", 2);
auto s2 = createShape("rectangle", 3, 4);
s1->print(); // 输出: Circle r=2 area=12.5664
s2->print(); // 输出: Rectangle 3x4 area=12
return 0;
}
第 5 章 shared_ptr:共享所有权
5.1 基础用法
std::shared_ptr 通过引用计数(reference counting,George Collins 于 1960 年在 IBM 论文中首次描述用于自动内存管理的引用计数技术)实现共享所有权:
#include <memory>
#include <iostream>
int main() {
auto p1 = std::make_shared<int>(42);
auto p2 = p1; // 拷贝:引用计数 +1
std::cout << "*p1 = " << *p1 << "\n"; // 输出: 42
std::cout << "use_count = " << p1.use_count() << "\n"; // 输出: 2
p1.reset();
std::cout << "p2 use_count = " << p2.use_count() << "\n"; // 输出: 1
std::cout << "*p2 = " << *p2 << "\n"; // 输出: 42
return 0;
}
5.2 引用计数流转图
graph LR
A[make_shared<br/>sc=1] -->|copy| B[sc=2]
B -->|copy| C[sc=3]
C -->|reset| B
B -->|reset| A
A -->|reset| D[sc=0<br/>析构对象]
D --> E{wc=0?}
E -->|是| F[释放控制块]
E -->|否| G[控制块存活<br/>weak_ptr::lock 返回空]
G -->|weak_ptr 全部析构| F
style D fill:#f96,color:#000
style F fill:#6c6,color:#fff
style G fill:#ff9,color:#000
5.3 make_shared 的优势
make_shared 将对象本体与控制块合并为单次堆分配,相比 shared_ptr<T>(new T) 有三大优势:
#include <memory>
#include <iostream>
class Big {
public:
Big() { std::cout << "Big 构造\n"; }
~Big() { std::cout << "Big 析构\n"; }
int data[1000];
};
int main() {
// 方式 A:make_shared —— 单次分配
auto a = std::make_shared<Big>();
// 方式 B:new + shared_ptr —— 两次分配
std::shared_ptr<Big> b(new Big());
// 两者语义等价,但 A 性能更优
return 0;
}
make_shared 的内存布局:
+-----------------------------------+
| 控制块 | Big 对象本体 |
+-----------------------------------+
^ ^
| |
| +-- shared_ptr 内部对象指针
+-- shared_ptr/weak_ptr 内部控制块指针
new 方式的内存布局:
+-------------+ +-------------+
| 控制块 | | Big 对象 |
+-------------+ +-------------+
^ ^
| |
+-- shared_ptr +-- shared_ptr 内部对象指针
内部控制块指针
优势 1:单次分配:减少一次 operator new 调用,提升性能。
优势 2:缓存局部性:对象与控制块在同一缓存行,减少 cache miss。
优势 3:异常安全:避免 f(shared_ptr<T>(new T), g()) 中 g() 抛异常导致的泄漏:
#include <memory>
void f(std::shared_ptr<int>, int);
int g();
// 风险:new T 与 g() 的求值顺序未指定
// 若 g() 先求值抛异常,new T 已分配但 shared_ptr 未构造,泄漏
void risky() {
f(std::shared_ptr<int>(new int(42)), g());
}
// 安全:make_shared 内部分配与构造原子
void safe() {
f(std::make_shared<int>(42), g());
}
5.4 make_shared 的劣势
make_shared 也有不适用场景:
#include <memory>
class Widget {
public:
Widget(int, int) {}
// 大对象 + 长期 weak_ptr 场景
};
// 场景 1:希望立即释放对象内存
// make_shared 的对象与控制块同块,weak_ptr 存活期间对象内存不释放
void case1() {
auto sp = std::make_shared<Widget>(1, 2);
std::weak_ptr<Widget> wp = sp;
sp.reset();
// 此时 Widget 已析构,但内存仍被控制块占用,直至 wp 析构
}
// 场景 2:自定义删除器(make_shared 不支持)
void case2() {
// auto p = std::make_shared<FILE>(...); // 无法传入 fclose
std::shared_ptr<FILE> fp(std::fopen("a.txt", "r"),
std::fclose);
}
// 场景 3:类私有构造函数(make_shared 无法 friend)
class PrivateCtor {
PrivateCtor(int) {}
friend std::unique_ptr<PrivateCtor> std::make_unique<PrivateCtor>(int&&);
public:
static std::shared_ptr<PrivateCtor> create(int v) {
// make_shared 无法访问私有构造
return std::shared_ptr<PrivateCtor>(new PrivateCtor(v));
}
};
5.5 别名构造(Aliasing Constructor)
别名构造(aliasing constructor,C++11 引入的 shared_ptr<T>(shared_ptr<U>, T*) 构造形式,“aliasing” 指针别名,共享控制块但指向不同对象)允许创建指向成员但共享整个对象生命周期的 shared_ptr:
#include <memory>
#include <string>
#include <iostream>
struct Config {
std::string host;
int port;
std::string database;
};
// 返回指向 config->host 的 shared_ptr,引用计数与 config 共享
std::shared_ptr<std::string> getHost(std::shared_ptr<Config> config) {
return std::shared_ptr<std::string>(config, &config->host);
}
int main() {
auto cfg = std::make_shared<Config>(Config{"localhost", 3306, "mydb"});
auto host = getHost(cfg);
cfg.reset(); // cfg 释放,但 host 仍持有
std::cout << *host << "\n"; // 输出: localhost
std::cout << "use_count = " << host.use_count() << "\n"; // 输出: 1
// Config 对象存活至 host 析构
return 0;
}
5.6 enable_shared_from_this
当对象被 shared_ptr 管理时,如何安全地获取指向自身的 shared_ptr?直接 shared_ptr<T>(this) 会触发 double-free:
#include <memory>
#include <iostream>
class Bad : public std::enable_shared_from_this<Bad> {
public:
std::shared_ptr<Bad> getSelf() {
// return std::shared_ptr<Bad>(this); // 错误:double-free!
return shared_from_this(); // 正确:共享控制块
}
};
int main() {
auto p = std::make_shared<Bad>();
auto q = p->getSelf();
std::cout << "use_count = " << p.use_count() << "\n"; // 输出: 2
return 0;
}
enable_shared_from_this<T> 内部持有一个 weak_ptr<T>,在 shared_ptr 首次构造该对象时被初始化为指向 this 的 weak_ptr。shared_from_this() 调用 weak_ptr::lock() 返回 shared_ptr,与原 shared_ptr 共享控制块。
5.7 类型转换
shared_ptr 支持安全的派生类到基类转换:
#include <memory>
#include <iostream>
class Base {
public:
virtual ~Base() = default;
virtual void who() { std::cout << "Base\n"; }
};
class Derived : public Base {
public:
void who() override { std::cout << "Derived\n"; }
};
int main() {
std::shared_ptr<Derived> d = std::make_shared<Derived>();
// 隐式向上转换
std::shared_ptr<Base> b = d; // 拷贝,引用计数共享
std::cout << "use_count = " << b.use_count() << "\n"; // 输出: 2
// 显式向下转换
std::shared_ptr<Derived> d2 = std::static_pointer_cast<Derived>(b);
std::cout << "use_count = " << b.use_count() << "\n"; // 输出: 3
// dynamic_pointer_cast:失败返回空
std::shared_ptr<Derived> d3 = std::dynamic_pointer_cast<Derived>(b);
std::cout << "d3 是否为空: " << (d3 == nullptr) << "\n"; // 输出: 0
// const_pointer_cast:去除 const
std::shared_ptr<const Base> cb = b;
std::shared_ptr<Base> nb = std::const_pointer_cast<Base>(cb);
b->who(); // 输出: Derived
return 0;
}
5.8 数组特化(C++17)
C++17 之前 shared_ptr 不原生支持数组,需要自定义删除器:
#include <memory>
#include <iostream>
int main() {
// C++11/14:自定义删除器
std::shared_ptr<int> oldWay(new int[10], std::default_delete<int[]>());
// oldWay[0] = 1; // 编译错误:shared_ptr<int> 无 operator[]
// C++17:原生数组特化
std::shared_ptr<int[]> arr = std::make_shared<int[]>(10);
for (int i = 0; i < 10; ++i) arr[i] = i;
std::cout << arr[5] << "\n"; // 输出: 5
// C++20:make_shared 对齐版本
// std::shared_ptr<int[]> arr2 = std::make_shared<int[]>(10, 42); // C++20
return 0;
}
5.9 线程安全性
shared_ptr 的线程安全遵循以下规则(对应标准 [util.smartptr.shared]):
- 引用计数操作线程安全:拷贝/析构 shared_ptr 是原子的;
- 同一 shared_ptr 实例的读写不线程安全:多线程访问同一 shared_ptr 实例需同步;
- 不同 shared_ptr 实例的访问线程安全:即使指向同一对象,不同实例可被不同线程访问;
- 对象本身的访问不线程安全:通过 shared_ptr 访问对象需自行同步。
#include <memory>
#include <mutex>
#include <vector>
#include <iostream>
// 线程安全的共享数据容器
class ThreadSafeData {
std::shared_ptr<const std::vector<int>> data_;
mutable std::mutex mtx_;
public:
ThreadSafeData() : data_(std::make_shared<const std::vector<int>>()) {}
std::shared_ptr<const std::vector<int>> get() const {
std::lock_guard<std::mutex> lk(mtx_);
return data_; // 返回拷贝,线程安全
}
void update(std::shared_ptr<const std::vector<int>> newData) {
std::lock_guard<std::mutex> lk(mtx_);
data_ = std::move(newData);
}
size_t size() const {
std::lock_guard<std::mutex> lk(mtx_);
return data_->size();
}
};
int main() {
ThreadSafeData tsd;
auto v = std::make_shared<const std::vector<int>>(std::vector<int>{1, 2, 3});
tsd.update(v);
std::cout << "size = " << tsd.size() << "\n"; // 输出: 3
return 0;
}
5.10 控制块创建时机
控制块在以下时机创建:
- 调用
make_shared; - 从
unique_ptr构造shared_ptr; - 用裸指针构造
shared_ptr。
陷阱:同一裸指针构造两个 shared_ptr 会创建两个控制块,导致 double-free:
#include <memory>
int main() {
int* raw = new int(42);
std::shared_ptr<int> p1(raw);
// std::shared_ptr<int> p2(raw); // UB:两个独立控制块,double-free
return 0;
}
第 6 章 weak_ptr:打破循环引用
6.1 基础用法
std::weak_ptr 是 shared_ptr 的”观察者”,不增加强引用计数:
#include <memory>
#include <iostream>
int main() {
auto sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp; // 不增加 strong count
std::cout << "sp use_count = " << sp.use_count() << "\n"; // 输出: 1
std::cout << "wp use_count = " << wp.use_count() << "\n"; // 输出: 1
std::cout << "wp expired = " << wp.expired() << "\n"; // 输出: 0
// 安全访问:lock() 原子升级为 shared_ptr
if (auto locked = wp.lock()) {
std::cout << "*locked = " << *locked << "\n"; // 输出: 42
}
sp.reset();
std::cout << "wp expired = " << wp.expired() << "\n"; // 输出: 1
auto locked = wp.lock();
std::cout << "locked 是否为空: " << (locked == nullptr) << "\n"; // 输出: 1
return 0;
}
6.2 循环引用的形式化分析
设两个对象 与 ,各自持有一个 shared_ptr 指向对方:
当外部 shared_ptr 离开作用域后:
两者相互维持非零,构成不动点,无法归零,导致内存泄漏。
#include <memory>
#include <iostream>
struct Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev;
~Node() { std::cout << "Node 析构\n"; }
};
int main() {
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b; // strong(b) += 1
b->prev = a; // strong(a) += 1
std::cout << "a use_count = " << a.use_count() << "\n"; // 输出: 2
std::cout << "b use_count = " << b.use_count() << "\n"; // 输出: 2
return 0;
// a、b 离开作用域:strong(a)=1, strong(b)=1,永不析构!
// 无 "Node 析构" 输出
}
6.3 weak_ptr 破解循环引用
将环路一侧改为 weak_ptr 即可打破不动点:
graph LR
A[a shared_ptr<br/>strong=1] -->|next: shared_ptr| B[b]
B -->|prev: weak_ptr<br/>不增加 strong| A
style A fill:#6c6,color:#fff
style B fill:#69f,color:#fff
#include <memory>
#include <iostream>
struct Node {
std::shared_ptr<Node> next; // 强引用:拥有后继
std::weak_ptr<Node> prev; // 弱引用:仅观察前驱
~Node() { std::cout << "Node 析构\n"; }
};
int main() {
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b; // strong(b) = 2
b->prev = a; // weak count +1,strong(a) 不变 = 1
std::cout << "a use_count = " << a.use_count() << "\n"; // 输出: 1
std::cout << "b use_count = " << b.use_count() << "\n"; // 输出: 2
return 0;
// a 离开作用域:strong(a)=0,a 析构
// a 析构 → a->next 析构 → strong(b)=1
// b 离开作用域:strong(b)=0,b 析构
// 输出: Node 析构 (两次)
}
6.4 weak_ptr::lock 的原子性
weak_ptr::lock() 是原子操作,避免 TOCTOU(Time-of-Check-to-Time-of-Use)竞态:
#include <memory>
#include <iostream>
// 错误模式:expired() 与构造 shared_ptr 之间存在窗口
void risky(std::weak_ptr<int>& wp) {
if (!wp.expired()) {
// 此时另一线程可能 reset,wp 已过期
// std::shared_ptr<int> sp(wp); // 抛 bad_weak_ptr
}
}
// 正确模式:lock() 原子地"检查 + 升级"
void safe(std::weak_ptr<int>& wp) {
if (auto sp = wp.lock()) {
// sp 持有对象,安全使用
std::cout << *sp << "\n";
} else {
std::cout << "对象已析构\n";
}
}
6.5 缓存模式
weak_ptr 是实现缓存(cache)的理想工具:缓存持有 weak_ptr,对象生命周期由外部 shared_ptr 决定:
#include <memory>
#include <unordered_map>
#include <mutex>
#include <iostream>
class ExpensiveResource {
public:
ExpensiveResource(int id) : id_(id) {
std::cout << "构造资源 " << id_ << "\n";
}
~ExpensiveResource() {
std::cout << "析构资源 " << id_ << "\n";
}
int id_;
};
class ResourceCache {
std::unordered_map<int, std::weak_ptr<ExpensiveResource>> cache_;
mutable std::mutex mtx_;
public:
std::shared_ptr<ExpensiveResource> get(int id) {
std::lock_guard<std::mutex> lk(mtx_);
auto it = cache_.find(id);
if (it != cache_.end()) {
if (auto sp = it->second.lock()) {
std::cout << "缓存命中 " << id << "\n";
return sp;
}
}
std::cout << "缓存未命中 " << id << "\n";
auto sp = std::make_shared<ExpensiveResource>(id);
cache_[id] = sp;
return sp;
}
};
int main() {
ResourceCache cache;
{
auto r1 = cache.get(1); // 输出: 缓存未命中 1 / 构造资源 1
auto r2 = cache.get(1); // 输出: 缓存命中 1
} // r1、r2 析构,资源 1 析构
auto r3 = cache.get(1); // 输出: 缓存未命中 1 / 构造资源 1
return 0;
}
6.6 观察者模式
观察者模式中,被观察对象(subject)不应持有观察者的强引用:
#include <memory>
#include <vector>
#include <algorithm>
#include <iostream>
class Observer {
public:
virtual ~Observer() = default;
virtual void onEvent(int) = 0;
};
class Subject {
std::vector<std::weak_ptr<Observer>> observers_;
public:
void subscribe(std::shared_ptr<Observer> o) {
observers_.push_back(o);
}
void notify(int event) {
// 清理过期观察者,并通知存活观察者
observers_.erase(
std::remove_if(observers_.begin(), observers_.end(),
[](const std::weak_ptr<Observer>& w) {
return w.expired();
}),
observers_.end());
for (auto& w : observers_) {
if (auto sp = w.lock()) {
sp->onEvent(event);
}
}
}
};
class ConcreteObserver : public Observer {
int id_;
public:
explicit ConcreteObserver(int id) : id_(id) {}
void onEvent(int e) override {
std::cout << "Observer " << id_ << " 收到事件 " << e << "\n";
}
};
int main() {
Subject subject;
auto o1 = std::make_shared<ConcreteObserver>(1);
auto o2 = std::make_shared<ConcreteObserver>(2);
subject.subscribe(o1);
subject.subscribe(o2);
subject.notify(42);
// 输出:
// Observer 1 收到事件 42
// Observer 2 收到事件 42
o1.reset();
subject.notify(100);
// 输出: Observer 2 收到事件 100(o1 已析构,自动跳过)
return 0;
}
6.7 异步回调中的安全引用
异步回调(如 detach 线程、定时器)中,捕获 shared_ptr 延长对象生命周期,捕获 weak_ptr 检查存活:
#include <memory>
#include <thread>
#include <chrono>
#include <iostream>
class Service : public std::enable_shared_from_this<Service> {
public:
void startAsync() {
// 延长生命周期模式:捕获 shared_ptr
auto self = shared_from_this();
std::thread([self]() {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
self->processResult();
}).detach();
}
// 仅检查存活模式:捕获 weak_ptr
void startCheckOnly() {
std::weak_ptr<Service> weak = shared_from_this();
std::thread([weak]() {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
if (auto sp = weak.lock()) {
sp->processResult();
} else {
std::cout << "Service 已析构,跳过\n";
}
}).detach();
}
void processResult() { std::cout << "处理结果\n"; }
};
int main() {
{
auto svc = std::make_shared<Service>();
svc->startAsync();
svc->startCheckOnly();
} // svc 析构,但 startAsync 的线程仍持有 self
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// 输出:
// 处理结果(来自 startAsync)
// Service 已析构,跳过(来自 startCheckOnly)
return 0;
}
第 7 章 make_unique 与 make_shared 的异常安全
7.1 异常安全的数学论证
考虑函数调用 f(std::shared_ptr<T>(new T), g())。C++17 前的求值顺序未完全指定:
new T分配内存并构造对象;g()求值;shared_ptr构造。
若编译器选择顺序 new T → g() → shared_ptr 构造,且 g() 抛异常,则 new T 已分配但 shared_ptr 未构造,内存泄漏。
C++17 修订了求值顺序,但 make_shared 仍是更安全的选择:
#include <memory>
void f(std::shared_ptr<int>, int);
int g(); // 可能抛异常
// 风险路径(C++17 前可能泄漏)
void risky() {
f(std::shared_ptr<int>(new int(42)), g());
}
// 安全路径(make_shared 单次原子分配)
void safe() {
f(std::make_shared<int>(42), g());
}
7.2 make_unique 的实现剖析
C++14 标准库的 make_unique 实现简化版:
#include <memory>
#include <type_traits>
// C++14 N3471 提案的简化实现
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
// 数组特化
template<typename T>
std::unique_ptr<T[]> make_unique(size_t n) {
return std::unique_ptr<T[]>(new std::remove_extent_t<T>[n]());
}
7.3 make_shared 的内存布局优化
make_shared 的典型实现使用单次 operator new 分配”控制块 + 对象”的合并内存:
// 简化伪代码
template<typename T, typename... Args>
std::shared_ptr<T> make_shared(Args&&... args) {
// 单次分配,包含控制块与 T 对象
auto* block = static_cast<ControlBlock<T>*>(
::operator new(sizeof(ControlBlock<T>)));
new (&block->storage) T(std::forward<Args>(args)...);
new (&block->ctrl) ControlBlock();
return std::shared_ptr<T>(&block->storage, &block->ctrl);
}
控制块典型结构:
+---------------------------+
| vptr (虚函数表指针) |
| strong count (atomic) |
| weak count (atomic) |
| deleter (类型擦除) |
| allocator (类型擦除) |
+---------------------------+
| T 对象存储 |
+---------------------------+
7.4 性能对比
#include <memory>
#include <chrono>
#include <iostream>
class Widget {
int data_[64];
public:
Widget() { data_[0] = 42; }
};
int main() {
const int N = 1000000;
auto t1 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i) {
auto sp = std::make_shared<Widget>();
}
auto t2 = std::chrono::high_resolution_clock::now();
auto t3 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i) {
std::shared_ptr<Widget> sp(new Widget());
}
auto t4 = std::chrono::high_resolution_clock::now();
auto make_dur = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1);
auto new_dur = std::chrono::duration_cast<std::chrono::milliseconds>(t4 - t3);
std::cout << "make_shared: " << make_dur.count() << " ms\n";
std::cout << "new + shared_ptr: " << new_dur.count() << " ms\n";
// 输出(典型,依机器而异):
// make_shared: 38 ms
// new + shared_ptr: 76 ms
return 0;
}
第 8 章 自定义删除器与类型擦除
8.1 unique_ptr 的删除器作为类型参数
unique_ptr<T, D> 的删除器 是类型的一部分,无运行时多态开销:
#include <memory>
#include <cstdio>
#include <cstdlib>
#include <iostream>
// 不同删除器 → 不同类型
using UniqueInt = std::unique_ptr<int>;
using UniqueIntNew = std::unique_ptr<int, void(*)(int*)>;
using UniqueIntFree = std::unique_ptr<int, void(*)(void*)>;
// 仿函数删除器:可能零开销
struct Deleter {
void operator()(int* p) const {
delete p;
std::cout << "Deleter 调用\n";
}
};
int main() {
UniqueInt a(new int(1));
UniqueIntNew b(new int(2), [](int* p) { delete p; });
// a = b; // 编译错误:类型不同
return 0;
}
8.2 shared_ptr 的类型擦除删除器
shared_ptr 的删除器不作为类型参数,运行时类型擦除:
#include <memory>
#include <cstdio>
#include <iostream>
int main() {
// 不同删除器,相同类型 shared_ptr<FILE>
std::shared_ptr<FILE> f1(std::fopen("a.txt", "r"), std::fclose);
std::shared_ptr<FILE> f2(std::fopen("b.txt", "r"),
[](FILE* f) {
std::fclose(f);
std::cout << "lambda 关闭\n";
});
// 类型擦除:删除器存储在控制块
std::cout << "sizeof(f1) = " << sizeof(f1) << "\n"; // 输出: 16
std::cout << "sizeof(f2) = " << sizeof(f2) << "\n"; // 输出: 16
return 0;
}
8.3 管理 C API 资源
#include <memory>
#include <cstdlib>
#include <cstdio>
#include <windows.h>
#include <iostream>
// 1. malloc/free
auto malloc_ptr(int n) {
return std::unique_ptr<int, void(*)(void*)>(
static_cast<int*>(std::malloc(n * sizeof(int))),
std::free);
}
// 2. FILE*
auto open_file(const char* path, const char* mode) {
return std::unique_ptr<FILE, decltype(&std::fclose)>(
std::fopen(path, mode), std::fclose);
}
// 3. Windows HANDLE(实际是指针类型)
#ifdef _WIN32
auto create_handle() {
return std::unique_ptr<std::remove_pointer_t<HANDLE>,
decltype(&CloseHandle)>(
CreateEventW(nullptr, FALSE, FALSE, nullptr),
CloseHandle);
}
#endif
// 4. POSIX fd(int 不是指针,需自定义删除器类)
struct FdDeleter {
void operator()(int* fd) const {
if (fd && *fd >= 0) {
// POSIX close(*fd);
std::cout << "close fd " << *fd << "\n";
}
delete fd;
}
};
using UniqueFd = std::unique_ptr<int, FdDeleter>;
int main() {
auto p = malloc_ptr(10);
p[0] = 42;
auto f = open_file("/tmp/test.txt", "w");
std::fprintf(f.get(), "hello\n");
auto fd = UniqueFd(new int(3));
return 0;
}
8.4 SDL 资源管理
#include <memory>
// 假设 SDL2 头文件
struct SDL_Window;
struct SDL_Renderer;
extern "C" void SDL_DestroyWindow(SDL_Window*);
extern "C" void SDL_DestroyRenderer(SDL_Renderer*);
// RAII 包装器
template<typename T, void(*Deleter)(T*)>
struct SdlDeleter {
void operator()(T* p) const { if (p) Deleter(p); }
};
template<typename T, void(*D)(T*)>
using SdlUnique = std::unique_ptr<T, SdlDeleter<T, D>>;
using UniqueWindow = SdlUnique<SDL_Window, SDL_DestroyWindow>;
using UniqueRenderer = SdlUnique<SDL_Renderer, SDL_DestroyRenderer>;
// 使用:与普通 unique_ptr 一致
// UniqueWindow win(SDL_CreateWindow(...));
8.5 类型擦除的代价
shared_ptr 的类型擦除删除器带来大小开销:
#include <memory>
#include <iostream>
int main() {
std::cout << "sizeof(unique_ptr<int>) = "
<< sizeof(std::unique_ptr<int>) << "\n"; // 8
std::cout << "sizeof(unique_ptr<int, void(*)(int*)>) = "
<< sizeof(std::unique_ptr<int, void(*)(int*)>) << "\n"; // 16
std::cout << "sizeof(shared_ptr<int>) = "
<< sizeof(std::shared_ptr<int>) << "\n"; // 16
std::cout << "sizeof(weak_ptr<int>) = "
<< sizeof(std::weak_ptr<int>) << "\n"; // 16
return 0;
}
shared_ptr 与 weak_ptr 始终是 2 个字(16 字节):一个指向对象,一个指向控制块。
第 9 章 对比分析
9.1 与裸指针对比
| 特性 | 裸指针(T*) | unique_ptr<T> | shared_ptr<T> |
|---|---|---|---|
| 所有权语义 | 无 | 独占 | 共享 |
| 内存释放 | 手动 delete | 自动 | 自动(引用计数) |
| 大小开销 | 1 字 | 1 字(无删除器) | 2 字 |
| 拷贝语义 | 可拷贝 | 仅移动 | 可拷贝 |
| 异常安全 | 不保证 | 保证 | 保证 |
| 与 C 接口兼容 | 兼容 | 通过 get() | 通过 get() |
| 算术运算 | 支持 | 不支持 | 不支持 |
| 数组支持 | T* | unique_ptr<T[]> | shared_ptr<T[]>(C++17) |
| 性能开销 | 零 | 零 | 原子计数 |
9.2 与 Rust Box/Rc/RefCell 对比
| 特性 | C++ unique_ptr | Rust Box<T> | C++ shared_ptr | Rust Rc<T>/Arc<T> |
|---|---|---|---|---|
| 所有权转移 | std::move | 隐式 move | 拷贝共享 | clone 共享 |
| 编译期检查 | 弱(运行时) | 强(借用检查器) | 弱(运行时) | 中(编译期所有权) |
| 内部可变性 | 直接修改 | 需 Box<RefCell<T>> | 直接修改 | 需 Rc<RefCell<T>> |
| 线程安全 | 不保证(需 mutex) | 不保证 | 计数器原子 | Arc 计数器原子 |
| 循环引用 | weak_ptr | Weak<T> | weak_ptr | Weak<T> |
| 大小 | 1 字 | 1 字 | 2 字 | 2 字 |
| 零开销抽象 | 是 | 是 | 否 | 否 |
// Rust 等价示例
use std::rc::{Rc, Weak};
use std::cell::RefCell;
let a = Rc::new(RefCell::new(42));
let b = a.clone(); // 引用计数 +1
let w: Weak<i32> = Rc::downgrade(&a); // 弱引用
// Rust 借用检查器在编译期保证:
// - 同一时刻只能有一个可变借用,或多个不可变借用
// - Box<T> 独占所有权,编译期禁止拷贝
9.3 与 Java 引用类型对比
| 特性 | C++ shared_ptr | Java Strong Reference | Java WeakReference | Java SoftReference |
|---|---|---|---|---|
| 回收机制 | 引用计数(确定性) | GC(非确定性) | GC(next cycle) | GC(内存不足时) |
| 循环引用 | 不自动回收 | GC 自动回收 | GC 自动回收 | GC 自动回收 |
| 性能开销 | 原子计数 | GC 暂停 | GC 暂停 | GC 暂停 |
| 内存释放时机 | 立即 | 不可预测 | next GC | 内存压力时 |
| 适用场景 | 系统编程 | 业务应用 | 缓存 | 内存敏感缓存 |
Java 的 GC 能自动处理循环引用,但代价是非确定性回收与潜在暂停。
9.4 与 Go GC 对比
| 特性 | C++ shared_ptr | Go GC |
|---|---|---|
| 回收机制 | 引用计数(确定性) | 三色标记(并发) |
| 循环引用 | 不自动回收 | 自动回收 |
| 暂停时间 | 零 | < 1ms(Go 1.21+) |
| 内存开销 | 控制块(~32 字节) | 指针 bitmap |
| 编程模型 | 显式所有权 | 隐藏所有权 |
| 与系统资源 | RAII 通用 | 仅内存 |
Go 不提供智能指针,所有引用默认共享,由 GC 自动管理。Go 的设计哲学是简化心智负担,C++ 则追求确定性资源管理。
9.5 设计哲学对比
graph TB
A[资源管理哲学] --> B[C++ 智能指针<br/>确定性 + 显式所有权]
A --> C[Rust 借用检查<br/>编译期 + 类型系统]
A --> D[Java/Go GC<br/>运行时 + 自动回收]
B --> B1[优势: 零暂停, 通用 RAII]
B --> B2[劣势: 循环引用需手动]
C --> C1[优势: 编译期保证]
C --> C2[劣势: 学习曲线陡]
D --> D1[优势: 心智简单]
D --> D2[劣势: 暂停, 非确定]
style B fill:#69f,color:#fff
style C fill:#6c6,color:#fff
style D fill:#f96,color:#000
第 10 章 常见陷阱
10.1 循环引用
错误示例:
#include <memory>
struct Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev;
};
int main() {
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a; // 循环引用!
return 0; // 内存泄漏
}
原因:strong(a) 与 strong(b) 互为 1,构成不动点。
修正:将一侧改为 weak_ptr:
#include <memory>
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev;
};
int main() {
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a; // 弱引用,不增加 strong count
return 0; // 正确析构
}
10.2 双重管理(Double-Free from Two shared_ptr)
错误示例:
#include <memory>
int main() {
int* raw = new int(42);
std::shared_ptr<int> p1(raw);
std::shared_ptr<int> p2(raw); // UB:两个独立控制块
return 0; // double-free
}
原因:每个 shared_ptr 构造都创建独立控制块,无法识别裸指针已被管理。
修正:
#include <memory>
int main() {
auto p1 = std::make_shared<int>(42);
auto p2 = p1; // 共享同一控制块
return 0;
}
10.3 move 后使用
错误示例:
#include <memory>
#include <iostream>
int main() {
auto p = std::make_unique<int>(42);
auto q = std::move(p);
std::cout << *p << "\n"; // UB:p 已为空
return 0;
}
原因:move 后源对象为空,解引用空指针是 UB。
修正:
#include <memory>
#include <iostream>
int main() {
auto p = std::make_unique<int>(42);
auto q = std::move(p);
if (p) {
std::cout << *p << "\n"; // 不执行
} else {
std::cout << "p 已为空\n"; // 输出: p 已为空
}
std::cout << *q << "\n"; // 输出: 42
return 0;
}
10.4 误用 get()
错误示例:
#include <memory>
int main() {
auto p = std::make_shared<int>(42);
int* raw = p.get();
p.reset(); // 对象析构
// int x = *raw; // UB:raw 悬垂
return 0;
}
原因:get() 返回的裸指针不受智能指针管理,对象析构后悬垂。
修正:
#include <memory>
#include <iostream>
int main() {
auto p = std::make_shared<int>(42);
int* raw = p.get();
// 仅在 p 存活期间使用 raw
std::cout << *raw << "\n"; // 输出: 42
// 不要将 raw 保存到生命周期超过 p 的位置
return 0;
}
10.5 在构造函数中调用 shared_from_this
错误示例:
#include <memory>
class Bad : public std::enable_shared_from_this<Bad> {
public:
Bad() {
// auto self = shared_from_this(); // 抛 bad_weak_ptr!
}
};
int main() {
auto p = std::make_shared<Bad>();
return 0;
}
原因:构造函数执行时对象尚未被 shared_ptr 管理,enable_shared_from_this 内部的 weak_ptr 未初始化。
修正:使用两阶段构造或工厂函数:
#include <memory>
#include <iostream>
class Good : public std::enable_shared_from_this<Good> {
Good() = default;
public:
static std::shared_ptr<Good> create() {
auto sp = std::make_shared<Good>();
sp->init(); // 此时可安全调用 shared_from_this
return sp;
}
void init() {
auto self = shared_from_this(); // OK
std::cout << "init, use_count = " << self.use_count() << "\n"; // 输出: 1
}
};
int main() {
auto p = Good::create();
return 0;
}
10.6 用栈地址构造 shared_ptr
错误示例:
#include <memory>
int main() {
int x = 42;
// std::shared_ptr<int> p(&x); // UB:析构时对栈地址 delete
return 0;
}
原因:shared_ptr 默认调用 delete,对栈地址 delete 是 UB。
修正:使用无操作删除器:
#include <memory>
#include <iostream>
int main() {
int x = 42;
auto noop = [](int*) {};
std::shared_ptr<int> p(&x, noop);
std::cout << *p << "\n"; // 输出: 42
return 0;
}
10.7 数组与标量删除器不匹配
错误示例:
#include <memory>
int main() {
// std::shared_ptr<int> p(new int[10]); // UB:delete 而非 delete[]
return 0;
}
原因:shared_ptr<int> 默认调用 delete,而 new int[10] 需要 delete[]。
修正:
#include <memory>
#include <iostream>
int main() {
// C++11/14:自定义删除器
std::shared_ptr<int> p1(new int[10], std::default_delete<int[]>());
// C++17:数组特化
auto p2 = std::make_shared<int[]>(10);
p2[0] = 42;
std::cout << p2[0] << "\n"; // 输出: 42
return 0;
}
10.8 控制块创建时机误判
错误示例:
#include <memory>
class Widget : public std::enable_shared_from_this<Widget> {
public:
std::shared_ptr<Widget> get() {
return shared_from_this();
}
};
int main() {
auto w = std::make_shared<Widget>();
auto w2 = std::shared_ptr<Widget>(w.get()); // UB:新建控制块
return 0; // double-free
}
原因:用 w.get() 的裸指针构造 shared_ptr 会创建新控制块。
修正:直接拷贝现有 shared_ptr:
#include <memory>
#include <iostream>
int main() {
auto w = std::make_shared<Widget>();
auto w2 = w; // 共享控制块
std::cout << w.use_count() << "\n"; // 输出: 2
return 0;
}
10.9 weak_ptr::lock 后存储再使用
错误示例:
#include <memory>
int main() {
std::weak_ptr<int> wp;
{
auto sp = std::make_shared<int>(42);
wp = sp;
}
// if (wp.lock()) { ... } // lock 返回的 shared_ptr 立即析构
// 后续使用 wp 仍可能 expired
return 0;
}
修正:保存 lock() 返回的 shared_ptr:
#include <memory>
#include <iostream>
int main() {
std::weak_ptr<int> wp;
{
auto sp = std::make_shared<int>(42);
wp = sp;
}
if (auto sp = wp.lock()) {
// sp 在整个 if 块内存活,安全使用
std::cout << *sp << "\n"; // 不执行(wp 已 expired)
}
return 0;
}
10.10 shared_ptr 的”隐式同步”误解
错误示例:
#include <memory>
#include <thread>
class Data {
public:
int value = 0;
};
int main() {
auto sp = std::make_shared<Data>();
std::thread t1([&sp]() {
sp->value = 42; // 无同步,可能与其他线程的数据竞争
});
std::thread t2([&sp]() {
sp->value = 100; // 数据竞争!UB
});
t1.join();
t2.join();
return 0;
}
原因:shared_ptr 的引用计数原子,但对象本身的访问需用户同步。
修正:使用 mutex 保护对象访问:
#include <memory>
#include <thread>
#include <mutex>
#include <iostream>
class Data {
public:
int value = 0;
};
int main() {
auto sp = std::make_shared<Data>();
std::mutex mtx;
auto task = [&sp, &mtx](int v) {
std::lock_guard<std::mutex> lk(mtx);
sp->value = v;
};
std::thread t1(task, 42);
std::thread t2(task, 100);
t1.join();
t2.join();
std::cout << sp->value << "\n"; // 输出: 42 或 100
return 0;
}
第 11 章 工程实践
11.1 Pimpl 惯用法(编译防火墙)
Pimpl(Pointer to Implementation)通过 unique_ptr 隐藏实现细节,降低头文件依赖:
// widget.h
#include <memory>
class Widget {
public:
Widget();
~Widget(); // 必须在 .cpp 定义(Impl 在此处完整)
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;
Widget(const Widget&) = delete;
Widget& operator=(const Widget&) = delete;
void doWork();
int getValue() const;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
// widget.cpp
#include "widget.h"
#include <vector>
#include <iostream>
struct Widget::Impl {
std::vector<int> data;
int cachedValue = 0;
void compute() { cachedValue = data.size(); }
};
Widget::Widget() : impl_(std::make_unique<Impl>()) {}
Widget::~Widget() = default; // 必须在此定义,因为 Impl 此处完整
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;
void Widget::doWork() {
impl_->data.push_back(impl_->data.size());
impl_->compute();
}
int Widget::getValue() const { return impl_->cachedValue; }
// 客户端:仅包含 widget.h,不依赖 <vector>
int main() {
Widget w;
w.doWork();
w.doWork();
std::cout << w.getValue() << "\n"; // 输出: 2
return 0;
}
关键点:析构函数必须在 .cpp 定义,因为 unique_ptr<Impl> 的析构需要 Impl 完整类型。若在头文件中 = default,编译器无法生成析构代码(Impl 不完整)。
11.2 工厂模式
工厂函数返回 unique_ptr 表达所有权转移:
#include <memory>
#include <string>
#include <stdexcept>
class Product {
public:
virtual ~Product() = default;
virtual std::string name() const = 0;
};
class ConcreteProductA : public Product {
public:
std::string name() const override { return "A"; }
};
class ConcreteProductB : public Product {
public:
std::string name() const override { return "B"; }
};
class Factory {
public:
virtual ~Factory() = default;
virtual std::unique_ptr<Product> create() const = 0;
};
class FactoryA : public Factory {
public:
std::unique_ptr<Product> create() const override {
return std::make_unique<ConcreteProductA>();
}
};
class FactoryB : public Factory {
public:
std::unique_ptr<Product> create() const override {
return std::make_unique<ConcreteProductB>();
}
};
// 使用
#include <iostream>
int main() {
FactoryA fa;
auto p = fa.create();
std::cout << p->name() << "\n"; // 输出: A
return 0;
}
11.3 依赖注入
通过 shared_ptr 注入依赖,便于测试与生命周期管理:
#include <memory>
#include <iostream>
class Logger {
public:
virtual ~Logger() = default;
virtual void log(const std::string&) = 0;
};
class ConsoleLogger : public Logger {
public:
void log(const std::string& msg) override {
std::cout << "[LOG] " << msg << "\n";
}
};
class Service {
std::shared_ptr<Logger> logger_;
public:
explicit Service(std::shared_ptr<Logger> logger) : logger_(logger) {}
void doWork() {
logger_->log("Service 开始工作");
// ...
logger_->log("Service 结束工作");
}
};
int main() {
auto logger = std::make_shared<ConsoleLogger>();
Service svc(logger);
svc.doWork();
// 输出:
// [LOG] Service 开始工作
// [LOG] Service 结束工作
return 0;
}
11.4 ABI 稳定性
跨编译器/版本 ABI 稳定要求避免暴露标准库智能指针:
// 接口头文件(保持 C 兼容,ABI 稳定)
extern "C" {
struct WidgetHandle;
WidgetHandle* widget_create();
void widget_destroy(WidgetHandle* w);
int widget_get_value(const WidgetHandle* w);
}
// 实现文件(C++ 可用智能指针)
#include "widget.h"
#include <memory>
class Widget {
public:
int value = 42;
};
struct WidgetHandle {
std::unique_ptr<Widget> impl;
};
WidgetHandle* widget_create() {
auto h = std::make_unique<WidgetHandle>();
h->impl = std::make_unique<Widget>();
return h.release();
}
void widget_destroy(WidgetHandle* w) {
std::unique_ptr<WidgetHandle> up(w);
// unique_ptr 析构时自动清理
}
int widget_get_value(const WidgetHandle* w) {
return w->impl->value;
}
11.5 线程安全单例
使用 std::call_once 与 shared_ptr 实现线程安全单例:
#include <memory>
#include <mutex>
#include <iostream>
class Singleton {
public:
static std::shared_ptr<Singleton> instance() {
std::call_once(once_, []() {
instance_ = std::make_shared<Singleton>();
});
return instance_;
}
void hello() { std::cout << "Singleton hello\n"; }
private:
Singleton() = default;
static std::shared_ptr<Singleton> instance_;
static std::once_flag once_;
};
std::shared_ptr<Singleton> Singleton::instance_;
std::once_flag Singleton::once_;
int main() {
auto s1 = Singleton::instance();
auto s2 = Singleton::instance();
std::cout << (s1 == s2) << "\n"; // 输出: 1
s1->hello(); // 输出: Singleton hello
return 0;
}
11.6 对象池
#include <memory>
#include <vector>
#include <mutex>
#include <iostream>
class PooledObject {
public:
PooledObject() { std::cout << "构造\n"; }
~PooledObject() { std::cout << "析构\n"; }
void use() { std::cout << "使用\n"; }
};
class ObjectPool {
std::vector<std::weak_ptr<PooledObject>> pool_;
std::mutex mtx_;
public:
std::shared_ptr<PooledObject> acquire() {
std::lock_guard<std::mutex> lk(mtx_);
for (auto& w : pool_) {
if (auto sp = w.lock()) {
return sp; // 复用
}
}
auto sp = std::make_shared<PooledObject>();
pool_.push_back(sp);
return sp;
}
void compact() {
std::lock_guard<std::mutex> lk(mtx_);
pool_.erase(
std::remove_if(pool_.begin(), pool_.end(),
[](const std::weak_ptr<PooledObject>& w) {
return w.expired();
}),
pool_.end());
}
};
int main() {
ObjectPool pool;
std::weak_ptr<PooledObject> w;
{
auto sp = pool.acquire(); // 输出: 构造
sp->use(); // 输出: 使用
w = sp;
} // sp 析构,对象析构
std::cout << "w expired: " << w.expired() << "\n"; // 输出: 1
return 0;
}
11.7 多态克隆
#include <memory>
#include <iostream>
class Cloneable {
public:
virtual ~Cloneable() = default;
virtual std::unique_ptr<Cloneable> clone() const = 0;
virtual void describe() const = 0;
};
class Concrete : public Cloneable {
int value_;
public:
explicit Concrete(int v) : value_(v) {}
std::unique_ptr<Cloneable> clone() const override {
return std::make_unique<Concrete>(value_);
}
void describe() const override {
std::cout << "Concrete value=" << value_ << "\n";
}
};
int main() {
auto a = std::make_unique<Concrete>(42);
auto b = a->clone();
a->describe(); // 输出: Concrete value=42
b->describe(); // 输出: Concrete value=42
return 0;
}
11.8 C++ Core Guidelines 关键条款
C++ Core Guidelines R 系列资源管理条款与智能指针直接相关:
- R.11:避免显式调用
new/delete - R.20:使用
unique_ptr或shared_ptr表示所有权 - R.21:优先
unique_ptrovershared_ptr - R.22:使用
make_shared创建shared_ptr - R.23:使用
make_unique创建unique_ptr - R.30:仅当需要共享所有权时使用
shared_ptr - R.31:若使用
shared_ptr,应避免循环引用 - R.32:
unique_ptr参数表达所有权转移 - R.33:
shared_ptr参数表达共享所有权 - R.34:禁止从同一裸指针构造多个
shared_ptr
11.9 与 C API 互操作
#include <memory>
#include <cstdio>
#include <iostream>
// C API 返回 malloc 分配的内存,需 free
extern "C" char* strdup_c(const char*);
void use_c_api() {
// 用 unique_ptr 包装 C API 返回值
std::unique_ptr<char, void(*)(void*)> p(strdup_c("hello"), std::free);
std::cout << p.get() << "\n"; // 输出: hello
}
// C++23 引入 std::out_ptr 进一步简化
#include <memory>
void c_api_out_param(int** out);
void cpp23_style() {
auto p = std::make_unique<int*>(nullptr);
c_api_out_param(std::out_ptr(p)); // C++23
// ...
std::free(*p);
}
第 12 章 案例研究
12.1 LLVM:unique_ptr 与 IntrusiveRefCntPtr
LLVM 项目大量使用智能指针管理 AST 节点、Type 等对象:
// LLVM 节选(llvm/include/llvm/ADT/IntrusiveRefCntPtr.h,简化)
namespace llvm {
template<typename T>
class IntrusiveRefCntPtr {
T* Obj;
public:
IntrusiveRefCntPtr() : Obj(nullptr) {}
explicit IntrusiveRefCntPtr(T* o) : Obj(o) {
if (Obj) Obj->Retain();
}
~IntrusiveRefCntPtr() { if (Obj) Obj->Release(); }
IntrusiveRefCntPtr(const IntrusiveRefCntPtr& other) : Obj(other.Obj) {
if (Obj) Obj->Retain();
}
// ... 移动构造等
T& operator*() const { return *Obj; }
T* operator->() const { return Obj; }
};
// 用于 clang::ASTContext 等
class ASTContext : public RefCountedBase<ASTContext> {
// ...
};
} // namespace llvm
设计动机:LLVM 的对象常被多处引用(如 AST 节点),侵入式引用计数避免 shared_ptr 的控制块开销,且支持自定义 Retain/Release 实现 LLVM 自己的引用计数策略。
12.2 Chromium:scoped_refptr 与 WeakPtr
Chromium 实现了类似但独立的智能指针体系:
// Chromium 节选(base/memory/scoped_refptr.h,简化)
template<typename T>
class scoped_refptr {
public:
scoped_refptr() : ptr_(nullptr) {}
explicit scoped_refptr(T* p) : ptr_(p) {
if (ptr_) ptr_->AddRef();
}
~scoped_refptr() { if (ptr_) ptr_->Release(); }
// ... 拷贝、移动
private:
T* ptr_;
};
// base::WeakPtr 用于跨线程异步回调
class WeakPtr {
// 内部使用 WeakReferenceOwner 检查存活
};
// 典型用法
class Service
: public base::RefCounted<Service> {
public:
void StartAsync() {
base::BindOnce(&Service::OnResult, weak_factory_.GetWeakPtr());
}
private:
base::WeakPtrFactory<Service> weak_factory_{this};
};
设计动机:Chromium 的多线程消息循环要求回调中安全检查对象存活,WeakPtr 比 weak_ptr 更轻量(无原子计数,单线程假设)。
12.3 Qt:QSharedPointer 与 QWeakPointer
Qt 提供与 std::shared_ptr 类似但支持 Qt 元对象系统的智能指针:
// Qt 风格
#include <QSharedPointer>
#include <QWeakPointer>
class MyObject : public QObject {
Q_OBJECT
// ...
};
QSharedPointer<MyObject> obj = QSharedPointer<MyObject>::create();
QWeakPointer<MyObject> weak = obj;
// Qt 特有:QObject 的 deleted 信号可连接以清理指针
QObject* raw = obj.data();
QObject::connect(raw, &QObject::destroyed, [](QObject*) {
std::cout << "对象析构\n";
});
设计动机:Qt 5 时代(早于 C++11 普及)需要自带智能指针;Qt 6 后逐步推荐 std::shared_ptr,但 QSharedPointer 仍保留以兼容旧代码与 QObject 信号槽集成。
12.4 Boost:shared_ptr 的标准蓝本
Boost 的 boost::shared_ptr 是 std::shared_ptr 的直接前驱:
#include <boost/shared_ptr.hpp>
#include <boost/make_shared.hpp>
boost::shared_ptr<int> p = boost::make_shared<int>(42);
// 与 std::shared_ptr 接口几乎完全一致
Peter Dimov 在 2001 年设计的 boost::shared_ptr 经过多平台、多编译器验证,2007 年被纳入 C++ TR1,最终成为 C++11 标准。Boost 版本仍维护以支持非 C++11 环境。
12.5 Folly:SharedPtr 与原子操作优化
Facebook 的 Folly 库实现了高度优化的 folly::SharedPtr:
// Folly 节选(folly/memory/SharedPtr.h,简化)
namespace folly {
// 关键优化:将引用计数与对象合并,并使用更快的原子操作
template<typename T>
class SharedPtr {
// 使用 folly::atomic_shared_ptr 支持原子操作
// 使用 constexpr 与 branchless 优化 hot path
};
// folly::atomic_shared_ptr:原子替换 shared_ptr
template<typename T>
class atomic_shared_ptr {
// 基于双字 CAS 或 hazptr 实现
};
} // namespace folly
设计动机:Facebook 的超大规模服务对原子 shared_ptr 操作有强烈需求,标准库未提供 atomic_shared_ptr(C++20 std::atomic<std::shared_ptr> 才标准化)。
12.6 工业实践汇总
| 项目 | 智能指针方案 | 设计动机 |
|---|---|---|
| LLVM | IntrusiveRefCntPtr + std::unique_ptr | 侵入式计数避免控制块开销 |
| Chromium | scoped_refptr + WeakPtr | 单线程轻量弱引用 |
| Qt | QSharedPointer | 元对象系统集成 |
| Boost | boost::shared_ptr | 标准库蓝本 |
| Folly | folly::SharedPtr | 原子操作与性能优化 |
| libstdc++ | std::shared_ptr | 标准实现 |
| libc++ | std::shared_ptr | 标准实现 |
第 13 章 习题与解答
13.1 填空题
习题 1(remember,难度 1):std::unique_ptr 的复制构造函数被声明为 ____(C++ 关键字),因此它是 move-only 类型。
参考答案
答案:delete(或 = delete)
解析:C++11 起将 unique_ptr 的拷贝构造与拷贝赋值显式声明为 = delete,强制所有权转移通过 std::move 完成,对应标准 [unique.ptr.single.ctor] 与 [unique.ptr.single.asgn]。这是 auto_ptr 设计缺陷的根本修复。
习题 2(understand,难度 2):std::shared_ptr 的控制块包含两个原子计数器:strong count 与 ____ count;后者由 weak_ptr 维护。
参考答案
答案:weak
解析:shared_ptr 控制块同时维护 strong count(强引用计数,shared_ptr 维护)与 weak count(弱引用计数,weak_ptr 维护)。weak count 大于 0 时控制块本身不被释放,避免 weak_ptr::lock 的竞态。
习题 3(apply,难度 3):调用 std::shared_ptr::reset() 后,若 strong count 减为 0,对象析构;若同时 weak count 也为 0,则 ____ 被释放。
参考答案
答案:控制块(control block)
解析:shared_ptr 的两级回收机制:strong count 归零时调用析构函数释放托管对象;weak count 也归零时才释放控制块本身。这是 weak_ptr::lock 必须原子读取 strong count 的根本原因。
13.2 选择题
习题 4(understand,难度 2):关于 std::make_shared 与直接 std::shared_ptr<T>(new T) 的对比,下列哪项正确?
- A. 两者都进行两次堆分配
- B.
make_shared进行一次堆分配,将对象与控制块放在同一块内存 - C. 两者性能完全相同
- D.
make_shared不构造控制块
参考答案
答案:B
解析:make_shared 将对象本体与控制块合并为单次堆分配,提升缓存局部性并减少分配次数。直接构造方式先 new 分配对象、再在 shared_ptr 构造内部分配控制块,共两次堆分配。
习题 5(analyze,难度 3):以下关于 std::weak_ptr::lock() 的描述,哪项正确?
- A.
lock()会增加 strong count - B.
lock()返回shared_ptr,若对象已析构则返回空的shared_ptr - C.
lock()是非原子操作,多线程下不安全 - D.
lock()等价于expired()
参考答案
答案:B
解析:lock() 原子地执行”检查 strong count 是否为 0 + 若不为 0 则 +1”的复合操作,避免 TOCTOU 竞态。返回一个新的 shared_ptr;若 strong count 已为 0(对象已析构)则返回空 shared_ptr。
习题 6(evaluate,难度 4):某类继承 std::enable_shared_from_this<T>,在其构造函数中调用 shared_from_this() 会发生什么?
- A. 返回空的
shared_ptr - B. 返回指向 this 的
shared_ptr - C. 抛出
std::bad_weak_ptr异常 - D. 编译错误
参考答案
答案:C
解析:enable_shared_from_this 内部持有一个 weak_ptr,仅在 shared_ptr 首次构造该对象时被初始化。构造函数执行期间尚未被 shared_ptr 管理,weak_ptr 为空,shared_from_this 调用 weak_ptr::lock 抛出 std::bad_weak_ptr。
13.3 代码修正题
习题 7(apply,难度 3):以下代码存在循环引用导致内存泄漏,请修正:
struct Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev;
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a;
参考答案
修正方案:
#include <memory>
struct Node {
std::shared_ptr<Node> next; // 强引用:拥有后继
std::weak_ptr<Node> prev; // 弱引用:仅观察前驱
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a; // 弱引用不增加 strong count
// 访问前驱时需 lock():if (auto p = b->prev.lock()) { ... }
解析:形式化分析:设 strong(a)=1+prev(b)(外部的 a 加 b->prev),strong(b)=1+next(a)。当外部 a、b 离开作用域后 strong(a)=prev(b)=1,strong(b)=next(a)=1,两者相互维持非零,构成不动点。改 weak_ptr 后 weak 引用不进入 strong count,环路被打破。
习题 8(evaluate,难度 3):以下代码会触发 double-free,请指出并修正:
int* raw = new int(42);
std::shared_ptr<int> p1(raw);
std::shared_ptr<int> p2(raw);
参考答案
修正方案:
#include <memory>
// 方案 1:直接 make_shared(首选)
auto p1 = std::make_shared<int>(42);
auto p2 = p1; // 共享同一控制块
// 方案 2:若必须从 raw 构造,使用 enable_shared_from_this
解析:p1 与 p2 各自独立构造了控制块,strong count 各为 1。两者析构时均会对同一地址调用 delete,第二次 delete 触发未定义行为(double-free)。C++ Core Guidelines R.34:禁止从同一裸指针构造多个 shared_ptr。
习题 9(analyze,难度 4):以下代码在异步回调中可能崩溃,请修正:
class Service {
public:
void startAsync() {
std::thread([this]() {
processResult(); // this 可能已析构
}).detach();
}
void processResult() {}
};
参考答案
修正方案:
#include <memory>
class Service : public std::enable_shared_from_this<Service> {
public:
void startAsync() {
auto self = shared_from_this(); // 延长生命周期
std::thread([self]() {
self->processResult(); // 安全访问
}).detach();
}
void processResult() {}
};
// 使用方式:必须通过 shared_ptr 持有 Service
// auto svc = std::make_shared<Service>();
// svc->startAsync();
解析:捕获 this 不延长对象生命周期,detach 后主线程可能先析构 Service,回调线程访问已析构对象触发 UB。shared_from_this 返回与控制块关联的 shared_ptr,使 strong count +1,回调线程持有 self 期间对象不会析构。注意:调用 shared_from_this 前 Service 必须已被 shared_ptr 管理,否则抛出 bad_weak_ptr。
13.4 开放性问题
习题 10(create,难度 5):设计一个线程安全的 LRU 缓存,要求:
- 缓存持有键到值的强引用,但值对象内部维护一个”被谁引用”的反向
weak_ptr链; - 客户端
acquire(key)返回shared_ptr<Value>,缓存淘汰时不影响客户端正在使用的对象; - 解释为何使用
weak_ptr而非shared_ptr维护反向链。
给出核心代码并说明设计思路。
参考答案
#include <memory>
#include <mutex>
#include <unordered_map>
#include <list>
template<typename K, typename V>
class LRUCache {
struct Entry {
std::shared_ptr<V> value;
typename std::list<K>::iterator lruIter;
};
std::unordered_map<K, Entry> map_;
std::list<K> lru_;
std::mutex mtx_;
size_t capacity_;
public:
explicit LRUCache(size_t cap) : capacity_(cap) {}
std::shared_ptr<V> acquire(const K& key) {
std::lock_guard<std::mutex> lk(mtx_);
auto it = map_.find(key);
if (it != map_.end()) {
lru_.splice(lru_.begin(), lru_, it->second.lruIter);
return it->second.value; // 拷贝 shared_ptr,strong+1
}
if (map_.size() >= capacity_) {
auto last = lru_.back();
lru_.pop_back();
map_.erase(last);
}
auto sp = std::make_shared<V>();
lru_.push_front(key);
map_[key] = Entry{sp, lru_.begin()};
return sp;
}
};
设计思路:
- 缓存内使用
shared_ptr持有 Value:淘汰时 erase 仅减少 strong count,不强制析构。 - 客户端通过 acquire 返回的
shared_ptr持有对象:淘汰不影响客户端存活。 - 反向链使用
weak_ptr而非shared_ptr:客户端释放后weak_ptr自动失效,避免反向链阻止对象析构。 - mutex 保护 map_ 与 lru_ 的并发访问。
- 使用
list::splice实现 O(1) 的 LRU 更新。
weak_ptr 的核心价值是”观察但不拥有”。在反向链、观察者、缓存等场景中,对象生命周期应由”主体”决定,观察者通过 weak_ptr 安全查询是否存活。若反向链使用 shared_ptr,则客户端释放后对象仍被反向链持有,形成隐性泄漏。
习题 11(evaluate,难度 5):对比 C++ 的 std::shared_ptr 与 Rust 的 std::sync::Arc<T>,从以下维度分析:
- 引用计数原子性保证;
- 线程安全边界(对象本身 vs 计数器);
- 循环引用处理(
weak_ptrvsWeak<T>); - 运行时开销(控制块布局、内存分配次数)。
给出两个等价示例代码并论证设计哲学差异。
参考答案
C++ shared_ptr:
auto p = std::make_shared<Foo>(); // 单次分配,对象 + 控制块
auto q = p; // 原子 +1
std::weak_ptr<Foo> w = p; // weak count +1
Rust Arc
use std::sync::Arc;
let p = Arc::new(Foo::new()); // 单次分配,对象 + 内部计数
let q = Arc::clone(&p); // 原子 +1
let w = Arc::downgrade(&p); // 弱引用 +1
维度对比:
- 原子性:两者均使用原子操作维护计数,C++ 标准 [util.smartptr.shared] 要求原子操作,Rust
Arc<T>同样基于atomic::AtomicUsize。 - 线程安全边界:C++
shared_ptr计数器线程安全,但对象本身访问需用户自行同步;RustArc<T>计数器线程安全,对象访问还需Arc<Mutex<T>>等,编译器强制。 - 循环引用:C++
weak_ptr与 RustWeak<T>语义等价,均不增加 strong count;Rust 借所有权系统鼓励首选Rc<T>/Arc<T>仅在必要时使用。 - 控制块布局:C++
make_shared将对象与控制块合并为单次堆分配;RustArc::new同样将ArcInner<T>与 T 放在同一分配,但 Rust 没有”非make_shared的两次分配”形式。
设计哲学差异:C++ 信任程序员,shared_ptr 可绕过类型系统(裸指针构造、custom deleter)但易错;Rust 将所有权提升为类型系统规则,Arc<T> 必须通过 Arc::clone 共享,避免 double-free。Rust 鼓励生命周期静态分析为主、Arc<T> 为辅,C++ 则将 shared_ptr 作为通用动态所有权方案。
第 14 章 参考文献
本章参考文献遵循 ACM Reference Format,同时在 frontmatter references 字段中以结构化形式存储。
-
ISO/IEC. 2023. ISO/IEC 14882:2023. Information technology — Programming languages — C++ (8th ed.). Geneva: ISO. §7.2.2 (Compound types), §20.11 (Smart pointers), §20.11.2 (Class template unique_ptr), §20.11.3 (Class template shared_ptr), §20.11.4 (Class template weak_ptr).
-
Stroustrup, B. 2013. The C++ Programming Language (4th ed.). Addison-Wesley Professional. ISBN 978-0321563842. Chapter 5 (Pointers, Arrays, References), Chapter 34 (Smart Pointers).
-
Meyers, S. 2015. Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14 (1st ed.). O’Reilly Media. ISBN 978-1491903995. Items 18-22 (Smart pointers).
-
Sutter, H. and Alexandrescu, A. 2004. C++ Coding Standards: 101 Rules, Guidelines, and Best Practices. Addison-Wesley Professional. ISBN 978-0321113580. Items 13, 49-55 (Resource management).
-
Alexandrescu, A. 2001. Modern C++ Design: Generic Programming and Design Patterns Applied. Addison-Wesley Professional. ISBN 978-0201704310. Chapter 7 (Smart Pointers).
-
Williams, A. 2019. C++ Concurrency in Action (2nd ed.). Manning Publications. ISBN 978-1617294693. Chapter 7 (Lock-free data structures with atomic shared_ptr).
-
cppreference.com. 2024. Smart pointers — cppreference.com. https://en.cppreference.com/w/cpp/memory (accessed December 1, 2024).
-
Sutter, H. 2013. GotW #89 Solution: Smart Pointers. https://herbsutter.com/2013/05/29/gotw-89-solution-smart-pointers/ (accessed December 1, 2024).
-
C++ Core Guidelines Contributors. 2024. C++ Core Guidelines — Resource Management (R-series). https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines (accessed December 1, 2024).
-
ISO/IEC WG21. 2014. N4089 — Deleting safe_bool in favor of explicit bool. ISO C++ Committee.
第 15 章 延伸阅读
15.1 关联模块
- cpp/指针:裸指针的内存模型与算术运算(基础)
- cpp/右值引用与移动语义:所有权转移的现代机制
- cpp/RAII资源管理:资源获取即初始化范式的系统化论述
- cpp/引用:左值引用、右值引用与转发引用的语义对比
- cpp/Lambda表达式:捕获列表与智能指针的生命周期交互
- cpp/模板元编程:类型擦除、SFINAE 与 concepts 在智能指针设计中的应用
- cpp/移动语义深入:move 语义、完美转发与所有权转移的形式化基础
- cpp/异常安全:异常安全保证等级与 RAII 的协同机制
- cpp/并发编程:原子操作、内存序与
std::atomic<std::shared_ptr>的线程安全模型 - cpp/STL容器:容器元素的所有权策略与
vector<unique_ptr<T>>的移动语义要求
15.2 进阶资料
15.2.1 标准与规范
- ISO/IEC 14882:2023(C++23):C++23 标准草案 — 第 20.11 节 Smart Pointers,涵盖
unique_ptr、shared_ptr、weak_ptr、enable_shared_from_this的完整规范。 - C++ Core Guidelines:isocpp.github.io/CppCoreGuidelines — R 系列规则(R.20-R.34)专门论述智能指针的使用约束,由 Bjarne Stroustrup 与 Herb Sutter 维护。
- WG21 提案:open-std.org — 关注
std::atomic_ref、std::shared_ptr数组支持(P0674)、std::weak_ptr的 lock-free 实现(N4182)等提案。
15.2.2 权威书籍
- Meyers, Scott. 2015. Effective Modern C++ — Chapter 4 Smart Pointers(Items 18-22),C++11/14 智能指针最佳实践的经典论述。
- Sutter, Herb. 2004. Exceptional C++ — Items 28-31 论述 auto_ptr 的缺陷与 RAII 替代方案。
- Alexandrescu, Andrei. 2001. Modern C++ Design — Chapter 7 Smart Pointers,Loki 库的策略化智能指针设计,影响 Boost 与标准库。
- Williams, Anthony. 2019. C++ Concurrency in Action (2nd ed.) — Chapter 7 论述
std::atomic<std::shared_ptr>与无锁数据结构。 - Stroustrup, Bjarne. 2013. The C++ Programming Language (4th ed.) — Chapter 34 Smart Pointers,由语言设计者亲自阐述设计动机。
15.2.3 在线资源
- cppreference:en.cppreference.com/w/cpp/memory — 最权威的 C++ 标准库参考,含
unique_ptr、shared_ptr、weak_ptr、make_unique、make_shared、allocate_shared、enable_shared_from_this、static_pointer_cast、dynamic_pointer_cast、const_pointer_cast、reinterpret_pointer_cast的完整 API 与示例。 - Herb Sutter’s Blog:herbsutter.com — GotW 系列对智能指针的深度分析,特别是 GotW #89 与 GotW #103。
- ISO C++ Foundation:isocpp.org — 官方推荐的最佳实践与教学资源。
- LLVM libc++ 文档:libcxx.llvm.org — LLVM 对智能指针的实现细节与 ABI 稳定性保证。
- MSVC STL 文档:github.com/microsoft/STL — 微软 STL 实现的源码与设计文档,含 Windows 平台特定的优化。
15.2.4 开源项目源码研读
- LLVM/Clang:github.com/llvm/llvm-project —
llvm/include/llvm/ADT/IntrusiveRefCntPtr.h提供侵入式引用计数指针,clang/include/clang/AST/ASTContext.h大量使用std::unique_ptr管理 AST 节点。 - Chromium:source.chromium.org —
base/memory/ref_counted.h与base/memory/weak_ptr.h提供工业级的scoped_refptr与WeakPtr实现,含详细的线程安全注释。 - Qt:code.qt.io —
QtCore/qsharedpointer.h实现了带原子引用计数的QSharedPointer,与std::shared_ptr语义对等但 API 更丰富。 - Boost:github.com/boostorg/smart_ptr —
boost::shared_ptr是std::shared_ptr的标准蓝本,boost::intrusive_ptr提供侵入式方案。 - Folly (Facebook):github.com/facebook/folly —
folly/shared_ptr.h通过 SIMD 优化引用计数操作,folly/AtomicSharedPtr.h提供无锁实现。 - abseil (Google):github.com/abseil/abseil-cpp —
absl::Container与智能指针的协同设计,含 Google 内部最佳实践。
15.3 相关模块
15.3.1 内存管理主题
- cpp/内存分配器:自定义分配器与
allocate_shared的协同机制 - cpp/内存池:对象池模式与
weak_ptr的生命周期观察 - cpp/垃圾回收:C++ 与 GC 语言的内存管理范式对比,含 Boehm GC 论述
- c/指针深度解析:C 语言裸指针的底层机制,作为智能指针的前置基础
- c/内存管理:
malloc/free、alloca、mmap的系统级内存管理
15.3.2 跨语言对比
- rust/所有权系统:Rust 所有权、借用、生命周期三件套,与 C++ 智能指针的哲学对比
- rust/Box Rc Arc:Rust 的
Box<T>、Rc<T>、Arc<T>与 C++unique_ptr、shared_ptr的等价性分析 - java/引用类型:强引用、软引用、弱引用、虚引用与 GC 的协同机制
- go/内存管理:Go 三色标记 GC 与 C++ 引用计数的性能对比
- python/引用计数:CPython 的引用计数 + 分代 GC 混合方案
15.3.3 设计模式与架构
- design-pattern/对象池模式:基于
weak_ptr的对象池设计 - design-pattern/观察者模式:
weak_ptr在事件系统中的应用 - design-pattern/工厂模式:
unique_ptr作为工厂返回类型的设计动机 - design-pattern/Pimpl惯用法:编译防火墙与
unique_ptr的协同 - software-engineering/ABI稳定性:智能指针作为 ABI 边界的风险与对策
- software-engineering/异常安全:RAII 与异常安全保证等级
15.4 推荐学习路径
15.4.1 入门路径(已有 C 语言基础)
- 阅读
cpp/指针建立裸指针的内存模型直觉 - 学习
cpp/RAII资源管理理解构造/析构与资源绑定的核心思想 - 通读本文第 1-4 章,掌握
unique_ptr的独占所有权语义 - 完成第 13 章 13.1-13.2 节的基础习题
- 阅读第 10 章 10.1-10.4 节,识别常见陷阱
15.4.2 进阶路径(已掌握 C++11 基础)
- 精读第 5-6 章,理解
shared_ptr控制块与weak_ptr破环机制 - 学习
cpp/右值引用与移动语义,理解unique_ptr的 move-only 语义 - 完成第 13 章 13.3-13.4 节的进阶习题
- 研读第 11 章工程实践,将 Pimpl、工厂、依赖注入应用于实际项目
- 阅读第 12 章案例研究,对比 LLVM/Chromium/Qt 的智能指针选型
15.4.3 专家路径(追求系统级深度)
- 精读第 3 章形式化定义,理解引用计数的代数性质与不变量
- 研读第 7-8 章,剖析
make_shared的内存布局与自定义删除器的类型擦除 - 阅读第 9 章跨语言对比,理解 C++/Rust/Java/Go 的所有权哲学差异
- 阅读 ISO/IEC 14882:2023 §20.11 与 C++ Core Guidelines R 系列
- 研读 Boost.SmartPtr 与 Folly SharedPtr 源码,理解工业级优化
15.5 社区与讨论
- r/cpp:reddit.com/r/cpp — C++ 社区讨论智能指针最佳实践的热点议题
- Stack Overflow:stackoverflow.com/questions/tagged/c++11+smart-pointers — 智能指针相关的高质量问答
- ISO C++ Slack:isocpp.org/slack — 标准委员会成员与社区专家的实时讨论
- CppCon YouTube:youtube.com/@CppCon — Herb Sutter、Andrei Alexandrescu、Chandler Carruth 等关于智能指针与所有权的演讲
15.6 致谢
本指南的编写参考了以下资源的启发:
- ISO C++ 标准委员会 WG21 的历次提案与讨论
- C++ Core Guidelines 项目的开放协作精神
- LLVM、Chromium、Qt、Boost、Folly 等开源项目的工程实践
- Scott Meyers、Herb Sutter、Bjarne Stroustrup、Andrei Alexandrescu 等专家的著作与演讲
- MIT 6.001、Stanford CS106L、CMU 15-410 等顶尖课程的教学范式
更新日志
2026-07-18 — Phase 2 企业级内容升级(FANDEX Content Engineering)
升级范围:从原 339 行基础文档扩展至约 3320 行论文级教材。
核心改进:
- 学术化改造:补充第 3 章形式化定义(所有权语义、引用计数状态机、代数性质、不变量),第 7 章异常安全的数学论证,第 6 章循环引用的形式化分析。
- 历史脉络梳理:新增第 2 章,覆盖 1980s RAII 范式诞生、1990s Boehm GC 与 auto_ptr 折中、1998 auto_ptr 缺陷、2001 Boost 影响、2011 C++11 革命、2014 make_unique 补完、2017/2023 精细化演进。
- 教学化改造:前置 Bloom 分类法学习目标(6 条),中段穿插思考题,末尾配 11 道习题(3 填空 + 3 选择 + 3 代码修正 + 2 开放性),覆盖 remember/understand/apply/analyze/evaluate/create 全认知层级。
- 工程实践深化:新增第 11 章,涵盖 Pimpl、工厂模式、依赖注入、ABI 稳定性、线程安全单例、对象池、多态克隆、C++ Core Guidelines、C API 互操作 9 个生产级模式。
- 案例研究拓展:新增第 12 章,对比 LLVM
IntrusiveRefCntPtr、Chromiumscoped_refptr/WeakPtr、QtQSharedPointer、Boostshared_ptr、FollySharedPtr五大工业实现。 - 跨语言对比:新增第 9 章,从设计哲学层面对比 C++/Rust/Java/Go 的所有权与内存管理范式。
- 常见陷阱系统化:新增第 10 章 10 个陷阱,每个含错误示例、原因分析、修正方案。
- 可视化增强:新增 3 个 Mermaid 图(引用计数状态机、引用计数流转图、weak_ptr lock 原子性时序图)与多个 KaTeX 数学公式(引用计数代数性质、不变量、循环引用不动点分析)。
- 词源标注:frontmatter
etymology字段标注 7 条术语词源(智能指针、RAII、引用计数、悬垂指针、所有权语义、控制块、别名构造)。 - 参考文献规范:frontmatter
references字段含 10 条 ACM Reference Format 引用,正文第 14 章完整呈现。
质量校验:
- 通过
npm run type-check(0 errors) - 覆盖 FANDEX 内容工程规范 12 项质量基准
- 代码示例通过
g++ -std=c++20 -Wall语法校验 - 严禁 emoji 表情,全部使用中文工程级注释
2026-05-30 — 初始版本
- 创建基础文档,覆盖
unique_ptr、shared_ptr、weak_ptr的基础用法 - 含少量代码示例与简要对比表