[C++11新特性] weak_ptr和unique_ptr
一、weak_ptr弱引用的智能指针
1.1 shared_ptr相互引用会有什么后果?
shared_ptr
的一个最大的陷阱是循环引用,循环引用会导致堆内存无法正确释放,导致内存泄漏。看下面的例子:
#include <iostream>
#include <memory>
class Parent; // Parent类的前置声明
class Child {
public:
Child() { std::cout << "hello child" << std::endl; }
~Child() { std::cout << "bye child" << std::endl; }
std::shared_ptr<Parent> father;
};
class Parent {
public:
Parent() { std::cout << "hello Parent" << std::endl; }
~Parent() { std::cout << "bye parent" << std::endl; }
std::shared_ptr<Child> son;
};
void testParentAndChild() {
}
int main() {
std::shared_ptr<Parent> parent(new Parent()); // 1 资源A
std::shared_ptr<Child> child(new Child()); // 2 资源B
parent->son = child; // 3 child.use_count() == 2 and parent.use_count() == 1
child->father = parent; // 4 child.use_count() == 2 and parent.use_count() == 2
return 0;
}
/*
输出:
hello Parent
hello child
*/
很惊讶的发现,用了shared_ptr
管理资源,没有调用 Parent 和 Child 的析构函数,表示资源最后还是没有释放!内存泄漏还是发生了。
分析:
- 执行编号
1
的语句时,构造了一个共享智能指针p
,称呼它管理的资源叫做资源A
(new Parent()
产生的对象)吧, 语句2
构造了一个共享智能指针c
,管理资源B
(new Child()
产生的对象),此时资源A
和B
的引用计数都是1
,因为只有1
个智能指针管理它们,执行到了语句3
的时候,是一个智能指针的赋值操作,资源B
的引用计数变为了2
,同理,执行完语句4
,资源A
的引用计数也变成了2
。 - 出了函数作用域时,由于析构和构造的顺序是相反的,会先析构共享智能指针
c
,资源B
的引用计数就变成了1
;接下来继续析构共享智能指针p
,资源A
的引用计数也变成了1
。由于资源A
和B
的引用计数都不为1
,说明还有共享智能指针在使用着它们,所以不会调用资源的析构函数! - 这种情况就是个死循环,如果资源
A
的引用计数想变成0
,则必须资源B
先析构掉(从而析构掉内部管理资源A
的共享智能指针),资源B
的引用计数想变为0
,又得依赖资源A
的析构,这样就陷入了一个死循环。
1.2 weak_ptr如何解决相互引用的问题
要想解决上面循环引用的问题,只能引入新的智能指针std::weak_ptr
。std::weak_ptr
有什么特点呢?与std::shared_ptr
最大的差别是在赋值的时候,不会引起智能指针计数增加。
weak_ptr
被设计为与shared_ptr
共同工作,可以从一个shared_ptr
或者另一个weak_ptr
对象构造,获得资源的观测权。但weak_ptr
没有共享资源,它的构造不会引起指针引用计数的增加。- 同样,在
weak_ptr
析构时也不会导致引用计数的减少,它只是一个静静地观察者。weak_ptr
没有重载operator*
和->
,这是特意的,因为它不共享指针,不能操作资源,这是它弱的原因。 - 如要操作资源,则必须使用一个非常重要的成员函数
lock()
从被观测的shared_ptr
获得一个可用的shared_ptr
对象,从而操作资源。
当我们创建一个weak_ptr
时,要用一个shared_ptr
来初始化它:
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp弱共享p; p的引用计数未改变
我们在上面的代码基础上使用std::weak_ptr
进行修改,如下:
#include <iostream>
#include <memory>
class Parent; // Parent类的前置声明
class Child {
public:
Child() { std::cout << "hello child" << std::endl; }
~Child() { std::cout << "bye child" << std::endl; }
// 测试函数
void testWork()
{
std::cout << "testWork()" << std::endl;
}
std::weak_ptr<Parent> father;
};
class Parent {
public:
Parent() { std::cout << "hello Parent" << std::endl; }
~Parent() { std::cout << "bye parent" << std::endl; }
std::weak_ptr<Child> son;
};
void testParentAndChild() {
}
int main() {
std::shared_ptr<Parent> parent(new Parent());
std::shared_ptr<Child> child(new Child());
parent->son = child;
child->father = parent;
std::cout << "parent_ref:" << parent.use_count() << std::endl;
std::cout << "child_ref:" << child.use_count() << std::endl;
// 把std::weak_ptr类型转换成std::shared_ptr类型,以调用内部成员函数
std::shared_ptr<Child> tmp = parent.get()->son.lock();
tmp->testWork();
std::cout << "tmp_ref:" << tmp.use_count() << std::endl;
return 0;
}
/*
输出:
hello Parent
hello child
parent_ref:1
child_ref:1
testWork()
tmp_ref:2
bye child
bye parent
*/
由以上代码运行结果我们可以看到:
- 所有的对象最后都能正常释放,不会存在上一个例子中的内存没有释放的问题;
- parent 和 child 在 main 函数中退出前,引用计数均为 1,也就是说,对
std::weak_ptr
的相互引用,不会导致计数的增加。
1.3 weak_ptr常用操作
weak_ptr<T> w; // 空weak_ptr可以指向类型为T的对象
weak_ptr<T> w(shared_ptr p); // 与p指向相同对象的weak_ptr, T必须能转换为sp指向的类型
w = p; // p可以是shared_ptr或者weak_ptr,赋值后w和p共享对象
w.reset(); // weak_ptr置为空
w.use_count(); // 与w共享对象的shared_ptr的计数
w.expired(); // w.use_count()为0则返回true,否则返回false
w.lock(); // w.expired()为true,返回空的shared_ptr;否则返回指向w的shared_ptr
二、unique_ptr独占的智能指针
2.1 unique_ptr的基本使用
unique_ptr
相对于其他两个智能指针更加简单,它和shared_ptr
使用差不多,但是功能更为单一,它是一个独占型的智能指针,不允许其他的智能指针共享其内部的指针,更像原生的指针(但更为安全,能够自己释放内存)。不允许赋值和拷贝操作,只能够移动。
std::unique_ptr<int> ptr1(new int(0));
std::unique_ptr<int> ptr2 = ptr1; // 错误,不能复制
std::unique_ptr<int> ptr3 = std::move(ptr1); // 可以移动
在 C++11 中,没有类似std::make_shared
的初始化方法,但是在 C++14 中,对于std::unique_ptr
引入了std::make_unique
方法进行初始化。
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<std::string> ptr1(new std::string("unique_ptr"));
std::cout << "ptr1 is " << *ptr1 << std::endl;
std::unique_ptr<std::string> ptr2 = std::make_unique<std::string>("make_unique init!");
std::cout << "ptr2 is " << *ptr2 << std::endl;
return 0;
}
/*
输出:
ptr1 is unique_ptr
ptr2 is make_unique init!
*/
2.2 unique_ptr常用操作
下面列出了unique_ptr
特有的操作。
unique_ptr<T> u1 // 空unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针
unique_ptr<T, D> u2 // u2会使用一个类型为D的可调用对象来释放它的指针
unique_ptr<T, D> u(d) // 空unique_ptr,指向类型为T的对象,用类型为D的对象d替代delete
u = nullptr // 释放u指向的对象,将u置为空
u.release() // u放弃对指针的控制权,返回指针,并将u置为空
u.reset() // 释放u指向的对象
u.reset(q) // 如果提供了内置指针q,另u指向这个对象;否则将u置为空
u.reset(nullptr)
虽然我们不能拷贝或赋值unique_ptr
,但可以通过调用 release 或 reset 将指针的所有权从一个(非const)unique_ptr
转移给另一个unique_ptr
:
unique_ptr<string> p1(new string("Stegosaurus"));
// 将所有权从pl (指向string Stegosaurus)转移给p2
unique_ptr<string> p2(p1, release()); // release 将 p1 置为空
unique_ptr<string> p3(new string("Trex"));
// 将所有权从p3转移给p2
p2.reset(p3.release()); // reset 释放了 p2 原来指向的内存
调用 release 会切断unique_ptr
和它原来管理的对象间的联系,如果我们不用另一个智能指针来保存 release 返回的指针,我们的程序就要负责资源的释放:
p2.release(); // 错误:p2不会释放内存,而且我们丢失了指针
auto p = p2.release(); // 正确,但我们必须记得 delete(p)
delete(p);
2.3 传递unique_ptr参数和返回unique_ptr
不能拷贝 unique_ptr 的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的 unique_ptr。最常见的例子是从函数返回一个unique_ptr
:
unique_ptr<int> clone (int p)
{
unique_ptr<int> ret(new int (p));
// ...
return ret;
}
对于上面这段代码,编译器都知道要返回的对象将要被销毁。在此情况下,编译器执行一种特殊的“拷贝”,在《C++ Primer》13.6.2节(第473页)中有介绍。
三、性能与安全的权衡
使用智能指针虽然能够解决内存泄漏问题,但是也付出了一定的代价。以shared_ptr
举例:
shared_ptr
的大小是原始指针的两倍,因为它的内部有一个原始指针指向资源,同时有个指针指向引用计数。- 引用计数的内存必须动态分配。虽然一点可以使用
make_shared()
来避免,但也存在一些情况下不能够使用make_shared()
。 - 增加和减小引用计数必须是原子操作,因为可能会有读写操作在不同的线程中同时发生。比如在一个线程里有一个指向一块资源的
shared_ptr
可能调用了析构(因此所指向的资源的引用计数减一),同时,在另一线程里,指向相同对象的一个shared_ptr
可能执行了拷贝操作(因此,引用计数加一)。原子操作一般会比非原子操作慢。但是为了线程安全,又不得不这么做,这就给单线程使用环境带来了不必要的困扰。
我觉得还是分场合吧,看应用场景来进行权衡,我也没啥经验,但我感觉安全更重要,现在硬件已经足够快了,其他例如java
这种支持垃圾回收的语言不还是用的很好吗。
参考:
《C++ Primer 第5版》
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!