智能指针是 C++11 引入的核心特性,基于RAII(资源获取即初始化)机制,彻底解决了裸指针带来的内存泄漏、空悬指针、重复释放等问题。本文将深入拆解unique_ptr、shared_ptr、weak_ptr的原理、用法与陷阱,配合代码示例与内存结构分析,帮你彻底掌握智能指针。
一、为什么需要智能指针?
裸指针的三大痛点:
- 内存泄漏:忘记
delete或异常跳转导致delete未执行。 - 空悬指针:指针指向已释放的内存,访问时触发未定义行为。
- 重复释放:同一块内存被
delete多次,直接导致程序崩溃。
智能指针的核心思想:将资源(内存)的生命周期与对象的生命周期绑定。对象构造时获取资源,析构时自动释放资源,无需手动管理。
二、std::unique_ptr:独占所有权的智能指针
2.1 核心特性
- 独占所有权:同一时间只能有一个
unique_ptr指向同一个对象。 - 零开销:内存布局与裸指针几乎一致(无额外引用计数),性能极高。
- 仅支持移动语义:禁止拷贝构造和拷贝赋值,必须通过
std::move转移所有权。
2.2 基本用法(带详细注释)
#include <iostream> #include <memory> // 智能指针头文件 class MyClass { public: MyClass() { std::cout << "[MyClass] 构造函数调用\n"; } ~MyClass() { std::cout << "[MyClass] 析构函数调用\n"; } void do_something() { std::cout << "[MyClass] 执行 do_something()\n"; } }; int main() { // 1. 创建空 unique_ptr(C++11) std::unique_ptr<MyClass> ptr1; // 2. 用 new 初始化(不推荐,优先用 make_unique) std::unique_ptr<MyClass> ptr2(new MyClass()); // 3. 用 std::make_unique 创建(C++14 推荐,异常安全+高效) auto ptr3 = std::make_unique<MyClass>(); // 4. 访问对象成员(箭头运算符) ptr3->do_something(); // 5. 解引用访问对象 (*ptr3).do_something(); // 6. 转移所有权:std::move,转移后 ptr3 变为空 std::unique_ptr<MyClass> ptr4 = std::move(ptr3); if (!ptr3) std::cout << "[提示] ptr3 已失去所有权\n"; ptr4->do_something(); // 7. 释放所有权:release() 返回裸指针,需手动 delete MyClass* raw_ptr = ptr4.release(); if (!ptr4) std::cout << "[提示] ptr4 已释放所有权\n"; delete raw_ptr; // 必须手动释放,否则内存泄漏 // 8. 重置对象:reset() 释放当前对象,可指向新对象 std::unique_ptr<MyClass> ptr5(new MyClass()); ptr5.reset(); // 释放当前对象,ptr5 变空 ptr5.reset(new MyClass()); // 释放旧对象,指向新对象 return 0; } // main 结束,ptr2、ptr5 自动析构,释放内存2.3 自定义删除器
unique_ptr支持自定义删除器,可管理FILE*、数组等特殊资源:
#include <cstdio> #include <memory> // 自定义删除器:管理 FILE* struct FileDeleter { void operator()(FILE* fp) const { if (fp) { std::cout << "[FileDeleter] 关闭文件\n"; fclose(fp); } } }; int main() { // 管理文件指针 std::unique_ptr<FILE, FileDeleter> fp(fopen("test.txt", "w")); if (fp) fprintf(fp.get(), "Hello, unique_ptr!\n"); // 管理数组(C++11 原生支持) std::unique_ptr<int[]> arr(new int[5]{1, 2, 3, 4, 5}); for (int i = 0; i < 5; ++i) std::cout << arr[i] << " "; std::cout << "\n"; return 0; }2.4 内存结构示意
unique_ptr内存布局极简,仅包含一个裸指针(若有自定义删除器,可能通过空基类优化存储,不占额外内存):
unique_ptr 对象 └── 裸指针 ──→ 堆上的对象三、std::shared_ptr:共享所有权的智能指针
3.1 核心特性
共享所有权:多个shared_ptr可指向同一个对象。
引用计数:每个shared_ptr拷贝使引用计数 + 1,析构使引用计数 - 1;计数为 0 时自动释放对象。
线程安全:引用计数的增减是原子操作(多线程下安全,但对象本身的访问需额外同步)。
3.2 基本用法(带详细注释)
#include <iostream> #include <memory> class MyClass { public: MyClass() { std::cout << "[MyClass] 构造\n"; } ~MyClass() { std::cout << "[MyClass] 析构\n"; } }; int main() { // 1. 用 std::make_shared 创建(推荐,内存连续分配+异常安全) std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(); std::cout << "ptr1 引用计数: " << ptr1.use_count() << "\n"; // 输出 1 // 2. 拷贝构造:引用计数+1 std::shared_ptr<MyClass> ptr2 = ptr1; std::cout << "ptr1 引用计数: " << ptr1.use_count() << "\n"; // 输出 2 std::cout << "ptr2 引用计数: " << ptr2.use_count() << "\n"; // 输出 2 // 3. 移动构造:引用计数不变,ptr1 变空 std::shared_ptr<MyClass> ptr3 = std::move(ptr1); std::cout << "ptr3 引用计数: " << ptr3.use_count() << "\n"; // 输出 2 if (!ptr1) std::cout << "[提示] ptr1 已失去所有权\n"; // 4. reset():释放当前对象,引用计数-1 ptr2.reset(); std::cout << "ptr3 引用计数: " << ptr3.use_count() << "\n"; // 输出 1 return 0; } // ptr3 析构,引用计数-1→0,对象释放3.3 内存结构示意
shared_ptr包含两个指针:
对象指针:指向堆上的实际数据。
控制块指针:指向包含引用计数、弱引用计数、删除器、分配器的控制块。
shared_ptr 对象1 shared_ptr 对象2 ├── 裸指针 ──→ 对象 ├── 裸指针 ──→ 对象 └── 控制块指针 ──→ 控制块 └── 控制块指针 ──→ 控制块 控制块 ├── 强引用计数(shared_ptr 数量) ├── 弱引用计数(weak_ptr 数量) ├── 删除器 └── 分配器- 强引用计数为 0 → 对象释放。
- 弱引用计数也为 0 → 控制块释放。
3.4 致命陷阱:循环引用
shared_ptr最大的问题是循环引用,会导致内存永远无法释放:
#include <iostream> #include <memory> class B; // 前向声明 class A { public: std::shared_ptr<B> b_ptr; ~A() { std::cout << "[A] 析构\n"; } }; class B { public: std::shared_ptr<A> a_ptr; ~B() { std::cout << "[B] 析构\n"; } }; int main() { { auto a = std::make_shared<A>(); auto b = std::make_shared<B>(); a->b_ptr = b; // b 引用计数+1(→2) b->a_ptr = a; // a 引用计数+1(→2) } // 离开作用域,a、b 析构,引用计数各-1(→1) // 引用计数永远不为0,对象永远不释放! std::cout << "[main] 函数结束\n"; return 0; }运行代码,你会发现[A] 析构和[B] 析构永远不会打印 —— 这就是循环引用导致的内存泄漏。解决这个问题,需要std::weak_ptr。
四、std::weak_ptr:弱引用的智能指针
4.1 核心特性
不共享所有权:weak_ptr是shared_ptr的 “观察者”,不增加 / 减少强引用计数。
辅助作用:专门用于解决shared_ptr的循环引用问题,或观察对象是否已被释放。
访问控制块:weak_ptr指向shared_ptr的控制块,可通过expired()判断对象是否存在。
4.2 基本用法(带详细注释)
#include <iostream> #include <memory> class MyClass { public: MyClass() { std::cout << "[MyClass] 构造\n"; } ~MyClass() { std::cout << "[MyClass] 析构\n"; } void do_something() { std::cout << "[MyClass] 执行 do_something()\n"; } }; int main() { std::weak_ptr<MyClass> weak; { auto shared = std::make_shared<MyClass>(); weak = shared; // 从 shared_ptr 构造 weak_ptr,不增加引用计数 std::cout << "shared 引用计数: " << shared.use_count() << "\n"; // 输出 1 // 1. expired():检查对象是否已释放 if (!weak.expired()) std::cout << "[提示] 对象还存在\n"; // 2. lock():获取 shared_ptr(对象存在则返回有效指针,否则返回空) auto shared2 = weak.lock(); if (shared2) { shared2->do_something(); std::cout << "shared 引用计数: " << shared.use_count() << "\n"; // 输出 2 } } // 离开作用域,shared、shared2 析构,引用计数→0,对象释放 // 对象已释放 if (weak.expired()) std::cout << "[提示] 对象已释放\n"; auto shared3 = weak.lock(); if (!shared3) std::cout << "[提示] 无法获取 shared_ptr,对象已不存在\n"; return 0; }4.3 解决循环引用
将循环引用例子中的一个shared_ptr改为weak_ptr,即可打破循环:
#include <iostream> #include <memory> class B; class A { public: std::shared_ptr<B> b_ptr; // A 持有 B 的 shared_ptr ~A() { std::cout << "[A] 析构\n"; } }; class B { public: std::weak_ptr<A> a_ptr; // B 持有 A 的 weak_ptr(不增加引用计数) ~B() { std::cout << "[B] 析构\n"; } }; int main() { { auto a = std::make_shared<A>(); auto b = std::make_shared<B>(); a->b_ptr = b; // b 强引用计数+1(→2) b->a_ptr = a; // a 强引用计数不变(→1) } // 离开作用域,a 析构→强引用计数→0→a 释放 // a 释放后,b 的 shared_ptr 析构→b 强引用计数→0→b 释放 std::cout << "[main] 函数结束\n"; return 0; }现在运行代码,[A] 析构和[B] 析构都会正常打印,内存泄漏问题完美解决!
五、总结:智能指针选型与最佳实践
5.1 三种智能指针对比
表格
| 智能指针 | 所有权 | 引用计数 | 内存开销 | 适用场景 |
|---|---|---|---|---|
unique_ptr | 独占 | 无 | 极低 | 不需要共享所有权的对象 |
shared_ptr | 共享 | 有 | 中等 | 需要多个指针共享的对象 |
weak_ptr | 无(观察者) | 无 | 极低 | 辅助shared_ptr解决循环引用 |
5.2 最佳实践
- 优先使用
make_unique/make_shared:比直接用new更安全(异常安全)、更高效(内存连续分配)。 - 避免裸指针与智能指针混用:不要将同一个裸指针交给多个智能指针管理,否则会导致重复释放。
- 优先用
unique_ptr:只有在必须共享所有权时才用shared_ptr——unique_ptr性能更好,内存开销更小。 - 用
weak_ptr打破循环引用:当两个类需要互相引用时,将其中一个类的成员变量设为weak_ptr。
智能指针是现代 C++ 内存管理的核心工具,掌握它的原理与用法,能让你彻底告别内存泄漏的噩梦。如果需要进一步探究智能指针的底层实现(如引用计数的原子操作、控制块的内存布局),请等下一篇解读