本文已参与「新人创作礼」活动,一起开启掘金创作之路。
本文是我在掘金平台发表于2022年4月份的,从发表最初累计第11篇博客,希望大家关注我,我将会持续在后端和大数据等领域进行书写更多的文章。
摘要
shared_ptr和weak_ptr是一组相近的共享指针,他们都是为了解决自动管理内存的问题而创造的。后者是为了解决前者的循环依赖问题。本文介绍了shared_ptr的一些用法和weak_ptr的用法,并举了一些例子来形象化的展示。
一、shared_ptr学习
1.shared_ptr和weak_ptr 基础概念
- shared_ptr与weak_ptr智能指针均是C++ RAII的一种应用,可用于动态资源管理。
- shared_ptr基于“引用计数”模型实现,多个shared_ptr可指向同一个动态对象,并维护了一个共享的引用计数器,记录了引用同一对象的shared_ptr实例的数量。当最后一个指向动态对象的shared_ptr销毁时,会自动销毁其所指对象(通过delete操作符)。
- shared_ptr的默认能力是管理动态内存,但支持自定义的Deleter以实现个性化的资源释放动作。
- weak_ptr用于解决“引用计数”模型循环依赖问题,weak_ptr指向一个对象,并不增减该对象的引用计数器
RAII要求,资源的有效期与持有资源的对象的生命期严格绑定,即由对象的构造函数完成资源的分配(获取),同时由析构函数完成资源的释放。在这种要求下,只要对象能正确地析构,就不会出现资源泄露问题。
2.shared_ptr的基本用法
#include <memory>
#include <iostream>
struct Foo {
Foo() { std::cout << "Foo...\n"; }
~Foo() { std::cout << "~Foo...\n"; }
};
struct D {
//删除p所指向的Foo对象
// 这个是对象函数化的写法,就是它可以把一个对象当做函数来传参
void operator()(Foo* p) const {
std::cout << "Call delete for Foo object...\n";
delete p;
}
};
int main()
{
// 构建空指针
std::shared_ptr<Foo> sh1;
// 构建一个指向某个对象的指针
std::shared_ptr<Foo> sh2(new Foo);
std::shared_ptr<Foo> sh3(sh2);
std::cout << sh2.use_count() << '\n';
std::cout << sh3.use_count() << '\n';
//指定删除器的智能指针
std::shared_ptr<Foo> sh4(new Foo, D());
}
构造方法:
1.通过make_shared函数构造
auto s_s = make_shared(“hello”);
2.通过原生指针构造
int* pNode = new int(5);
shared_ptr<int> s_int(pNode);
//获取原生指针
int* pOrg = s_int.get();
3.通过赋值函数构造shared_ptr
4.重载的operator->, operator *,以及其他辅助操作如unique()、use_count(), get()等成员方法。
3 实验智能指针引用计数,增加和减少的规律
实验的主要内容有:
1.shared_ptr变量在生命周期中销毁后,引用计数是否减1?
2.shared_ptr作为函数参数,分为传值和传引用,引用计数如何变化?
2.函数返回值为shared_ptr类型时,引用计数是否会变化?
带着这几个问题,我们来看下[例1].
#include <iostream>
#include <memory>
using namespace std;
void Func1(shared_ptr<int> a)
{
cout<<"Enter Func1"<<endl;
cout<<"Ref count: "<<a.use_count()<<endl;
cout<<"Leave Func1"<<endl;
}
shared_ptr<int> Func2(shared_ptr<int>& a)
{
cout<<"Enter Func2"<<endl;
cout<<"Ref count: "<<a.use_count()<<endl;
cout<<"Leave Func2"<<endl;
return a;
}
int main()
{
//构造一个指向int类型对象的指针aObj1,引用计数+1
shared_ptr<int> aObj1(new int(10));
cout<<"Ref count: "<<aObj1.use_count()<<endl;
{
//同aObj1,不过由于生存周期在括号内,所以aObj2离开口号后会被销毁
shared_ptr<int> aObj2 = aObj1; // 调用拷贝构造函数
cout<<"Ref count: "<<aObj2.use_count()<<endl;//引用计数-1
}
//在调用函数时,参数为shared_ptr类型,参数为传值类型,智能指针引用计数+1
Func1(aObj1);
//在调用函数时,参数为shared_ptr类型,参数为传引用类型,智能指针引用计数不变
Func2(aObj1);
shared_ptr<int> aObj3 = Func2(aObj1);//引用计数+1
cout<<"Ref count:"<<aObj3.use_count()<<endl;
return 0;
}
运行结果如下:
Ref count: 1
Ref count: 2
Enter Func1
Ref count: 2
Leave Func1
Enter Func2
Ref count: 1
Leave Func2
Enter Func2
Ref count: 1
Leave Func2
Ref count:2
有效的掌握好智能指针的引用计数的变化规律,才能把程序写的更好.
4. shared_ptr的应用场景以及使用注意事项
4.1 对象之间“共享数据”,对象创建与销毁“分离”
4.2 放入容器中的动态对象,使用shared_ptr包装,比unique_ptr更合适
4.3 管理“动态数组”时,需要制定Deleter以使用delete[]操作符销毁内存,因为shared_ptr并没有针对数组的特化版本(unique_ptr有针对数组的特化版本)
5.shared_ptr的线程安全问题
1. 同一个shared_ptr被多个线程读,是线程安全的;
2. 同一个shared_ptr被多个线程写,不是 线程安全的;
3. 共享引用计数的不同的shared_ptr被多个线程写,是线程安全的。
对于第三点,我们一般采用:
对于线程中传入的外部shared_ptr对象,在线程内部进行一次新的构造,例如: sharedptr AObjTmp =
outerSharedptrObj;
二、weak_ptr学习
我们先搞清楚,weak_ptr为什么出现,或者说它是为了解决什么问题而存在的(存在即合理)。
我们先来观察[例2]:
#include <iostream>
#include <memory>
using namespace std;
class Child;//先声明,否则无法编译通过
class Parent
{
public:
shared_ptr<Child> child;
~Parent(){
cout << "delete Parent" << endl;
}
};
class Child
{
public:
shared_ptr<Parent> parent;
~Child(){
cout << "delete Child" << endl;
}
};
void f1(){
shared_ptr<Parent> pParent(new Parent);
shared_ptr<Child> pChild(new Child);
pParent->child = pChild; // [1]行, 开始纠缠,形成单方绑定,
pChild->parent = pParent; // [2]行, 形成死结,shared_ptr没办法自动释放内存,因为循环引用
}
int main()
{
f1();
return 0;
}
在这个例子中,当我们运行程序的时候发现没有调用Parent实例和Child实例的析构函数,说明这两个实例都没有被程序正确的释放,只有当程序退出后,其内存会被操作系统回收,但是这个时候已经不会执行我们自己的程序了,所以不会调用析构函数。
如果我们自己注释掉[1]或者[2]行,那么我们就可以换查到两个实例被析构。可以自己尝试一下。
在Parent类中存储了指向Child类对象的智能指针成员变量,而在Child类中也存储了指向Parent类对象的智能指针成员变量,如此就会造成环形引用,这个成因在C++中很好解释.
要解决环形引用的问题,没有特别好的办法,一般都是在可能出现环形引用的地方使用weak_ptr来代替shared_ptr。说到了weak_ptr,那下面就接着总结weak_ptr吧。
下面我们来一起学习下weak_ptr这个东东。
weak_ptr指向shared_ptr指针指向的对象的内存,却并不拥有该内存。
但是,使用weak_ptr成员lock,则可返回其指向内存的一个shared_ptr对象,且在所指对象内存已经无效时,返回指针空值(nullptr)。由于weak_ptr是指向shared_ptr所指向的内存的,所以,weak_ptr并不能独立存在。
我们来看看[例3]
#include <iostream>
#include <memory>
using namespace std;
void Check(weak_ptr<int> &wp)
{
shared_ptr<int> sp = wp.lock(); // 重新获得shared_ptr对象
if (sp != nullptr)
{
cout << "The value is " << *sp << endl;
}
else
{
cout << "Pointer is invalid." << endl;
}
}
int main()
{
shared_ptr<int> sp1(new int(10));
shared_ptr<int> sp2 = sp1;
weak_ptr<int> wp = sp1; // 指向sp1所指向的内存
cout << *sp1 << endl;
cout << *sp2 << endl;
Check(wp);
sp1.reset(); // 释放所管理的对象
cout << *sp2 << endl;
Check(wp);
sp2.reset();
Check(wp);
return 0;
}
然后我们在结合[例2]进行改造,生成[例4]
#include <iostream>
#include <memory>
using namespace std;
class Child;//先声明,否则无法编译通过
class Parent
{
public:
// 假设父进程拥有子进程, 而子进程只知道父进程,而不可以释放父进程
// 在进程模型里可以理解,父进程派生和拥有子进程
shared_ptr<Child> child;
~Parent(){
cout << "delete Parent" << endl;
}
};
class Child
{
public:
weak_ptr<Parent> parent; // [1]行
~Child(){
cout << "delete Child" << endl;
}
};
void f1(){
shared_ptr<Parent> pParent(new Parent);
shared_ptr<Child> pChild(new Child);
pParent->child = pChild; //
pChild->parent = pParent; //
}
int main()
{
f1();
return 0;
}
它的结果是
delete Parent // 栈的释放顺序?这里需要调查一下,感觉应该是 Child先被析构,因为它后创建的
delete Child
学习编程最好的方式就是一步步的跟踪去调试。 借鉴上面的代码,我们在使用weak_ptr时也要当心,时刻需要判断weak_ptr对应的shared_ptr是否为空,weak_ptr并不会增加shared_ptr的引用计数.
参考
[1] 下一站_浮华, C++11学习之share_ptr和weak_ptr, blog.csdn.net/wangjingqi9…
[2]维基百科,RAII,zh.wikipedia.org/wiki/RAII