C++ unique_ptr、shared_ptr、weak_ptr全面解析
文章目录
- unique_ptr
- 所有权转移
- 所有权释放
- make_unique 和直接(new)unique_ptr
- 区别一 :需要一次性处理多个资源分配的地方make_unique比unique_ptr更安全
- 区别二 :直接new支持自定义删除器
- shared_ptr
- shared_ptr智能指针指向同一个对象的不同成员
- 循环引用问题
- 为什么weak_ptr能解决shared_ptr的循环引用问题
- make_shared 和 直接 new 一个shared_ptr的区别
- 区别一:make_shared资源分配更安全
- 区别二:直接new支持自定义删除器
- 区别三:内存分配方式不同
- 为什么make_unique和直接new unique_ptr都是一次分配内存
- shared_ptr合并分配的缺点
- shared_ptr控制块内存的延迟释放是内存泄漏吗?
unique_ptr
所有权转移
unique_ptr不能被拷贝和用于赋值,因为unique_ptr删掉了这两个函数
但是底层源码重载了传右值的拷贝构造
所以可以通过std::move来通过转移所有权
unique_ptr<Data> p6(new Data());
//不可复制构造和赋值复制
//unique_ptr<Data>p7 = p6; 错误
//p6释放所有权 转移到p7
unique_ptr<Data>p7 = move(p6);
unique_ptr<Data>p8(new Data());
p7 = move(p8);//重新移动赋值,原有的p6会被释放掉
//重置空间,原空间清理
p7.reset(new Data());
所有权释放
注意!当unique_ptr释放所有权以后智能指针就不会再管理这块空间,需要自己手动释放空间!
//释放所有权
unique_ptr<Data>p9(new Data());
auto ptr9 = p9.release();//注意,release释放所有权以后要自己清理空间
delete ptr9;//!!!!!!
make_unique 和直接(new)unique_ptr
区别一 :需要一次性处理多个资源分配的地方make_unique比unique_ptr更安全
void process_data(
std::unique_ptr<Data> p1,
std::unique_ptr<Data> p2
);
process_data(
std::unique_ptr<Data>(new Data("A")), // 分配资源 A
std::unique_ptr<Data>(new Data("B")) // 分配资源 B
);
编译器在构造函数参数时,执行顺序是不确定的。可能的执行顺序例如:
- new Data(“A”) → 成功,得到一个裸指针 A
- new Data(“B”) → 成功,得到一个裸指针 B*
- 构造unique_ptr 接管 A*
- 构造 unique_ptr 接管 B*
如果中间发生异常:
- new Data(“A”) → 成功,得到 A*
- new Data(“B”) → 抛出异常(例如内存不足) 此时 A* 尚未被 unique_ptr 接管! 异常被抛出后,裸指针 A* 无法被自动释放 → 内存泄漏。
如果选用make_unique
process_data(
std::make_unique<Data>("A"), // 直接构造并接管资源 A
std::make_unique<Data>("B") // 直接构造并接管资源 B
);
此时每一步的执行:
- make_unique(“A”) → 立即构造对象并封装到 unique_ptr,无裸指针暴露
- make_unique(“B”) → 同上 如果 make_unique(“B”) 抛出异常:
make_unique(“A”) 已经返回的 unique_ptr 会正常析构 → 资源 A 被自动释放没有泄漏!
区别二 :直接new支持自定义删除器
new支持在构造 unique_ptr 时指定自定义删除器
std::unique_ptr<MyClass, Deleter> p(new MyClass, custom_deleter);
但是make_unique不支持,只能使用默认的 delete 操作符。
shared_ptr
shared_ptr智能指针指向同一个对象的不同成员
sc2和sc3 分别 指向sc1的index1成员和index2成员,使sc1的引用计数+2
class Data
{
public:
Data() {
cout<< "Begin Data" << endl;
}
~Data() { cout<< "End Data" << endl; }
int index1 = 0;
int index2 = 0;
};
{
shared_ptr<Data>sc1(new Data);
//打印引用计数 = 1
cout << "sc1.use_count() = " << sc1.use_count() << endl;
shared_ptr<int>sc2(sc1,&sc1->index1);//引用计数+1
shared_ptr<int>sc3(sc1, &sc1->index2);//引用计数+1
//打印引用计数 = 3
cout << "sc1.use_count() = " << sc1.use_count() << endl;
}
循环引用问题
当两个或多个对象通过 shared_ptr 互相持有对方时,它们的引用计数永远不会归零,导致内存无法释放。
class A
{
public:
A() { cout << "Create A" << endl; }
~A() { cout << " Drop A " << endl; }
void Do()
{
cout << "Do b2.use_count() = " << b2.use_count() << endl;
auto b = b2.lock(); //复制一个shared_ptr 引用计数加一
cout << "Do b2.use_count() = " << b2.use_count() << endl;
}
shared_ptr<B> b1;//强智能指针
weak_ptr<B> b2; //弱智能指针
};
class B
{
public:
B() { cout << "Create B" << endl; }
~B(){ cout << " Drop B " << endl; }
shared_ptr<A> a1;//强智能指针
weak_ptr<A> a2; //弱智能指针
};
{
auto a = make_shared<A>();//a引用计数+1
auto b = make_shared<B>();//b引用计数+1
a->b1 = b;//b引用计数+1
//出作用域前,引用计数为2
cout << "a->b1 = b;b.use_count()=" << b.use_count() << endl;
b->a1 = a;//a引用计数+1
//出作用域前,引用计数为2
cout << "b->a1 = a;a.use_count()=" << a.use_count() << endl;
}
由于调用问题,导致出作用域以后a和b的引用计数都还是1,所以空间没有被释放
改成使用weak_ptr
{
auto a = make_shared<A>();
auto b = make_shared<B>();
a->b2 = b;//weak_ptr 引用计数不加一
a->Do();//Do函数里引用计数加一,出Do函数作用域减一
cout << "a->b2 = b;b.use_count()=" << b.use_count() << endl;
b->a2 = a;//引用计数不加一
cout << "b->a2 = a;a.use_count()=" << a.use_count() << endl;
}//不会产生循环引用问题
为什么weak_ptr能解决shared_ptr的循环引用问题
weak_ptr 本身不拥有资源所有权:
它只是观察 shared_ptr 管理的对象,不会增加引用计数。
所以a->b2 = b;这句不会增加b的引用计数
同理b->a2 = a;这句也不会增加a的引用计数
但是由于weak_ptr只是一个观察者,无法访问任何资源,仅“观察”资源,不拥有所有权如果想要访问资源该怎么办呢?
如同a->Do();里做的那样
只要原 shared_ptr(即 a)未释放资源,就可以通过 lock() 获取有效的 shared_ptr 并访问。
void Do()
{
cout << "Do b2.use_count() = " << b2.use_count() << endl;
auto b = b2.lock(); //返回一个shared_ptr 引用计数加一
//这里还可以做其他的访问shared_ptr资源的操作
cout << "Do b2.use_count() = " << b2.use_count() << endl;
}//出作用域加上的那个引用计数自动-1
通过weak_ptr.lock()来 返回 一个 shared_ptr 并将引用计数加一
(注意!只有当原shared_ptr对象还存在的时候才会返回shared_ptr,否者返回nullptr)
除了放函数里,还能放判断条件里
if (auto a_shared = b->a_weak.lock()) {
// 返回nullptr 说明shared_ptr被释放
//不会执行此处
} else {
//引用计数+1
std::cout << "A is already destroyed!" << std::endl; // 输出此句
}//引用计数-1
通过这种方式,weak_ptr 可以安全地观察资源,而不会导致循环引用或内存泄漏
make_shared 和 直接 new 一个shared_ptr的区别
区别一:make_shared资源分配更安全
原因和make_unique一样,这里就不再赘述了
区别二:直接new支持自定义删除器
区别三:内存分配方式不同
由于shared_ptr除了维护对象本身的内存以外还要维护一个控制块
- 强引用计数(use_count):记录有多少个 shared_ptr 共享对象
- 弱引用计数(weak_count):记录有多少个weak_ptr 观察对象
- 自定义删除器(如果存在)。
- 对象指针(指向实际分配的对象)。
make_shared 在底层会 一次性分配一块连续内存,既存储对象本身,也存储控制块。
但是new shared_ptr会有两次分配
std::shared_ptr<MyClass> p(new MyClass);
- 第一次分配:new MyClass 分配对象内存。
- 第二次分配:shared_ptr 构造函数内部为控制块分配内存。
为什么make_unique和直接new unique_ptr都是一次分配内存
因为unique_ptr 不需要维护引用计数,因此 没有控制块。无论通过 make_unique 还是直接 new,都只需分配对象内存
shared_ptr合并分配的缺点
对象和控制块内存绑定,即使所有 shared_ptr 销毁,若仍有 weak_ptr 存在,对象内存仍然需等待控制块释放(但析构函数会被及时调用)
shared_ptr控制块内存的延迟释放是内存泄漏吗?
由于make_shared 是一次性分配一块连续内存,同时存储 对象实例 和 控制块(包含引用计数、弱引用计数等)
所以当没有weak_ptr存在时
通过make_shared建立的shared_ptr:
- 对象析构函数立即被调用。
- 整块内存(对象 + 控制块)立即释放。
通过new建立的shared_ptr:
- 对象内存立即释放。
- 控制块内存也立即释放。
当有weak_ptr存在时
通过make_shared建立的shared_ptr:
- 对象析构函数被调用(资源清理)。
- 对象内存和控制块内存暂时保留(直到所有 weak_ptr 也被销毁)。
通过new建立的shared_ptr:
- 对象内存立即释放(仅保留控制块内存)。
由于make_shared对象和控制块内存是连续的,无法单独释放对象内存。所以必须等待所有 weak_ptr 销毁后,整块内存才能一起释放。
那这是内存泄漏吗?
不是! 内存泄漏的定义是:无法再访问且未释放的内存。
而在此时:
对象析构函数已被调用(资源已清理)。
内存仍被 weak_ptr 的控制块管理,虽然未释放,但程序仍能通过 weak_ptr 的机制感知到内存状态。
当所有 weak_ptr 销毁后,内存会被正确释放。