C++智能指针是用于自动化管理动态内存的RAII(Resource Acquisition Is Initialization)封装类,核心目标是防止内存泄漏和空悬指针。标准库主要提供std::unique_ptr、std::shared_ptr和std::weak_ptr三种智能指针。
核心智能指针对比
| 特性 | std::unique_ptr | std::shared_ptr | std::weak_ptr |
|---|---|---|---|
| 所有权 | 独占所有权,不可拷贝,可移动 | 共享所有权,使用引用计数 | 弱引用,不增加引用计数,不拥有所有权 |
| 拷贝/移动 | 禁止拷贝,允许移动 | 允许拷贝和移动 | 允许拷贝和移动 |
| 主要用途 | 管理独占资源,替代auto_ptr | 管理多个指针共享的资源 | 解决shared_ptr循环引用问题,作为观察者 |
| 性能开销 | 近乎零开销(与裸指针相当) | 引用计数操作带来额外开销 | 引用计数操作带来额外开销 |
| 线程安全 | 自身线程安全,但管理的资源访问需额外同步 | 引用计数原子操作线程安全,但管理的资源访问需额外同步 | 同shared_ptr |
使用方法与代码示例
1.std::unique_ptr
独占资源所有权,离开作用域时自动释放内存。
#include <memory> #include <iostream> // 1. 创建 unique_ptr std::unique_ptr<int> u1(new int(42)); // 传统方式 auto u2 = std::make_unique<int>(100); // C++14 推荐方式,更安全高效 // 2. 访问资源 std::cout << *u1 << std::endl; // 输出: 42 u1.get(); // 获取裸指针// 3. 转移所有权(移动语义) std::unique_ptr<int> u3 = std::move(u1); // u1 变为 nullptr,所有权转移给 u3 // 4. 自定义删除器(例如用于文件句柄) struct FileDeleter { void operator()(FILE* fp) const { if (fp) fclose(fp); std::cout << "File closed." << std::endl; } }; std::unique_ptr<FILE, FileDeleter> upFile(fopen("test.txt", "r"));2.std::shared_ptr
通过引用计数实现共享所有权,当最后一个shared_ptr离开作用域时释放资源。
#include <memory> #include <iostream> // 1. 创建 shared_ptr auto s1 = std::make_shared<int>(200); //推荐方式,一次内存分配同时容纳引用计数和对象 std::shared_ptr<int> s2(new int(300)); // 不推荐,可能引发内存泄漏(如果后续操作抛出异常) // 2. 拷贝与引用计数 auto s3 = s1; //拷贝,引用计数+1 std::cout << s1.use_count() << std::endl; // 输出当前引用计数,例如 2 // 3. 自定义删除器 std::shared_ptr<int> s4(new int[10], [](int* p) { delete[] p; }); // 正确管理数组 // 4. 典型应用:共享资源 class Resource { public: Resource() { std::cout << "Resource acquired. "; } ~Resource() { std::cout << "Resource released. "; } void doSomething() { std::cout << "Doing something. "; } }; void process(std::shared_ptr<Resource> res) { res->doSomething(); // 函数结束,res 析构,但引用计数不为0,资源不释放 } int main() { auto mainRes = std::make_shared<Resource>(); // 引用计数=1 process(mainRes); // 传入时拷贝,引用计数=2;函数返回后,引用计数恢复为1 // main 结束,mainRes 析构,引用计数归0,资源释放 return 0; }3.std::weak_ptr
弱引用,用于打破shared_ptr的循环引用,避免内存泄漏。
#include <memory> #include <iostream> class B; // 前向声明 class A { public: std::shared_ptr<B> bPtr; ~A() { std::cout << "A destroyed. "; } }; class B { public: // 关键:使用 weak_ptr 而非 shared_ptr 来避免循环引用 std::weak_ptr<A> aWeakPtr; ~B() { std::cout << "B destroyed. "; } }; void testCycle() { auto a = std::make_shared<A>(); auto b = std::make_shared<B>(); a->bPtr = b; // A 强引用 B b->aWeakPtr = a; // B 弱引用 A,不增加 A 的引用计数 // 离开作用域,a 的引用计数从1变为0,A 被销毁。 // A 销毁导致其成员 bPtr 析构,B 的引用计数从1变为0,B 也被销毁。 // 若 B 中使用 shared_ptr 指向 A,则两者引用计数永不为0,导致内存泄漏。 } int main() { testCycle(); // 正常输出 "A destroyed." 和 "B destroyed." return 0; } // weak_ptr 的基本操作 auto sp = std::make_shared<int>(88); std::weak_ptr<int> wp = sp; // 创建 weak_ptr,不增加引用计数 if (auto locked = wp.lock()) { // 尝试提升为 shared_ptr std::cout << *locked << std::endl; // 提升成功,资源仍存在 } else { std::cout << "Object has been destroyed. "; // 提升失败,资源已释放 }核心原理:RAII 与引用计数
智能指针基于RAII思想:资源在构造函数中获取,在析构函数中自动释放,确保异常安全。shared_ptr内部维护一个控制块,其中包含引用计数、弱引用计数和删除器等。每次拷贝构造或赋值,引用计数递增;每次析构,引用计数递减,归零时调用删除器释放资源。
选择指南与最佳实践
- 默认选择
std::unique_ptr:除非需要共享所有权,否则优先使用unique_ptr,因其开销最小且语义明确。 - 需共享时使用
std::shared_ptr:当多个对象需要共同管理同一资源的生命周期时使用。 - 打破循环引用使用
std::weak_ptr:当存在shared_ptr相互引用或需要观察但不拥有资源时使用。 - 优先使用
make_shared和make_unique:它们提供更强的异常安全性,且对于make_shared,能将对象和控制块分配在连续内存中,提升效率。 - 避免裸指针与智能指针混用:不要将同一块原生内存交给多个独立的智能指针管理,会导致重复释放。
- 注意线程安全:智能指针本身的引用计数操作是原子的,但其所指向数据的读写仍需用户自行同步。
参考来源
- C++ 智能指针详细介绍:底层原理、使用方法和应用场景
- C++中智能指针的设计和使用
- 智能指针--C++
- 【C++】智能指针的使用及其原理
- cpp智能指针的原理与使用