C++ 智能指针(Smart Pointer)是 C++11 引入的用于自动管理动态内存的模板类,其核心作用是通过RAII(资源获取即初始化)机制,在智能指针生命周期结束时自动释放所管理的内存,从而避免传统裸指针(Raw Pointer)可能导致的内存泄漏、重复释放、悬垂指针等问题。
智能指针通过 RAII(Resource Acquisition Is Initialization) idiom 来解决这些问题:
- 获取资源即初始化:在构造函数中获取资源(分配内存)。
- 释放资源即析构:在析构函数中自动释放资源。只要智能指针对象超出作用域,无论是因为正常执行还是异常,其析构函数都会被调用,从而保证资源被释放。
所有智能指针都定义在 <memory>
头文件中。
1、std::unique_ptr - 独占所有权指针
std::unique_ptr
是独占式智能指针,其管理的资源只能被一个 unique_ptr
拥有,不允许拷贝(避免多个指针同时管理同一资源),但允许移动(转移所有权)。
1.1 基本用法
#include <memory>
#include <iostream>
using namespace std;class MyClass {
public:MyClass(int id) : id(id) {cout << "MyClass(" << id << ") 构造" << endl;}~MyClass() {cout << "MyClass(" << id << ") 析构" << endl;}void print() {cout << "MyClass id: " << id << endl;}
private:int id;
};int main() {// 1. 创建 unique_ptr(管理动态对象)unique_ptr<MyClass> ptr1(new MyClass(1)); // 直接初始化(不推荐,可能抛异常)auto ptr2 = make_unique<MyClass>(2); // 推荐:make_unique(更安全,避免内存泄漏)// 2. 访问资源(重载 * 和 ->)(*ptr1).print(); // 等价于 ptr1->print()ptr2->print();// 3. 转移所有权(只能通过 move,原指针变为空)unique_ptr<MyClass> ptr3 = move(ptr1); // ptr1 失去所有权,变为空if (ptr1 == nullptr) {cout << "ptr1 已为空" << endl;}ptr3->print(); // 仍可访问资源// 4. 主动释放资源(reset())ptr3.reset(); // 释放资源,ptr3 变为空if (ptr3 == nullptr) {cout << "ptr3 已释放资源" << endl;}return 0; // ptr2 离开作用域,自动释放资源
}
输出结果:
MyClass(1) 构造
MyClass(2) 构造
MyClass id: 1
MyClass id: 2
ptr1 已为空
MyClass id: 1
MyClass(1) 析构
ptr3 已释放资源
MyClass(2) 析构
1.2 注意事项
- 推荐使用
make_unique
:make_unique<T>(args)
直接在内部构造对象,避免裸new
可能导致的异常安全问题(如new
后未传给智能指针前抛异常,导致内存泄漏)。 - 禁止拷贝:
unique_ptr
的拷贝构造和拷贝赋值被删除(= delete
),只能通过std::move
转移所有权:unique_ptr<MyClass> ptr1 = make_unique<MyClass>(1); // unique_ptr<MyClass> ptr2 = ptr1; // 编译错误:禁止拷贝 unique_ptr<MyClass> ptr2 = move(ptr1); // 正确:转移所有权
- 数组支持:
unique_ptr
可管理动态数组(自动调用delete[]
):auto arr_ptr = make_unique<int[]>(5); // 管理包含5个int的数组 arr_ptr[0] = 10; // 支持下标访问
- 定制删除器:默认使用
delete
释放资源,可自定义删除器(如释放文件指针、网络连接等):// 自定义删除器(释放文件指针) auto file_deleter = [](FILE* f) {if (f) {fclose(f);cout << "文件已关闭" << endl;} };// 创建带自定义删除器的 unique_ptr unique_ptr<FILE, decltype(file_deleter)> file_ptr(fopen("test.txt", "w"), file_deleter);
1.3 应用场景
- 首选用于管理动态生命周期资源。
- 作为类的成员变量,表示独占资源。
- 在函数中动态创建对象并返回所有权(编译器会进行返回值优化 RVO,可能连移动都不需要)。
std::unique_ptr<MyClass> createObject() {return std::make_unique<MyClass>(); } auto obj = createObject();
2、std::shared_ptr - 共享所有权指针
多个 shared_ptr
可以共享同一个对象的所有权。通过引用计数机制来跟踪有多少个 shared_ptr
指向同一个对象。当最后一个 shared_ptr
被销毁时,对象才会被删除。
2.1 基本使用
#include <memory>
#include <iostream>
using namespace std;int main() {// 1. 创建 shared_ptr(推荐用 make_shared)shared_ptr<MyClass> ptr1 = make_shared<MyClass>(1);cout << "引用计数: " << ptr1.use_count() << endl; // 1// 2. 拷贝 shared_ptr(引用计数 +1)shared_ptr<MyClass> ptr2 = ptr1; // 拷贝,计数变为 2cout << "ptr1 计数: " << ptr1.use_count() << endl; // 2cout << "ptr2 计数: " << ptr2.use_count() << endl; // 2// 3. 转移部分指针的所有权(计数不变)shared_ptr<MyClass> ptr3 = move(ptr1); // ptr1 变为空,计数仍为 2(ptr2 和 ptr3 共享)cout << "ptr1 是否为空: " << (ptr1 == nullptr ? "是" : "否") << endl; // 是cout << "ptr3 计数: " << ptr3.use_count() << endl; // 2// 4. 释放部分指针(计数 -1)ptr2.reset(); // ptr2 释放,计数变为 1cout << "ptr3 计数: " << ptr3.use_count() << endl; // 1return 0; // ptr3 离开作用域,计数变为 0,资源释放
}
输出结果:
MyClass(1) 构造
引用计数: 1
ptr1 计数: 2
ptr2 计数: 2
ptr1 是否为空: 是
ptr3 计数: 2
ptr3 计数: 1
MyClass(1) 析构
2.2 注意事项
- 推荐使用
make_shared
:make_shared<T>(args)
一次性分配对象和引用计数的内存,效率高于shared_ptr<T>(new T(args))
(后者可能分配两次内存)。 - 循环引用问题:两个
shared_ptr
互相引用会导致引用计数无法归零,造成内存泄漏(需用weak_ptr
解决):
输出结果(无析构输出,内存泄漏):class B; // 前向声明 class A { public:shared_ptr<B> b_ptr; // A 持有 B 的 shared_ptr~A() { cout << "A 析构" << endl; } }; class B { public:shared_ptr<A> a_ptr; // B 持有 A 的 shared_ptr~B() { cout << "B 析构" << endl; } };int main() {auto a = make_shared<A>();auto b = make_shared<B>();a->b_ptr = b; // 互相引用b->a_ptr = a;// 离开作用域时,a和b的引用计数均为1(互相持有),无法释放,导致内存泄漏return 0; }
(无任何析构信息,A和B对象未释放)
- 定制删除器:与
unique_ptr
类似,可自定义删除器(所有共享该资源的shared_ptr
需使用相同删除器):// 自定义删除器(释放数组) auto arr_deleter = [](int* p) {delete[] p;cout << "数组已释放" << endl; };shared_ptr<int> arr_ptr(new int[5], arr_deleter);
2.3 应用场景
- 需要多个指针共享同一个对象时。
- 需要将指针存入标准容器时(
unique_ptr
C++11 不能直接存入容器,C++17 可以,但语义仍是独占)。 - 实现缓存的场景。
3、std::weak_ptr - 弱引用指针
std::weak_ptr
是一种弱引用智能指针,它指向 shared_ptr
管理的资源,但不增加引用计数,主要用于解决 shared_ptr 的循环引用问题。
3.1 基本使用
struct B;
struct A {std::shared_ptr<B> b_ptr;~A() { std::cout << "A destroyed\n"; }
};
struct B {std::shared_ptr<A> a_ptr; // 如果这里是shared_ptr,会产生循环引用~B() { std::cout << "B destroyed\n"; }
};// 循环引用导致内存泄漏:
{auto a = std::make_shared<A>();auto b = std::make_shared<B>();a->b_ptr = b; // b的引用计数为2b->a_ptr = a; // a的引用计数为2
} // 离开作用域,a和b的引用计数都减为1,永远不会变为0,对象无法被销毁!// 使用weak_ptr打破循环引用:
struct B;
struct A {std::shared_ptr<B> b_ptr;~A() { std::cout << "A destroyed\n"; }
};
struct B {std::weak_ptr<A> a_weak_ptr; // 使用weak_ptr而不是shared_ptr~B() { std::cout << "B destroyed\n"; }
};{auto a = std::make_shared<A>();auto b = std::make_shared<B>();a->b_ptr = b;b->a_weak_ptr = a; // 这里不会增加a的引用计数!(a的计数仍为1)
} // 离开作用域,a的计数变为0,先被销毁。然后b的计数变为0,也被销毁。
// 输出: A destroyed \n B destroyed
3.2 注意事项
- 不能直接访问资源:
weak_ptr
没有重载*
和->
,必须通过lock()
获取shared_ptr
后才能访问资源(避免访问已释放的资源)。 - 检查资源有效性:
expired()
方法可判断资源是否已释放(true
表示已释放):weak_ptr<MyClass> wp; {auto sp = make_shared<MyClass>(1);wp = sp; // wp 指向 sp 管理的资源cout << "资源是否有效: " << (!wp.expired() ? "是" : "否") << endl; // 是 } // sp 销毁,资源释放 cout << "资源是否有效: " << (!wp.expired() ? "是" : "否") << endl; // 否
3.3 适用场景
- 打破
shared_ptr
的循环引用。 - 实现缓存、观察者模式等,观察者不需要拥有被观察对象的所有权。
4、常见问题
std::unique_ptr
和std::shared_ptr
的根本区别是什么?
根本区别在于所有权语义。
unique_ptr
表示独占所有权,一个对象只能由一个unique_ptr
拥有,它不能被拷贝,只能移动。
shared_ptr
表示共享所有权,多个shared_ptr
可以共享同一个对象的所有权,通过引用计数机制管理生命周期。
为什么更推荐使用 make_shared
而不是直接 new
来创建 shared_ptr
?
主要有两个原因:
- 性能:
make_shared
通常只进行一次内存分配,同时为对象和控制块分配内存,而new
需要两次分配。 - 异常安全:
make_shared
避免了在函数参数求值过程中可能发生异常而导致的内存泄漏问题。
-
什么是循环引用?
weak_ptr
是如何解决它的?
循环引用是指两个或多个对象通过shared_ptr
互相持有,导致它们的引用计数永远无法降为 0,从而无法被析构,产生内存泄漏。weak_ptr
通过提供一种不增加引用计数的“弱引用”来打破这种循环。它将循环中的某一个shared_ptr
替换为weak_ptr
,这样就不会阻止所指向对象的销毁。 -
能否从一个
weak_ptr
直接访问资源?如果不能,应该如何做?
不能。因为weak_ptr
不拥有资源,它不知道资源是否已被释放。必须使用lock()
方法,它会返回一个shared_ptr
。如果资源还存在,这个shared_ptr
是有效的(并且会增加引用计数);如果资源已被释放,则返回一个空的shared_ptr
。必须检查lock()
的返回值。 -
智能指针能否用于管理数组?
可以。
std::unique_ptr
完全支持数组:std::unique_ptr<T[]>
,并且重载了operator[]
。使用make_unique<T[]>(size)
(C++17)。std::shared_ptr
对数组的支持不如unique_ptr
直接。在 C++17 及以后,可以用std::make_shared<T[]>(size)
,但 API 不如unique_ptr
完善。在 C++17 之前,通常需要为shared_ptr
指定一个删除器,如std::shared_ptr<int> sp(new int[10], std::default_delete<int[]>());
。
- 智能指针的大小是多少?开销有多大?
std::unique_ptr
:大小通常等于一个指针(例如 8 字节 on x64),开销极小,几乎就是封装了一个原生指针和一些编译期决定的逻辑。std::shared_ptr
:大小通常是两个指针(例如 16 字节 on x64)。一个指向管理的对象,另一个指向包含引用计数等的控制块。其开销包括额外的内存分配(控制块)和维护引用计数的原子操作。std::weak_ptr
:大小通常和shared_ptr
一样(两个指针),它也需要访问控制块。