目录
[一、裸指针的 "血泪史":为什么我们需要智能指针?](#一、裸指针的 “血泪史”:为什么我们需要智能指针?)
[1.1 内存泄漏:最常见的 "噩梦"](#1.1 内存泄漏:最常见的 “噩梦”)
[1.2 二次释放:致命的 "双重打击"](#1.2 二次释放:致命的 “双重打击”)
[1.3 野指针:潜伏的 "幽灵"](#1.3 野指针:潜伏的 “幽灵”)
[1.4 异常安全:被忽略的 "隐形杀手"](#1.4 异常安全:被忽略的 “隐形杀手”)
[1.5 智能指针的核心使命](#1.5 智能指针的核心使命)
[二、智能指针的 "三驾马车":unique_ptr、shared_ptr、weak_ptr](#二、智能指针的 “三驾马车”:unique_ptr、shared_ptr、weak_ptr)
[2.1 unique_ptr:独占所有权的 "独行侠"](#2.1 unique_ptr:独占所有权的 “独行侠”)
[2.1.1 unique_ptr 的核心原理](#2.1.1 unique_ptr 的核心原理)
[2.1.2 unique_ptr 的基本使用](#2.1.2 unique_ptr 的基本使用)
[2.1.3 unique_ptr 的使用场景与最佳实践](#2.1.3 unique_ptr 的使用场景与最佳实践)
[2.2 shared_ptr:共享所有权的 "社交达人"](#2.2 shared_ptr:共享所有权的 “社交达人”)
[2.2.1 shared_ptr 的核心原理:引用计数](#2.2.1 shared_ptr 的核心原理:引用计数)
[2.2.2 shared_ptr 的基本使用](#2.2.2 shared_ptr 的基本使用)
[2.2.3 循环引用:shared_ptr 的 "阿喀琉斯之踵"](#2.2.3 循环引用:shared_ptr 的 “阿喀琉斯之踵”)
[2.3 weak_ptr:打破循环的 "旁观者"](#2.3 weak_ptr:打破循环的 “旁观者”)
[2.3.1 weak_ptr 的核心原理](#2.3.1 weak_ptr 的核心原理)
[2.3.2 weak_ptr 的基本使用与循环引用解决方案](#2.3.2 weak_ptr 的基本使用与循环引用解决方案)
[2.3.3 weak_ptr 的使用场景与最佳实践](#2.3.3 weak_ptr 的使用场景与最佳实践)
[三、智能指针的 "进阶技巧":定制删除器、类型转换与性能优化](#三、智能指针的 “进阶技巧”:定制删除器、类型转换与性能优化)
[3.1 定制删除器:处理特殊资源释放](#3.1 定制删除器:处理特殊资源释放)
[3.1.1 unique_ptr 的定制删除器](#3.1.1 unique_ptr 的定制删除器)
[3.1.2 shared_ptr 的定制删除器](#3.1.2 shared_ptr 的定制删除器)
[3.2 智能指针的类型转换](#3.2 智能指针的类型转换)
[3.3 智能指针的性能优化](#3.3 智能指针的性能优化)
[3.3.1 优先使用 unique_ptr](#3.3.1 优先使用 unique_ptr)
[3.3.2 使用 make_shared 减少内存分配](#3.3.2 使用 make_shared 减少内存分配)
[3.3.3 避免不必要的 shared_ptr 拷贝](#3.3.3 避免不必要的 shared_ptr 拷贝)
[3.3.4 合理使用 weak_ptr 的 lock ()](#3.3.4 合理使用 weak_ptr 的 lock ())
[四、智能指针的 "避坑指南":常见错误与最佳实践总结](#四、智能指针的 “避坑指南”:常见错误与最佳实践总结)
[4.1 常见错误](#4.1 常见错误)
[4.2 最佳实践总结](#4.2 最佳实践总结)
前言
在 C++ 的世界里,有一个让无数开发者 "谈虎色变" 的话题 ------ 内存管理。指针作为 C++ 的 "灵魂",赋予了开发者直接操作内存的强大能力,但也埋下了内存泄漏、野指针、二次释放等一系列 "定时炸弹"。你是否也曾因为一个忘记释放的
new,调试了一下午的内存泄漏?是否也曾被野指针导致的程序崩溃搞得怀疑人生?幸运的是,C++ 标准库为我们提供了一套 "自动驾驶" 级别的解决方案 ------ 智能指针。它就像给裸指针装上了 "自动刹车" 和 "自动泊车" 系统,让内存管理变得安全、高效且省心。本文将从底层原理到实际应用,全方位拆解智能指针的设计精髓,带你彻底掌握
unique_ptr、shared_ptr、weak_ptr的使用技巧,从此和内存问题说再见!下面就让我们正式开始吧!
一、裸指针的 "血泪史":为什么我们需要智能指针?
在深入智能指针之前,我们先回顾一下**裸指针(Raw Pointer)**的 "坑"。正是这些痛点,催生了智能指针的诞生。
1.1 内存泄漏:最常见的 "噩梦"
内存泄漏是指程序分配的内存空间在使用完毕后,没有被正确释放,导致这部分内存永远无法被再次使用。尤其在复杂的程序逻辑中,一个疏忽就可能造成内存泄漏。
cpp
// 反面示例:裸指针导致的内存泄漏
void func() {
int* p = new int(10); // 分配堆内存
// 业务逻辑处理...
if (some_condition) {
return; // 提前返回,忘记释放p
}
// 其他操作...
delete p; // 正常路径下的释放,但异常路径会跳过
}
int main() {
while (true) {
func(); // 循环调用,每次都会泄漏4字节内存
sleep(1);
}
return 0;
}
在上述代码中,如果some_condition为true,函数会提前返回,delete p语句就永远不会执行,导致堆内存泄漏。如果这段代码在循环中执行,内存会持续增长,最终导致程序崩溃。
1.2 二次释放:致命的 "双重打击"
二次释放是指对同一块内存进行多次delete操作。这会破坏堆内存的完整性,导致程序崩溃或未定义行为。
cpp
// 反面示例:裸指针导致的二次释放
void func() {
int* p = new int(20);
delete p; // 第一次释放
// ... 中间经过复杂的逻辑,忘记p已经被释放
delete p; // 第二次释放,程序崩溃
}
这种问题在多人协作或复杂代码中尤为常见 ------ 当一个指针被传递到多个函数后,很难追踪它是否已经被释放。
1.3 野指针:潜伏的 "幽灵"
野指针是指指向已释放内存或非法内存地址的指针。访问野指针会导致程序崩溃、数据损坏等不可预测的结果。
cpp
// 反面示例:裸指针导致的野指针问题
int* func() {
int x = 10; // 栈内存,函数返回后会被销毁
return &x; // 返回栈内存地址,形成野指针
}
int main() {
int* p = func();
cout << *p << endl; // 访问野指针,行为未定义(可能输出乱码或崩溃)
return 0;
}
栈内存的生命周期与函数作用域绑定,函数返回后栈内存会被系统回收,此时返回的指针就成了野指针。
1.4 异常安全:被忽略的 "隐形杀手"
当程序发生异常时,正常的执行流程会被打断,可能导致裸指针无法被释放。
cpp
// 反面示例:异常导致的内存泄漏
void func() {
int* p = new int(30);
try {
// 模拟抛出异常
throw runtime_error("something wrong");
} catch (...) {
// 未处理p的释放,导致内存泄漏
throw; // 重新抛出异常
}
delete p; // 永远不会执行
}
即使我们小心谨慎地处理了所有正常路径,异常也可能成为 "漏网之鱼",导致内存泄漏。
1.5 智能指针的核心使命
面对裸指针的种种问题,智能指针的核心设计思想应运而生:将指针的生命周期管理与对象的生命周期绑定,通过 RAII(资源获取即初始化)机制,实现内存的自动释放。
简单来说,智能指针是一个**"包装器类"** ,它封装了裸指针,并在其析构函数中自动执行delete操作。由于 C++ 的对象生命周期遵循 "出作用域即析构" 的规则,当智能指针对象离开作用域时,析构函数会自动调用,从而保证内存被正确释放,从根本上避免了内存泄漏、二次释放等问题。
二、智能指针的 "三驾马车":unique_ptr、shared_ptr、weak_ptr
C++11 标准库提供了三种核心智能指针:unique_ptr、shared_ptr和weak_ptr。它们各自有着不同的设计理念和适用场景,共同构成了 C++ 内存管理的 "主力军"。
2.1 unique_ptr:独占所有权的 "独行侠"
unique_ptr是最简单、最高效的智能指针,它的核心特性是独占所有权 ------ 同一时间,只能有一个unique_ptr指向一块内存。当unique_ptr对象被销毁时,它所指向的内存也会被自动释放。
2.1.1 unique_ptr 的核心原理
unique_ptr的底层实现非常简洁:
- 封装一个裸指针(T ptr*);
- 禁用拷贝构造函数和拷贝赋值运算符(C++11 中通过**= delete**实现),确保所有权无法被复制;
- 支持移动构造函数和移动赋值运算符,允许所有权的 "转移";
- 析构函数中调用delete(或delete[],针对数组类型)释放内存。
我们可以通过一个简化版的unique_ptr来理解其原理:
cpp
// 简化版unique_ptr实现(仅演示核心逻辑)
template <typename T>
class MyUniquePtr {
private:
T* ptr; // 封装的裸指针
public:
// 构造函数:接收裸指针
explicit MyUniquePtr(T* p = nullptr) : ptr(p) {}
// 析构函数:自动释放内存
~MyUniquePtr() {
delete ptr; // 核心:自动delete
ptr = nullptr;
}
// 禁用拷贝构造(独占所有权,不允许复制)
MyUniquePtr(const MyUniquePtr& other) = delete;
// 禁用拷贝赋值
MyUniquePtr& operator=(const MyUniquePtr& other) = delete;
// 移动构造:转移所有权
MyUniquePtr(MyUniquePtr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr; // 原指针置空,避免二次释放
}
// 移动赋值:转移所有权
MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
if (this != &other) {
delete ptr; // 释放当前指针指向的内存
ptr = other.ptr; // 接收对方的指针
other.ptr = nullptr; // 原指针置空
}
return *this;
}
// 重载->运算符,支持指针访问语法
T* operator->() const { return ptr; }
// 重载*运算符,支持解引用
T& operator*() const { return *ptr; }
// 获取裸指针(谨慎使用)
T* get() const { return ptr; }
// 释放所有权(返回裸指针,智能指针不再管理该内存)
T* release() {
T* temp = ptr;
ptr = nullptr;
return temp;
}
// 重置指针(释放当前内存,指向新地址)
void reset(T* p = nullptr) {
delete ptr;
ptr = p;
}
};
从简化实现可以看出,unique_ptr通过禁用拷贝、支持移动,确保了所有权的独占性,同时通过析构函数自动释放内存,实现了 "零泄漏" 的保证。
2.1.2 unique_ptr 的基本使用
unique_ptr的使用非常直观,以下是常见操作示例:
cpp
#include <memory>
#include <iostream>
using namespace std;
class Test {
public:
Test(int id) : id_(id) {
cout << "Test(" << id_ << ") 构造" << endl;
}
~Test() {
cout << "Test(" << id_ << ") 析构" << endl;
}
void show() {
cout << "Test id: " << id_ << endl;
}
private:
int id_;
};
// 1. 基本初始化与使用
void test_unique_ptr_basic() {
cout << "=== test_unique_ptr_basic ===" << endl;
// 方式1:通过make_unique创建(推荐,更安全)
unique_ptr<Test> up1 = make_unique<Test>(1);
up1->show(); // 调用成员函数
cout << "up1 get: " << up1.get() << endl; // 获取裸指针
// 方式2:通过new创建(不推荐,可能导致内存泄漏)
unique_ptr<Test> up2(new Test(2));
up2->show();
// 错误:不允许拷贝构造
// unique_ptr<Test> up3 = up1;
// 错误:不允许拷贝赋值
// unique_ptr<Test> up4;
// up4 = up1;
// 正确:移动构造(转移所有权)
unique_ptr<Test> up5 = move(up1);
up5->show();
cout << "up1 get after move: " << up1.get() << endl; // up1变为nullptr
// 正确:移动赋值(转移所有权)
unique_ptr<Test> up6;
up6 = move(up2);
up6->show();
cout << "up2 get after move: " << up2.get() << endl; // up2变为nullptr
// 重置指针(释放当前内存,指向新对象)
up5.reset(new Test(5));
up5->show();
// 释放所有权(up6不再管理该内存,需手动释放)
Test* raw_ptr = up6.release();
delete raw_ptr; // 必须手动delete,否则内存泄漏
cout << "=== test_unique_ptr_basic end ===" << endl;
}
// 2. 管理数组(需指定数组类型)
void test_unique_ptr_array() {
cout << "\n=== test_unique_ptr_array ===" << endl;
// 管理int数组(注意模板参数为int[])
unique_ptr<int[]> up_arr(new int[5]{10, 20, 30, 40, 50});
for (int i = 0; i < 5; ++i) {
cout << up_arr[i] << " "; // 支持[]运算符
}
cout << endl;
// 析构时会自动调用delete[],无需手动释放
cout << "=== test_unique_ptr_array end ===" << endl;
}
// 3. 作为函数返回值(自动移动,无需显式调用move)
unique_ptr<Test> create_test(int id) {
return make_unique<Test>(id); // 编译器自动优化为移动语义
}
void test_unique_ptr_return() {
cout << "\n=== test_unique_ptr_return ===" << endl;
unique_ptr<Test> up = create_test(10);
up->show();
cout << "=== test_unique_ptr_return end ===" << endl;
}
int main() {
test_unique_ptr_basic();
test_unique_ptr_array();
test_unique_ptr_return();
return 0;
}
运行结果如下:
=== test_unique_ptr_basic ===
Test(1) 构造
Test id: 1
up1 get: 0x7f8b4a405a00
Test(2) 构造
Test id: 2
Test id: 1
up1 get after move: 0x0
Test id: 2
up2 get after move: 0x0
Test(5) 构造
Test id: 5
Test(2) 析构
Test(5) 析构
=== test_unique_ptr_basic end ===
=== test_unique_ptr_array ===
10 20 30 40 50
=== test_unique_ptr_array end ===
=== test_unique_ptr_return ===
Test(10) 构造
Test id: 10
=== test_unique_ptr_return end ===
Test(10) 析构
从运行结果可以看出:
- unique_ptr对象离开作用域时,所指向的
Test对象会自动析构; - 移动操作后,原unique_ptr会被置空,避免二次释放;
- 管理数组时,析构函数会自动调用delete[],无需手动处理。
2.1.3 unique_ptr 的使用场景与最佳实践
unique_ptr是最常用的智能指针,适用于以下场景:
- 独占资源所有权 :当一块内存只需要被一个指针管理时,优先使用unique_ptr;
- 作为函数参数 / 返回值:传递临时对象的所有权(通过移动语义);
- 管理局部动态对象:替代裸指针,避免函数退出时忘记释放内存;
- 容器元素 :**vector<unique_ptr<T>>**是常见用法,避免容器元素的拷贝开销。
最佳实践:
- 优先使用
make_unique创建unique_ptr:make_unique是 C++14 引入的函数,它能避免直接使用new,减少内存泄漏风险(例如make_unique<Test>(1)比unique_ptr<Test>(new Test(1))更安全);- 避免手动调用
get()、release():这些函数会暴露裸指针,可能破坏unique_ptr的所有权管理,仅在必要时使用;- 管理数组时指定数组类型 :使用unique_ptr<T[]>而非
unique_ptr<T>,确保析构时调用delete[]。
2.2 shared_ptr:共享所有权的 "社交达人"
unique_ptr的独占性虽然高效,但无法满足 "多个指针共享同一块内存" 的场景(例如,多个对象需要引用同一个资源)。此时,shared_ptr应运而生 ------ 它支持共享所有权 ,多个shared_ptr可以指向同一块内存,当最后一个shared_ptr被销毁时,内存才会被释放。
2.2.1 shared_ptr 的核心原理:引用计数
shared_ptr的核心机制是引用计数(Reference Counting):
- 每个shared_ptr都封装了一个**"数据指针"**(指向实际对象)和一个 "控制块指针"(指向控制块);
- 控制块中存储了引用计数(当前指向该对象的shared_ptr数量)、弱引用计数 (当前指向该对象的
weak_ptr数量)以及对象的析构器等信息;- 当创建一个新的shared_ptr指向对象时,引用计数加 1;
- 当shared_ptr被销毁(析构)或指向其他对象(赋值)时,引用计数减 1;
- 当引用计数减为 0 时,控制块会调用析构器释放对象内存,随后释放控制块本身。

· 我们可以通过简化版的shared_ptr理解其原理:
cpp
// 简化版shared_ptr实现(仅演示核心逻辑)
template <typename T>
class MySharedPtr {
private:
T* data_ptr; // 指向实际对象的指针
struct ControlBlock {
int ref_count; // 引用计数
int weak_count; // 弱引用计数(为weak_ptr预留)
T* obj_ptr; // 指向对象的指针
ControlBlock(T* p) : ref_count(1), weak_count(0), obj_ptr(p) {}
~ControlBlock() {
delete obj_ptr; // 释放对象内存
}
};
ControlBlock* control_block; // 指向控制块的指针
// 减少引用计数,必要时释放控制块
void decrement_ref_count() {
if (control_block) {
control_block->ref_count--;
// 引用计数为0,且弱引用计数也为0时,释放控制块
if (control_block->ref_count == 0) {
if (control_block->weak_count == 0) {
delete control_block;
} else {
// 弱引用存在时,仅释放对象,不释放控制块
control_block->obj_ptr = nullptr;
}
}
control_block = nullptr;
data_ptr = nullptr;
}
}
public:
// 构造函数:创建新的控制块
explicit MySharedPtr(T* p = nullptr) : data_ptr(p) {
if (p) {
control_block = new ControlBlock(p);
} else {
control_block = nullptr;
}
}
// 析构函数:减少引用计数
~MySharedPtr() {
decrement_ref_count();
}
// 拷贝构造函数:共享控制块,引用计数加1
MySharedPtr(const MySharedPtr& other) {
data_ptr = other.data_ptr;
control_block = other.control_block;
if (control_block) {
control_block->ref_count++;
}
}
// 拷贝赋值运算符:先减少当前引用计数,再共享新的控制块
MySharedPtr& operator=(const MySharedPtr& other) {
if (this != &other) {
decrement_ref_count(); // 释放当前资源
data_ptr = other.data_ptr;
control_block = other.control_block;
if (control_block) {
control_block->ref_count++;
}
}
return *this;
}
// 移动构造函数:转移所有权,不修改引用计数
MySharedPtr(MySharedPtr&& other) noexcept {
data_ptr = other.data_ptr;
control_block = other.control_block;
other.data_ptr = nullptr;
other.control_block = nullptr;
}
// 移动赋值运算符:转移所有权
MySharedPtr& operator=(MySharedPtr&& other) noexcept {
if (this != &other) {
decrement_ref_count();
data_ptr = other.data_ptr;
control_block = other.control_block;
other.data_ptr = nullptr;
other.control_block = nullptr;
}
return *this;
}
// 重载->和*运算符
T* operator->() const { return data_ptr; }
T& operator*() const { return *data_ptr; }
// 获取引用计数
int use_count() const {
return control_block ? control_block->ref_count : 0;
}
// 获取裸指针
T* get() const { return data_ptr; }
};
从简化实现可以看出,shared_ptr的核心是控制块和引用计数。多个shared_ptr通过共享同一个控制块,实现了引用计数的同步更新,从而保证了共享所有权的正确性。
2.2.2 shared_ptr 的基本使用
shared_ptr的使用方式与unique_ptr类似,但支持拷贝操作。以下是常见操作示例:
cpp
#include <memory>
#include <iostream>
using namespace std;
class Test {
public:
Test(int id) : id_(id) {
cout << "Test(" << id_ << ") 构造" << endl;
}
~Test() {
cout << "Test(" << id_ << ") 析构" << endl;
}
void show() {
cout << "Test id: " << id_ << ", use_count: " << sp_self->use_count() << endl;
}
// 持有自身的shared_ptr(用于演示循环引用)
shared_ptr<Test> sp_self;
private:
int id_;
};
// 1. 基本初始化与拷贝
void test_shared_ptr_basic() {
cout << "=== test_shared_ptr_basic ===" << endl;
// 方式1:通过make_shared创建(推荐)
shared_ptr<Test> sp1 = make_shared<Test>(1);
cout << "sp1 use_count: " << sp1.use_count() << endl; // 1
// 拷贝构造:引用计数加1
shared_ptr<Test> sp2 = sp1;
cout << "sp1 use_count after copy: " << sp1.use_count() << endl; // 2
cout << "sp2 use_count after copy: " << sp2.use_count() << endl; // 2
// 拷贝赋值:引用计数加1
shared_ptr<Test> sp3;
sp3 = sp1;
cout << "sp1 use_count after assign: " << sp1.use_count() << endl; // 3
// 移动构造:引用计数不变
shared_ptr<Test> sp4 = move(sp1);
cout << "sp1 use_count after move: " << sp1.use_count() << endl; // 0(sp1已置空)
cout << "sp4 use_count after move: " << sp4.use_count() << endl; // 2(sp2和sp3仍在)
// 重置指针:引用计数减1
sp2.reset();
cout << "sp2 reset, sp3 use_count: " << sp3.use_count() << endl; // 1(仅sp3和sp4)
sp3.reset(new Test(2));
cout << "sp3 reset to new Test, use_count: " << sp3.use_count() << endl; // 1
cout << "=== test_shared_ptr_basic end ===" << endl;
// sp4、sp3离开作用域,Test(1)和Test(2)会被析构
}
// 2. 共享所有权的实际场景
void test_shared_ptr_share() {
cout << "\n=== test_shared_ptr_share ===" << endl;
shared_ptr<Test> sp = make_shared<Test>(10);
// 多个函数共享同一个Test对象
auto func1 = [&]() {
shared_ptr<Test> sp1 = sp;
cout << "func1: ";
sp1->show();
};
auto func2 = [&]() {
shared_ptr<Test> sp2 = sp;
cout << "func2: ";
sp2->show();
};
func1();
func2();
cout << "main: sp use_count: " << sp.use_count() << endl; // 1(func1和func2的sp1、sp2已析构)
cout << "=== test_shared_ptr_share end ===" << endl;
}
// 3. 循环引用问题(shared_ptr的致命缺陷)
void test_shared_ptr_cycle() {
cout << "\n=== test_shared_ptr_cycle ===" << endl;
shared_ptr<Test> sp1 = make_shared<Test>(100);
shared_ptr<Test> sp2 = make_shared<Test>(200);
// 循环引用:sp1持有sp2,sp2持有sp1
sp1->sp_self = sp2;
sp2->sp_self = sp1;
cout << "sp1 use_count: " << sp1.use_count() << endl; // 2(sp1和sp2->sp_self)
cout << "sp2 use_count: " << sp2.use_count() << endl; // 2(sp2和sp1->sp_self)
// sp1和sp2离开作用域时,引用计数减为1(而非0),导致Test对象无法析构
cout << "=== test_shared_ptr_cycle end ===" << endl;
}
int main() {
test_shared_ptr_basic();
test_shared_ptr_share();
test_shared_ptr_cycle();
return 0;
}
运行结果如下:
=== test_shared_ptr_basic ===
Test(1) 构造
sp1 use_count: 1
sp1 use_count after copy: 2
sp2 use_count after copy: 2
sp1 use_count after assign: 3
sp1 use_count after move: 0
sp4 use_count after move: 2
sp2 reset, sp3 use_count: 1
Test(2) 构造
sp3 reset to new Test, use_count: 1
=== test_shared_ptr_basic end ===
Test(1) 析构
Test(2) 析构
=== test_shared_ptr_share ===
Test(10) 构造
func1: Test id: 10, use_count: 2
func2: Test id: 10, use_count: 2
main: sp use_count: 1
=== test_shared_ptr_share end ===
Test(10) 析构
=== test_shared_ptr_cycle ===
Test(100) 构造
Test(200) 构造
sp1 use_count: 2
sp2 use_count: 2
=== test_shared_ptr_cycle end ===
从运行结果可以发现一个关键问题:test_shared_ptr_cycle函数结束后,Test(100)和Test(200)并没有被析构!这就是shared_ptr的致命缺陷 ------循环引用。
2.2.3 循环引用:shared_ptr 的 "阿喀琉斯之踵"
循环引用是指两个或多个shared_ptr互相持有对方的引用,导致它们的引用计数永远无法减为 0,从而造成内存泄漏。
在test_shared_ptr_cycle中:
sp1指向Test(100),sp2指向Test(200),初始引用计数均为 1;- sp1->sp_self = sp2:Test(100)的sp_self持有
sp2,sp2的引用计数变为 2;- sp2->sp_self = sp1:Test(200)的sp_self持有
sp1,sp1的引用计数变为 2;- 函数结束时,
sp1和sp2离开作用域,它们的引用计数各减 1,变为 1;- 此时,Test(100)的sp_self仍持有
sp2,Test(200)的sp_self仍持有sp1,引用计数无法减为 0,两个Test对象永远无法析构,造成内存泄漏。

为了解决循环引用问题 ,C++ 标准库引入了第三种智能指针 ------weak_ptr。
2.3 weak_ptr:打破循环的 "旁观者"
weak_ptr是一种**"弱引用"智能指针,它的核心特性是不拥有对象的所有权** ,仅能观察shared_ptr所管理的对象。weak_ptr不会增加shared_ptr的引用计数,因此不会导致循环引用问题。
2.3.1 weak_ptr 的核心原理
weak_ptr的底层依赖shared_ptr的控制块:
- weak_ptr仅存储控制块的指针,不存储数据指针;
- 创建weak_ptr时,会将控制块的弱引用计数加 1;
- weak_ptr无法直接访问对象,必须通过lock()方法获取一个shared_ptr(此时引用计数加 1),才能访问对象;
- 当shared_ptr的引用计数减为 0 时,对象会被析构,但控制块会保留到弱引用计数也为 0 时才释放;
- weak_ptr可以通过**expired()**方法判断所观察的对象是否已被析构。
我们可以通过简化版的weak_ptr理解其原理:
cpp
// 简化版weak_ptr实现(与MySharedPtr配套)
template <typename T>
class MyWeakPtr {
private:
typename MySharedPtr<T>::ControlBlock* control_block; // 仅指向控制块
public:
// 默认构造
MyWeakPtr() : control_block(nullptr) {}
// 从shared_ptr构造:弱引用计数加1
MyWeakPtr(const MySharedPtr<T>& sp) {
control_block = sp.control_block;
if (control_block) {
control_block->weak_count++;
}
}
// 拷贝构造
MyWeakPtr(const MyWeakPtr& other) {
control_block = other.control_block;
if (control_block) {
control_block->weak_count++;
}
}
// 析构函数:弱引用计数减1
~MyWeakPtr() {
if (control_block) {
control_block->weak_count--;
// 引用计数和弱引用计数均为0时,释放控制块
if (control_block->ref_count == 0 && control_block->weak_count == 0) {
delete control_block;
}
control_block = nullptr;
}
}
// 拷贝赋值
MyWeakPtr& operator=(const MyWeakPtr& other) {
if (this != &other) {
// 释放当前弱引用
if (control_block) {
control_block->weak_count--;
if (control_block->ref_count == 0 && control_block->weak_count == 0) {
delete control_block;
}
}
// 指向新的控制块
control_block = other.control_block;
if (control_block) {
control_block->weak_count++;
}
}
return *this;
}
// 从shared_ptr赋值
MyWeakPtr& operator=(const MySharedPtr<T>& sp) {
// 释放当前弱引用
if (control_block) {
control_block->weak_count--;
if (control_block->ref_count == 0 && control_block->weak_count == 0) {
delete control_block;
}
}
// 指向sp的控制块
control_block = sp.control_block;
if (control_block) {
control_block->weak_count++;
}
return *this;
}
// 检查对象是否已过期(被析构)
bool expired() const {
return !control_block || control_block->ref_count == 0;
}
// 获取shared_ptr,用于访问对象
MySharedPtr<T> lock() const {
if (expired()) {
return MySharedPtr<T>(nullptr); // 对象已过期,返回空shared_ptr
}
// 引用计数加1,返回shared_ptr
return MySharedPtr<T>(control_block->obj_ptr, control_block);
}
// 获取弱引用计数
int use_count() const {
return control_block ? control_block->ref_count : 0;
}
};
从简化实现可以看出,weak_ptr不直接管理对象内存,仅通过控制块的弱引用计数跟踪shared_ptr的状态。通过lock()方法,weak_ptr可以安全地获取shared_ptr来访问对象,避免了悬空指针问题。
2.3.2 weak_ptr 的基本使用与循环引用解决方案
weak_ptr不能单独使用,必须与shared_ptr配合。以下是其常见用法,重点演示如何解决循环引用问题:
cpp
#include <memory>
#include <iostream>
using namespace std;
class Test {
public:
Test(int id) : id_(id) {
cout << "Test(" << id_ << ") 构造" << endl;
}
~Test() {
cout << "Test(" << id_ << ") 析构" << endl;
}
void show() {
cout << "Test id: " << id_ << endl;
}
// 将shared_ptr改为weak_ptr,打破循环引用
weak_ptr<Test> wp_self;
private:
int id_;
};
// 1. weak_ptr的基本操作
void test_weak_ptr_basic() {
cout << "=== test_weak_ptr_basic ===" << endl;
shared_ptr<Test> sp = make_shared<Test>(1);
// 从shared_ptr创建weak_ptr
weak_ptr<Test> wp = sp;
cout << "wp use_count: " << wp.use_count() << endl; // 1(引用计数,weak_ptr不影响)
cout << "wp expired: " << wp.expired() << endl; // false(对象未析构)
// 通过lock()获取shared_ptr,访问对象
if (auto sp1 = wp.lock()) {
cout << "lock success: ";
sp1->show();
cout << "sp1 use_count: " << sp1.use_count() << endl; // 2(sp和sp1)
} else {
cout << "object has been destroyed" << endl;
}
// 重置shared_ptr,对象析构
sp.reset();
cout << "sp reset, wp expired: " << wp.expired() << endl; // true(对象已析构)
// 再次lock(),返回空shared_ptr
if (auto sp2 = wp.lock()) {
sp2->show();
} else {
cout << "lock failed: object has been destroyed" << endl;
}
cout << "=== test_weak_ptr_basic end ===" << endl;
}
// 2. 解决循环引用问题
void test_weak_ptr_solve_cycle() {
cout << "\n=== test_weak_ptr_solve_cycle ===" << endl;
shared_ptr<Test> sp1 = make_shared<Test>(100);
shared_ptr<Test> sp2 = make_shared<Test>(200);
// 用weak_ptr代替shared_ptr,避免循环引用
sp1->wp_self = sp2; // wp_self是weak_ptr,不增加sp2的引用计数
sp2->wp_self = sp1; // 同理,不增加sp1的引用计数
cout << "sp1 use_count: " << sp1.use_count() << endl; // 1(仅sp1)
cout << "sp2 use_count: " << sp2.use_count() << endl; // 1(仅sp2)
// 通过lock()访问对方
if (auto sp = sp1->wp_self.lock()) {
cout << "sp1 access sp2: ";
sp->show();
}
cout << "=== test_weak_ptr_solve_cycle end ===" << endl;
// sp1和sp2离开作用域,引用计数减为0,Test对象被析构
}
// 3. weak_ptr的其他应用场景:观察者模式
class Subject; // 前向声明
// 观察者类(弱引用主题,避免循环引用)
class Observer {
public:
Observer(const shared_ptr<Subject>& subject) : wp_subject(subject) {}
void update() {
if (auto sp_subject = wp_subject.lock()) {
cout << "Observer update: subject state = " << sp_subject->getState() << endl;
} else {
cout << "Subject has been destroyed" << endl;
}
}
private:
weak_ptr<Subject> wp_subject; // 弱引用主题
};
// 主题类(持有观察者的shared_ptr)
class Subject {
public:
void setState(int state) {
state_ = state;
notifyObservers();
}
int getState() const { return state_; }
void addObserver(const shared_ptr<Observer>& observer) {
observers_.push_back(observer);
}
private:
void notifyObservers() {
for (auto& observer : observers_) {
observer->update();
}
}
int state_;
vector<shared_ptr<Observer>> observers_;
};
void test_weak_ptr_observer() {
cout << "\n=== test_weak_ptr_observer ===" << endl;
shared_ptr<Subject> subject = make_shared<Subject>();
// 创建观察者(持有主题的弱引用)
shared_ptr<Observer> observer1 = make_shared<Observer>(subject);
shared_ptr<Observer> observer2 = make_shared<Observer>(subject);
subject->addObserver(observer1);
subject->addObserver(observer2);
// 主题状态变化,通知观察者
subject->setState(10);
subject->setState(20);
// 主题被销毁
subject.reset();
cout << "After subject reset:" << endl;
observer1->update(); // 输出"Subject has been destroyed"
cout << "=== test_weak_ptr_observer end ===" << endl;
}
int main() {
test_weak_ptr_basic();
test_weak_ptr_solve_cycle();
test_weak_ptr_observer();
return 0;
}
运行结果如下:
=== test_weak_ptr_basic ===
Test(1) 构造
wp use_count: 1
wp expired: 0
lock success: Test id: 1
sp1 use_count: 2
sp reset, wp expired: 1
lock failed: object has been destroyed
Test(1) 析构
=== test_weak_ptr_basic end ===
=== test_weak_ptr_solve_cycle ===
Test(100) 构造
Test(200) 构造
sp1 use_count: 1
sp2 use_count: 1
sp1 access sp2: Test id: 200
=== test_weak_ptr_solve_cycle end ===
Test(200) 析构
Test(100) 析构
=== test_weak_ptr_observer ===
Observer update: subject state = 10
Observer update: subject state = 10
Observer update: subject state = 20
Observer update: subject state = 20
After subject reset:
Subject has been destroyed
=== test_weak_ptr_observer end ===
从运行结果可以看出:
- weak_ptr不会增加shared_ptr的引用计数,因此不会导致循环引用;
- 通过lock()方法可以安全地获取shared_ptr,访问对象前会检查对象是否已析构;
- 在观察者模式中,weak_ptr可以避免观察者和主题之间的循环引用,同时允许主题被正常销毁。
2.3.3 weak_ptr 的使用场景与最佳实践
weak_ptr的使用场景主要包括:
- 解决
shared_ptr的循环引用问题 :这是weak_ptr最核心的用途,将循环引用中的一方改为weak_ptr即可;- 观察者模式 :观察者持有主题的weak_ptr,避免主题被观察者 "绑架"(即使有观察者,主题也能正常销毁);
- 缓存场景 :缓存对象的weak_ptr,当对象被销毁时,缓存自动失效,避免悬空指针。
最佳实践:
- 不单独使用
weak_ptr:weak_ptr必须与shared_ptr配合使用,无法直接访问对象;- 使用
lock()前先检查expired():虽然**lock()会返回空指针,但提前检查expired()**可以提高代码可读性;- 避免长期持有
lock()返回的shared_ptr:lock()返回的shared_ptr会增加引用计数,长期持有会导致对象无法及时析构。
三、智能指针的 "进阶技巧":定制删除器、类型转换与性能优化
除了基本用法,智能指针还有一些进阶技巧,可以应对更复杂的场景。
3.1 定制删除器:处理特殊资源释放
默认情况下,智能指针会使用delete(或delete[])释放内存。但在某些场景下,我们需要自定义释放逻辑(例如,释放动态数组、文件句柄、网络连接等),此时可以使用 "定制删除器"。
3.1.1 unique_ptr 的定制删除器
unique_ptr的定制删除器是模板参数的一部分,语法如下:
cpp
#include <memory>
#include <iostream>
#include <fstream>
using namespace std;
// 1. 释放动态数组(unique_ptr<T[]>已支持,此处仅为示例)
void delete_array(int* p) {
cout << "Custom deleter: delete[] array" << endl;
delete[] p;
}
// 2. 释放文件句柄
void close_file(FILE* fp) {
if (fp) {
cout << "Custom deleter: close file" << endl;
fclose(fp);
}
}
// 3. lambda表达式作为删除器(更简洁)
auto delete_lambda = [](Test* p) {
cout << "Custom deleter (lambda): delete Test(" << p->id_ << ")" << endl;
delete p;
};
void test_custom_deleter_unique() {
cout << "=== test_custom_deleter_unique ===" << endl;
// 1. 函数指针作为删除器
unique_ptr<int, decltype(&delete_array)> up1(new int[5]{1,2,3,4,5}, delete_array);
// 2. 释放文件句柄
FILE* fp = fopen("test.txt", "w");
unique_ptr<FILE, decltype(&close_file)> up2(fp, close_file);
// 3. lambda表达式作为删除器
unique_ptr<Test, decltype(delete_lambda)> up3(new Test(10), delete_lambda);
cout << "=== test_custom_deleter_unique end ===" << endl;
}
运行结果:
=== test_custom_deleter_unique ===
Test(10) 构造
=== test_custom_deleter_unique end ===
Custom deleter (lambda): delete Test(10)
Custom deleter: close file
Custom deleter: delete[] array
3.1.2 shared_ptr 的定制删除器
shared_ptr的定制删除器是构造函数的参数,语法更灵活
cpp
void test_custom_deleter_shared() {
cout << "\n=== test_custom_deleter_shared ===" << endl;
// 1. 函数指针作为删除器
shared_ptr<int> sp1(new int[5]{1,2,3,4,5}, [](int* p) {
cout << "Custom deleter (shared): delete[] array" << endl;
delete[] p;
});
// 2. 释放文件句柄
FILE* fp = fopen("test.txt", "w");
shared_ptr<FILE> sp2(fp, close_file);
// 3. 成员函数作为删除器
class FileDeleter {
public:
void operator()(FILE* fp) {
if (fp) {
cout << "Custom deleter (member function): close file" << endl;
fclose(fp);
}
}
};
shared_ptr<FILE> sp3(fopen("test2.txt", "w"), FileDeleter());
cout << "=== test_custom_deleter_shared end ===" << endl;
}
运行结果:
=== test_custom_deleter_shared ===
=== test_custom_deleter_shared end ===
Custom deleter (shared): delete[] array
Custom deleter: close file
Custom deleter (member function): close file
3.2 智能指针的类型转换
裸指针可以通过static_cast、dynamic_cast等进行类型转换,但智能指针不能直接使用这些运算符。C++ 标准库提供了专门的类型转换函数:
- static_pointer_cast:对应static_cast,用于静态类型转换;
- dynamic_pointer_cast:对应dynamic_cast,用于动态类型转换(支持运行时类型检查);
- const_pointer_cast:对应const_cast,用于去除
const限定。
示例代码:
cpp
#include <memory>
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() { cout << "Base show" << endl; }
virtual ~Base() {}
};
class Derived : public Base {
public:
void show() override { cout << "Derived show" << endl; }
void derived_func() { cout << "Derived specific function" << endl; }
};
void test_pointer_cast() {
cout << "=== test_pointer_cast ===" << endl;
// 1. dynamic_pointer_cast(动态类型转换)
shared_ptr<Base> sp_base = make_shared<Derived>();
if (auto sp_derived = dynamic_pointer_cast<Derived>(sp_base)) {
sp_derived->show(); // Derived show
sp_derived->derived_func(); // Derived specific function
} else {
cout << "dynamic_cast failed" << endl;
}
// 2. static_pointer_cast(静态类型转换)
shared_ptr<Base> sp_base2 = static_pointer_cast<Base>(make_shared<Derived>());
sp_base2->show(); // Derived show
// 3. const_pointer_cast(去除const限定)
shared_ptr<const Base> sp_const = make_shared<Base>();
auto sp_non_const = const_pointer_cast<Base>(sp_const);
sp_non_const->show(); // Base show
cout << "=== test_pointer_cast end ===" << endl;
}
int main() {
test_pointer_cast();
return 0;
}
运行结果:
=== test_pointer_cast ===
Derived show
Derived specific function
Derived show
Base show
=== test_pointer_cast end ===
注意:
- dynamic_pointer_cast仅对多态类型(含有虚函数)有效,非多态类型会编译失败;
- 类型转换仅适用于shared_ptr,unique_ptr不支持(因为unique_ptr的所有权独占,转换会破坏安全性)。
3.3 智能指针的性能优化
智能指针的性能开销主要来自两个方面:
- shared_ptr的引用计数操作(原子操作,有一定开销);
- 控制块的内存分配(shared_ptr的控制块需要额外分配内存)。
3.3.1 优先使用 unique_ptr
unique_ptr的性能几乎与裸指针一致(无额外开销),因此在不需要共享所有权时,优先使用unique_ptr,而非shared_ptr。
3.3.2 使用 make_shared 减少内存分配
make_shared会一次性分配对象和控制块的内存,而**shared_ptr<T>(new T())**会分两次分配(一次分配对象,一次分配控制块)。因此,make_shared不仅更安全,还能减少内存分配次数,提高性能。
cpp
// 推荐:一次内存分配(对象+控制块)
auto sp1 = make_shared<Test>(1);
// 不推荐:两次内存分配(对象和控制块分开)
auto sp2 = shared_ptr<Test>(new Test(2));
3.3.3 避免不必要的 shared_ptr 拷贝
shared_ptr的拷贝会增加引用计数(原子操作),频繁拷贝会影响性能。如果仅需要临时访问对象,可通过const&传递shared_ptr,避免拷贝。
cpp
// 推荐:传递const引用,避免拷贝
void func(const shared_ptr<Test>& sp) {
sp->show();
}
// 不推荐:拷贝shared_ptr,增加引用计数
void func(shared_ptr<Test> sp) {
sp->show();
}
3.3.4 合理使用 weak_ptr 的 lock ()
weak_ptr::lock()会返回shared_ptr,增加引用计数。如果仅需要短暂访问对象,应尽快释放lock()返回的shared_ptr,避免引用计数长期不为 0。
cpp
// 推荐:短暂持有lock()返回的shared_ptr
if (auto sp = wp.lock()) {
sp->show();
} // sp离开作用域,引用计数减1
// 不推荐:长期持有
auto sp = wp.lock();
// 长时间操作...
sp->show();
四、智能指针的 "避坑指南":常见错误与最佳实践总结
智能指针虽然强大,但如果使用不当,仍可能导致内存问题。以下为大家总结一下常见错误和最佳实践。
4.1 常见错误
-
将裸指针交给多个智能指针管理:导致二次释放。
cpp// 错误 int* p = new int(10); unique_ptr<int> up(p); shared_ptr<int> sp(p); // 错误:p被up和sp同时管理,会二次释放 -
使用
get()返回的裸指针创建智能指针:同样导致二次释放。cpp// 错误 shared_ptr<int> sp = make_shared<int>(10); int* p = sp.get(); shared_ptr<int> sp2(p); // 错误:sp和sp2都管理p,会二次释放 -
shared_ptr的循环引用 :导致内存泄漏(已通过weak_ptr解决)。 -
unique_ptr的拷贝操作 :unique_ptr禁用拷贝,强行拷贝会编译失败。cpp// 错误 unique_ptr<int> up1 = make_unique<int>(10); unique_ptr<int> up2 = up1; // 编译失败:拷贝构造被禁用 -
管理数组时使用
unique_ptr<T>而非unique_ptr<T[]>:导致析构时调用delete而非delete[],造成内存泄漏。cpp// 错误 unique_ptr<int> up(new int[5]); // 析构时调用delete,内存泄漏 // 正确 unique_ptr<int[]> up(new int[5]); // 析构时调用delete[] -
weak_ptr访问已过期的对象 :未检查expired()或lock()返回的shared_ptr是否为空,导致访问空指针。cpp// 错误 weak_ptr<int> wp; { auto sp = make_shared<int>(10); wp = sp; } *wp.lock(); // 错误:wp已过期,lock()返回空指针,解引用崩溃
4.2 最佳实践总结
- 优先使用智能指针,替代裸指针:除了极少数场景(如与 C 语言兼容),应尽量使用智能指针管理动态内存;
- 选择合适的智能指针 :
- 独占所有权:unique_ptr(优先选择,性能最优);
- 共享所有权:shared_ptr(配合
weak_ptr解决循环引用); - 观察对象:weak_ptr(不拥有所有权,解决循环引用);
- 优先使用
make_shared和make_unique:避免直接使用new,减少内存泄漏风险,提高性能; - 不混合使用裸指针和智能指针:避免二次释放和内存泄漏;
- 避免
get()、release()的滥用:仅在必要时使用,使用后确保裸指针的生命周期不超过智能指针; shared_ptr避免循环引用 :将循环引用中的一方改为weak_ptr;unique_ptr管理数组时指定T[]:确保析构时调用delete[];weak_ptr访问对象前检查有效性 :通过expired()或lock()返回的shared_ptr是否为空判断。
总结
从裸指针的 "步步惊心" 到智能指针的 "自动驾驶",C++ 的内存管理经历了一次革命性的进化。智能指针通过 RAII 机制,将内存管理的责任从开发者转移到编译器,从根本上解决了内存泄漏、二次释放、野指针等经典问题。
掌握智能指针的使用及其原理,不仅能让你的代码更安全、更高效,还能让你深刻理解 C++ 的 RAII 设计思想。在实际开发中,应遵循 "能⽤智能指针就不用裸指针" 的原则,合理选择合适的智能指针,让内存管理不再成为你的 "痛点",而是你的 "底气"。
最后,希望大家能记住一句话:智能指针不是银弹,但它是 C++ 内存管理最强大的武器。用好它,你就能在 C++ 的世界里 "畅行无阻" !