memory
<memory> 智能指针
什么是智能指针
智能指针(Smart Pointer)是对普通指针的一种包装,它能够在一定程度上自动管理所指对象的生命周期,避免显式 delete 带来的麻烦或潜在的内存泄漏、悬空指针等问题
智能指针是封装了原始指针的类,提供了自动内存管理的机制,它们通过 RAII(Resource Acquisition Is Initialization,资源获取即初始化)原则,确保资源在不再需要时自动释放,从而避免手动管理内存的复杂性和错误
C++ 的智能指针是标准库的一部分,特别适合处理动态分配的对象,减少了内存管理相关的 bug
C++11 引入了标准库中的一系列智能指针:std::unique_ptr、std::shared_ptr 和 std::weak_ptr(另外还有 std::auto_ptr,但已被废弃,不建议使用)
在 C++17、C++20 标准中也对这些智能指针进行了进一步的补充和强化
常见智能指针及其头文件
std::unique_ptr、std::shared_ptr 和 std::weak_ptr 均包含在 <memory> 这个标准库中
std::unique_ptr
特点:独占式所有权,不能被复制,但可以移动
适合表现“对象唯一所有者”的场景
std::shared_ptr
特点:共享式所有权,通过引用计数(Reference Counting)来管理同一对象的多个拥有者
在所有 std::shared_ptr 都销毁后,对象才会被释放
std::weak_ptr
特点:弱引用,不会影响引用计数,必须与 std::shared_ptr 搭配使用
它不会延长对象的生命周期,但可以安全地观测对象是否还存在
注意:在非常早期的 C++ 中曾有 std::auto_ptr(头文件
std::unique_ptr 的使用
std::unique_ptr 拥有对象的唯一所有权,不可复制,但可以移动(move)
举例如下:
# include <iostream>
# include <memory> // 包含智能指针相关定义
int main()
{
// 使用 std::unique_ptr 管理一个动态分配的 int
std::unique_ptr<int> uptr1(new int(10));
// C++14 开始建议用 std::make_unique 创建:
auto uptr2 = std::make_unique<int>(20);
// 不可复制,会编译报错:
// std::unique_ptr<int> uptr3 = uptr1; // error
// 但是可以移动
std::unique_ptr<int> uptr4 = std::move(uptr1);
// 此时 uptr1 变为空,uptr4 拥有之前 uptr1 所拥有的资源
// 访问指针内容
std::cout << "uptr4's value: " << *uptr4 << std::endl;
return 0;
}
使用注意事项:
-
std::unique_ptr不可拷贝,若需将其作为函数返回值、参数或成员变量,需要考虑移动语义(可以返回std::unique_ptr,也可以形参或成员变量直接持有它;如果希望转移所有权,可使用 std::move) -
如果你真的需要多个共享所有者,可以考虑使用 std::shared_ptr;若只需要唯一所有权则优先考虑 std::unique_ptr
-
不要手动 delete,智能指针会在被销毁时自动释放资源
std::shared_ptr 的使用
std::shared_ptr 使用引用计数来共享对象的所有权;当最后一个引用被销毁时,被管理的对象才会被释放
示例如下:
# include <iostream>
# include <memory>
int main()
{
// 通过 make_shared 创建共享指针(推荐用法)
std::shared_ptr<int> sptr1 = std::make_shared<int>(10);
// 拷贝构造或赋值可使引用计数+1
std::shared_ptr<int> sptr2(sptr1);
// 查看引用计数
std::cout << "Reference count: " << sptr1.use_count() << std::endl; // 2
// 访问指针的值
std::cout << "Value: " << *sptr1 << std::endl; // 10
// 销毁一个 sptr,不会立即销毁对象,只是计数减一
sptr2.reset();
std::cout << "Reference count after reset: " << sptr1.use_count() << std::endl; // 1
return 0;
}
使用注意事项:
-
避免循环引用(circular reference):
-
如果两个对象互相持有
std::shared_ptr会导致引用计数无法归零,从而造成内存泄漏 -
此时应将其中至少一方的
std::shared_ptr改为std::weak_ptr
-
-
创建方式通常推荐使用
std::make_shared<T>(),因为它更安全并且会进行单次内存分配(而new + std::shared_ptr的写法实际上可能分配两块内存,一块是对象,一块是控制块)。 -
std::shared_ptr可以被拷贝,也可以被移动;拷贝之后引用计数会增加,移动后原指针被置空。
std::weak_ptr 的使用
std::weak_ptr 不会增加引用计数,它主要用来观测由 std::shared_ptr 管理的对象是否还“活着”
因为它不影响对象的生命周期,所以可以在避免循环引用的场景中发挥重要作用。
# include <iostream>
# include <memory>
int main()
{
// 创建一个 std::shared_ptr
std::shared_ptr<int> sptr = std::make_shared<int>(100);
// 创建一个 std::weak_ptr 观察 sptr
std::weak_ptr<int> wptr(sptr);
// lock() 会返回一个 std::shared_ptr<int>
// 如果原对象已经被销毁,返回的共享指针将是空的
if (auto temp = wptr.lock())
{
std::cout << "Value: " << *temp << std::endl; // 100
std::cout << "use_count: " << temp.use_count() << std::endl;
}
else
{
std::cout << "Object is already destroyed." << std::endl;
}
// reset sptr
sptr.reset(); // sptr 被销毁,资源被释放
// 再次尝试 lock
if (auto temp = wptr.lock())
{
std::cout << "Value: " << *temp << std::endl;
}
else
{
std::cout << "Object is already destroyed." << std::endl;
}
return 0;
}
使用注意事项:
-
std::weak_ptr必须从std::shared_ptr构造或赋值而来,不能直接指向一个原始指针 -
由于
std::weak_ptr不增加引用计数,因此只有在lock()后才能安全地访问资源 -
常见用途:避免
std::shared_ptr之间的循环引用(如双向链表、树状结构的父子节点互相引用等)
常见的错误与注意事项
-
不匹配的
delete:-
一般不要通过
get()获得普通指针并手动delete,这样会破坏智能指针的内部机制 -
除非十分特殊的场景,否则都应当交由智能指针自己管理对象的释放
-
-
自定义删除器(Deleter):
- 在某些场景(如处理文件句柄、网络连接等资源)中,可以为智能指针传入自定义删除器,以在销毁时执行特定的清理逻辑
比如:
std::shared_ptr<FILE> filePtr(std::fopen("test.txt", "r"), [](FILE* fp){
if (fp) std::fclose(fp);
});
-
跨 DLL/共享库边界:
-
在某些操作系统和编译环境中,跨 DLL 或共享库边界传递
std::shared_ptr需要格外小心,必须保证使用相同的 C++ 运行时库,并且保证对象与其控制块在相同的内存堆中 -
如果库边界不兼容,可能会出现内存管理的问题
-
性能和内存方面的考虑
-
std::unique_ptr- 无需引用计数,开销最小,与普通指针几乎无差别
-
std::shared_ptr-
额外保存引用计数、控制块等信息,增加了内存和 CPU 负担
-
对象分配通常建议用
std::make_shared一次性分配,对象和控制块通常被连续分配在一起,这样性能和缓存命中更好
-
-
std::weak_ptr-
本身并不增加引用计数,但需要与
std::shared_ptr关联,有相应的控制块元数据 -
多次
lock()会有一定的管理开销
-
小结
-
头文件
- 所有标准智能指针都位于头文件
<memory>中,包含#include <memory>即可使用
- 所有标准智能指针都位于头文件
-
使用方法
-
优先使用工厂函数
std::make_shared<T>()或std::make_unique<T>()创建智能指针 -
对于需要“唯一所有权”资源,使用
std::unique_ptr -
对于“共享所有权”,使用
std::shared_ptr -
需要“观测”但不想延长对象生命周期时,使用
std::weak_ptr -
使用移动语义转移所有权,避免在不必要时使用复制拷贝
-
-
注意事项
-
不要产生循环引用,如有需要可引入
std::weak_ptr -
不要手动
delete由智能指针管理的资源,也不要通过get()后对原生指针进行不正确的操作 -
注意跨模块或库边界的兼容性
-
使用自定义删除器处理非常规资源时,确保删除器的正确性
-