shared_ptr创建方式以及循环引用问题
概念
shared_ptr 是 C++ 标准库提供的智能指针,用于管理动态分配的对象,通过引用计数实现资源的自动释放。它是现代 C++ 中避免内存泄漏的核心工具之一。
核心特点
- 引用计数:多个
shared_ptr
可共享同一对象,每复制 / 赋值一次计数 + 1,销毁时计数 - 1,计数为 0 时自动释放资源。 - 线程安全:引用计数的操作是原子的(但对象访问需自行同步)。
- 自定义删除器:支持自定义资源释放逻辑(如关闭文件、释放句柄等)。
创建方式
方式 | 语法示例 | 特点 |
---|---|---|
std::make_shared | auto ptr = std::make_shared<int>(42); | 推荐,一次内存分配,效率高;无法自定义删除器。 |
构造函数 | std::shared_ptr<int> ptr(new int(42)); | 两次内存分配;支持自定义删除器(如 [](int* p) { delete p; } )。 |
从 unique_ptr 转移 | std::unique_ptr<int> uptr(new int(42)); std::shared_ptr<int> sptr = std::move(uptr); | unique_ptr 放弃所有权,转为 shared_ptr 管理。 |
从原始指针转换 | int* raw = new int(42); std::shared_ptr<int> sptr(raw); | 不推荐,易导致多次释放(避免多个 shared_ptr 管理同一 |
操作
#include <memory>std::shared_ptr<int> ptr = std::make_shared<int>(42);// 获取引用计数
ptr.use_count(); // 返回当前共享对象的智能指针数量// 重置(释放当前对象,可指定新对象)
ptr.reset(); // 释放对象,ptr 为空
ptr.reset(new int(100)); // 释放原对象,指向新对象// 显式获取原始指针(谨慎使用)
int* raw = ptr.get(); // 返回原始指针,不增加引用计数// 判断是否独占对象
if (ptr.unique()) { /* ... */ } // 等价于 use_count() == 1
make_shared与直接构造对比
特性 | std::make_shared | 直接构造 (std::shared_ptr<T>(new T) ) |
---|---|---|
内存分配次数 | 1 次(对象 + 引用计数控制块一起分配) | 2 次(先分配对象,再分配控制块) |
内存布局 | 对象和控制块在同一块内存中 | 对象和控制块在不同内存区域 |
异常安全性 | 高(分配失败时无资源泄漏) | 需手动处理(可能导致资源泄漏) |
性能 | 更快(减少内存分配开销和碎片) | 略慢 |
自定义删除器 | 不支持 | 支持(如 [](T* p) { delete p; } ) |
数组支持 | C++17 起支持(需显式指定数组大小) | 直接支持(但需用 delete[] 删除器) |
构造函数参数 | 完美转发所有参数到对象构造函数 | 需显式创建对象(如 new T(args...) ) |
内存分配次数差异
这是两者最核心的区别,直接影响性能和内存布局。
std::make_shared
的内存分配
auto ptr = std::make_shared<int>(42); // 仅1次内存分配
- 步骤:
- 一次性分配一块足够大的内存,同时存储 对象 和 引用计数控制块。
- 在这块内存上构造对象,并初始化控制块。
- 优势:减少内存分配次数,提升缓存局部性(对象和控制块相邻)。
直接构造的内存分配
std::shared_ptr<int> ptr(new int(42)); // 2次内存分配
- 步骤:
- 先通过
new
分配对象的内存。 - 再分配
shared_ptr
内部的引用计数控制块。
- 先通过
- 劣势:两次分配可能导致内存碎片,且访问控制块和对象时缓存命中率较低。
异常安全性差异
std::make_shared
的安全性
// 假设 Foo 构造函数可能抛出异常
auto ptr = std::make_shared<Foo>(arg1, arg2);
- 安全机制:若构造
Foo
时抛出异常,整个内存分配会回滚,不会有资源泄漏。
直接构造的风险
std::shared_ptr<Foo> ptr(new Foo(arg1, arg2)); // 有潜在风险
风险点:
new Foo(arg1, arg2)
先执行,分配对象内存。- 若
Foo
构造函数抛出异常,而shared_ptr
尚未完全构造,对象内存无法被管理,导致泄漏。
修复方案
Foo* raw = new Foo(arg1, arg2); // 先分配
std::shared_ptr<Foo> ptr(raw); // 再传递给 shared_ptr(但仍需手动管理 raw)
构造函数参数
make_shared(完美转发参数到构造函数)
#include <memory>
#include <string>
using namespace std;class Person {
public:Person(string name, int age) : name_(name), age_(age) {}
private:string name_;int age_;
};int main() {// 使用 make_shared,直接传递构造函数需要的参数auto personPtr = make_shared<Person>("张三", 25);//std::shared_ptr<Person> ptr = std::make_shared<Person>("张三", 25);return 0;
}
在 make_shared<Person>("张三", 25)
中,"张三"
(右值,string
字面量隐式转换为 string
临时对象 )和 25
(右值) 会被完美转发给 Person
的构造函数。make_shared
内部通过模板和 std::forward
机制,保持参数的原始值类别(比如右值特性 ),高效地完成对象构造,不需要手动用 new
显式创建 Person
对象再传入智能指针构造。
直接构造 shared_ptr
(需显式创建对象)
#include <memory>
#include <string>
using namespace std;class Person {
public:Person(string name, int age) : name_(name), age_(age) {}
private:string name_;int age_;
};int main() {// 先显式用 new 创建 Person 对象Person* rawPerson = new Person("李四", 30);// 再用创建好的对象指针构造 shared_ptrshared_ptr<Person> personPtr(rawPerson);return 0;
}
这里必须先通过 new Person("李四", 30)
显式创建 Person
对象,得到原始指针 rawPerson
,然后再用它构造 shared_ptr
。相比于 make_shared
,多了手动 new
这一步,而且如果在 new
之后、构造 shared_ptr
之前发生异常,可能导致 rawPerson
指向的内存无法正确管理(引发内存泄漏风险 ),而 make_shared
是一次性完成内存分配和对象构造等逻辑,异常安全性更高。
为什么auto ptr
核心:方便少写代码
区别
//用auto
auto personPtr = make_shared<Person>("张三", 25);
//完整写出
shared_ptr<Person> ptr = std::make_shared<Person>("张三", 25);
写
auto
的话,编译器帮你猜类型,你少写很多字,还不会出错。额外好处:如果以后改了
std::make_shared
的类型(比如换成std::shared_ptr<Student>
),auto
不用动,编译器自己会更新类型。
循环引用问题
循环引用(Circular Reference)是 std::shared_ptr 使用中最常见的陷阱,指两个或多个对象通过 shared_ptr 互相引用,导致引用计数永远无法归零,造成内存泄漏。
问题示例
#include <memory>
#include<iostream>
class B; // 前向声明class A {
public:std::shared_ptr<B> b_ptr; // A 持有 B 的 shared_ptrA() { std::cout << "A created" << std::endl; }~A() { std::cout << "A destroyed" << std::endl; }
};class B {
public:std::shared_ptr<A> a_ptr; // B 持有 A 的 shared_ptrB() { std::cout << "B created" << std::endl; }~B() { std::cout << "B destroyed" << std::endl; }
};int main() {auto a = std::make_shared<A>(); // a 的引用计数为 1auto b = std::make_shared<B>(); // b 的引用计数为 1a->b_ptr = b; // b 的引用计数变为 2b->a_ptr = a; // a 的引用计数变为 2// main 函数结束时:// a 和 b 的局部变量被销毁,引用计数减为 1(而非 0)// 导致 A 和 B 的析构函数都不会被调用,内存泄漏!
}
问题本质
- 引用计数的死锁:A 和 B 互相持有对方的
shared_ptr
,导致各自的引用计数至少为 1。 - 内存泄漏:即使
main
函数结束,对象 A 和 B 的内存也无法释放。
解决方案:使用 std::weak_ptr
std::weak_ptr 是一种弱引用,不增加引用计数,用于打破循环引用:
#include <memory>
#include<iostream>
class B; // 前向声明class A {
public:std::weak_ptr<B> b_ptr; // A 持有 B 的 shared_ptrA() { std::cout << "A created" << std::endl; }~A() { std::cout << "A destroyed" << std::endl; }
};class B {
public:std::shared_ptr<A> a_ptr; // B 持有 A 的 shared_ptrB() { std::cout << "B created" << std::endl; }~B() { std::cout << "B destroyed" << std::endl; }
};int main() {auto a = std::make_shared<A>(); // a 的引用计数为 1auto b = std::make_shared<B>(); // b 的引用计数为 1a->b_ptr = b; // weak_ptr 不增加 b 的引用计数b->a_ptr = a; // a 的引用计数为 2std::cout << a.use_count() << std::endl;std::cout << b.use_count() << std::endl;// main 结束时:// 1. b 的局部变量销毁,b 的引用计数减为 1(来自 a->b_ptr 是 weak_ptr,不影响计数)// 2. a 的局部变量销毁,a 的引用计数减为 1(来自 b->a_ptr)// 3. 由于 b 的引用计数先变为 0,B 被销毁,b->a_ptr 被释放,a 的引用计数变为 0// 4. A 被销毁,内存正常释放
}