目录
智能指针的概念
C++中的智能指针是一种用于自动管理动态内存的工具,遵循RAII原则,确保资源在对象生命周期结束时自动释放,从而避免内存泄漏和悬空指针等问题。
RAII原则
RAII(Resource Acquisition Is Initialization,资源获取即初始化)是C++中管理资源的核心原则,通过对象的生命周期自动化资源管理,确保资源的获取与释放安全可靠。
构造时获取资源:在对象的构造函数中完成资源分配(如内存、文件句柄、锁等)。
析构时释放资源:在析构函数中自动释放资源,确保资源不泄漏。
对象生命周期绑定:资源管理与对象作用域一致,离开作用域时自动触发析构。
实现步骤
// 封装资源到类
class FileHandle
{
private:
FILE* file;
public:
FileHandle(const char* filename, const char* mode)
{
file = fopen(filename, mode);
if (!file)
{
throw std::runtime_error("Open failed");
}
}
~FileHandle() { if (file) fclose(file); }
// 禁用拷贝(避免重复释放)
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// 允许移动(安全转移所有权)
FileHandle(FileHandle&& other) noexcept
: file(other.file)
{
other.file = nullptr;
}
};
智能指针的使用
unique_ptr
unique_ptr
是 C++11 引入的智能指针,用于管理动态分配的内存资源,遵循 独占所有权(exclusive ownership) 原则。它的核心目标是自动释放内存,避免内存泄漏,同时提供零额外开销的高效内存管理。
核心作用
自动内存管理:
unique_ptr
离开作用域时,自动释放其管理的资源(调用delete
或自定义删除器)。独占所有权:同一时间只能有一个
unique_ptr
拥有资源,禁止拷贝操作(保证所有权唯一性)。高效轻量:与裸指针性能一致,无引用计数等额外开销(对比
std::shared_ptr
)。支持移动语义:通过
std::move
转移所有权,适合资源传递场景。
基本用法
一、创建
unique_ptr
1、使用 std::make_unique
(C++14+ 推荐)
安全且高效,避免直接使用 new
#include <memory>
auto ptr1 = make_unique<int>(42); // 管理一个 int
auto ptr2 = make_unique<MyClass>(); // 管理一个对象
auto arr = make_unique<int[]>(10); // 管理动态数组(C++14+)
arr[0] = 1; // 直接通过下标访问
2、直接构造(C++11 或特殊场景)
需要显式 new
,适用于自定义删除器
unique_ptr<int> ptr(new int(10));
3、空指针初始化
默认构造函数生成空指针
unique_ptr<int> empty_ptr; // 初始化为 nullptr
二、所有权转移
1、通过 move
转移后,原指针变为 nullptr
auto ptr1 = std::make_unique<int>(100);
auto ptr2 = std::move(ptr1); // ptr1 不再拥有资源
2、函数返回 unique_ptr
函数可以安全返回 unique_ptr
(所有权转移)
unique_ptr<int> create_ptr(int value)
{
return make_unique<int>(value);
}
auto ptr = create_ptr(50); // 所有权转移给 ptr
三、访问资源
1、操作符 ->
和 *
类似裸指针访问成员或解引用
struct MyClass
{
void doSomething()
{}
};
auto obj = make_unique<MyClass>();
obj->doSomething(); // 访问成员函数
int value = *obj; // 解引用获取值(假设 obj 管理的是 int)
四、释放与重置资源
1、主动释放资源 release()
放弃所有权,返回裸指针(需手动管理)
auto ptr = std::make_unique<int>(20);
int* raw = ptr.release(); // ptr 变为 nullptr,需手动 delete raw
2、重置资源 reset()
释放当前资源并接管新资源(或置空)
ptr.reset(new int(30)); // 释放旧值,管理新分配的 int(30)
ptr.reset(); // 等同于 ptr = nullptr
注意事项
禁止拷贝,只能移动。
优先使用
make_unique
。避免暴露裸指针。
// 1、禁止拷贝(编译错误示例)
auto ptr1 = make_unique<int>(10);
auto ptr2 = ptr1; // 错误!unique_ptr 不可拷贝
// 2、避免悬空指针(所有权转移后,原指针不再有效)
auto ptr1 = make_unique<int>(10);
auto ptr2 = move(ptr1);
*ptr1 = 20; // 未定义行为!ptr1 已为空
// 3、优先使用 make_unique
// 比直接 new 更安全(避免异常导致的内存泄漏)
// 4、不要混合使用裸指针
int* raw = new int(5);
unique_ptr<int> p1(raw);
unique_ptr<int> p2(raw); // 重复释放!
shared_ptr
shared_ptr
是 C++11 引入的智能指针,用于管理动态分配的资源,支持共享所有权。多个 shared_ptr
可以指向同一对象,通过引用计数自动释放资源。
通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次。
核心作用
共享所有权
多个shared_ptr
可共享同一对象的所有权,资源在所有所有者销毁后自动释放。引用计数
内部维护一个计数器,记录指向对象的shared_ptr
数量。计数归零时调用析构函数。线程安全
引用计数的增减是原子操作(线程安全),但对象本身的访问需额外同步。内存开销
每个shared_ptr
需要存储指向对象和控制块(含引用计数)的指针,比unique_ptr
占用更多内存。
基本用法
一、创建
shared_ptr
1、使用 make_shared
(C++14+ 推荐)
安全且高效,避免直接使用 new
#include <memory>
auto sptr1 = make_shared<int>(42); // 创建 int
auto sptr2 = make_shared<MyClass>(); // 创建对象
2、直接构造(C++11 或特殊场景)
需要显式 new
,适用于自定义删除器
shared_ptr<int> sptr(new int(10));
3、空指针初始化
默认构造函数生成空指针
shared_ptr<int> empty_sptr; // 初始化为 nullptr
二、共享所有权
1、拷贝与赋值
引用计数自动增加
auto sptr1 = make_shared<int>(100);
shared_ptr<int> sptr2 = sptr1; // 引用计数 +1
2、函数传递共享所有权
函数参数或返回值可安全传递 shared_ptr
void process(shared_ptr<MyClass> obj)
{
// 引用计数 +1,函数结束时 -1
}
auto obj = make_shared<MyClass>();
process(obj); // 安全传递
三、访问资源
1、操作符 ->
和 *
与裸指针用法一致
struct MyClass
{
void doSomething()
{
cout << "访问成员" << endl;
}
};
auto obj = make_shared<MyClass>();
auto sptr1 = make_shared<int>(10);
obj->doSomething(); // 访问成员
int value = *sptr1; // 解引用
cout << value << endl; // value = 10
四、引用计数管理
1、查看引用计数
通过 use_count()
(通常用于调试)
cout << sptr1.use_count(); // 输出当前引用计数
2、重置指针
使用 reset()
减少引用计数或释放资源
sptr1.reset(); // 引用计数 -1,若归零则释放资源
sptr1.reset(new int(20)); // 释放旧资源,管理新资源
循环引用与
weak_ptr
循环引用问题
两个对象互相持有对方的 shared_ptr
,导致引用计数无法归零:
class Node
{
public:
shared_ptr<Node> _next;
int _val;
};
int main()
{
auto node1 = make_shared<Node>();
auto node2 = make_shared<Node>();
node1->_next = node2; // node2 引用计数 = 2
node2->_next = node1; // node1 引用计数 = 2 → 循环引用!
cout << "node1:" << node1.use_count() << endl;
cout << "node2:" << node2.use_count() << endl;
return 0;
}
程序运行结束后变量node1 和 node2 都没有办法正常释放,是因为这两句连接结点的代码导致了循环引用。
- 当以 make_shared 的方式创建了两个 Node 结点并交给两个智能指针管理后,这两个资源对应的引用计数都被加到了1。
- 将这两个结点连接起来后,node1资源 当中的 _next成员 与 node2 一同管理 node2的资源,node2资源 中的_next 成员 与 node1 一同管理 node1的资源,此时这两个资源对应的引用计数都被加到了2。
- 当node1 和 node2的生命周期也就结束了,此时这两个资源对应的引用计数最终都减到了1。
循环引用导致资源未被释放的原因:
- 当资源对应的引用计数(use_count)减为0时对应的资源才会被释放,因此node1资源 的释放取决于 node2资源当中的 _next成员,而node2资源的释放取决于node1资源当中的 _next成员。
- 而node1资源当中的 _next成员的释放又取决于node1资源,node2资源当中的_next成员的释放又取 决于node1资源,于是这就变成了一个死循环,最终导致资源无法释放。
而如果连接结点时只进行一个连接操作,那么当node1和node2的生命周期结束时,就会有一个资源对应的引用计数被减为0,此时这个资源就会被释放,这个释放后另一个资源的引用计数也会被减为0,最终两个资源就都被释放了,这就是为什么只进行一个连接操作时这两个结点就都能够正确释放的原因。
线程安全
尽管引用计数是原子安全的,但资源本身的访问不提供线程安全保证。若多个线程通过不同的 shared_ptr
访问同一资源,需手动同步:
示例:未加锁导致数据竞争
#include <memory>
#include <thread>
struct Data
{
int value = 0;
};
void unsafe_increment(std::shared_ptr<Data> data)
{
for (int i = 0; i < 100000; ++i)
{
data->value++; // 非原子操作,存在数据竞争
}
}
int main()
{
auto data = std::make_shared<Data>();
std::thread t1(unsafe_increment, data);
std::thread t2(unsafe_increment, data);
t1.join();
t2.join();
// 预期 data->value = 200000,实际结果不确定
std::cout << data->value << std::endl;
return 0;
}
对共享资源的访问需通过互斥锁(如 std::mutex
)或其他同步机制保护:
#include <memory>
#include <thread>
#include <mutex>
struct Data
{
int value = 0;
std::mutex mtx; // 为资源添加互斥锁
};
void safe_increment(std::shared_ptr<Data> data)
{
for (int i = 0; i < 100000; ++i)
{
std::lock_guard<std::mutex> lock(data->mtx); // 加锁
data->value++; // 受保护的原子性操作
}
}
int main()
{
auto data = std::make_shared<Data>();
std::thread t1(safe_increment, data);
std::thread t2(safe_increment, data);
t1.join();
t2.join();
std::cout << data->value << std::endl; // 输出 200000
return 0;
}
自定义删除器
- 当智能指针对象的生命周期结束时,所有的智能指针默认都是以
delete
的方式将资源释放,这是不太合适的。 - 因为智能指针并不是只管理以
new
方式申请到的内存空间,智能指针管理的也可能是管理的是一个文件指针。管理非内存资源指定释放逻辑(如文件、网络连接)。
struct ListNode
{
ListNode* _next;
ListNode* _prev;
int _val;
};
shared_ptr<ListNode> sp1(new ListNode[10]);
shared_ptr<FILE> sp2(fopen("test.cpp", "r"));
这时当智能指针对象的生命周期结束时,再以delete
的方式释放管理的资源就会导致程序崩溃,因为以new[]
的方式申请到的内存空间必须以delete[]
的方式进行释放,而文件指针必须通过调用fclose
函数进行释放。
这时就需要用到定制删除器来控制释放资源的方式,C++标准库中的shared_ptr提供了如下构造函数:
template <class U, class D>
shared_ptr (U* p, D del);
// 参数说明:
// p:需要让智能指针管理的资源。
// del:删除器,这个删除器是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象。
// 当shared_ptr对象的生命周期结束时就会调用传入的删除器完成资源的释放,调用该删除器时会将shared_ptr管理的资源作为参数进行传入。
代码如下:
struct ListNode
{
ListNode* _next;
ListNode* _prev;
int _val;
};
void socket_deleter(ListNode* s)
{
cout << "delete[]: " << s << endl;
delete[] s;
}
int main()
{
// 使用函数指针
shared_ptr<ListNode> socket(new ListNode[10], socket_deleter);
// lambda 表达式
shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr) {
fclose(ptr);
});
return 0;
}
weak_ptr
weak_ptr
是 C++11 引入的智能指针,用于解决 shared_ptr
的循环引用问题,并提供一种非拥有性观察资源的方式。它不会增加引用计数,也无法直接访问资源,需通过 shared_ptr
间接操作。
将 Node 中的 _next 成员的类型换成 weak_ptr 就不会导致循环引用问题了。
class Node
{
public:
weak_ptr<Node> _next;
int _val;
};
int main()
{
auto node1 = make_shared<Node>();
auto node2 = make_shared<Node>();
node1->_next = node2; // node2 引用计数 = 1
node2->_next = node1; // node1 引用计数 = 1
cout << "node1:" << node1.use_count() << endl;
cout << "node2:" << node2.use_count() << endl;
return 0;
}
总结
智能指针通过RAII机制自动化资源管理,其核心原理如下:
类型 | 所有权模型 | 核心机制 | 典型用途 |
---|---|---|---|
std::unique_ptr |
独占所有权 | 禁用拷贝,支持移动 | 单一所有者,明确生命周期 |
std::shared_ptr |
共享所有权 | 引用计数与控制块 | 多所有者共享资源 |
std::weak_ptr |
非拥有性观察 | 弱引用,lock() 安全访问 |
解决循环引用,缓存观察 |
循环引用:
使用weak_ptr
替代shared_ptr
,打破引用循环。混合使用原始指针:
避免用同一裸指针初始化多个shared_ptr
,防止重复释放。线程安全:
引用计数操作原子安全,但资源访问需加锁。