智能指针之shared_ptr初始化,引用计数,常用操作和自定义删除器等等03

一 share_ptr

1 share_ptr基础

  • 1)共享所有权,不是被一个shared_ptr拥有,而是被多个shared_ptr之间相互协作。shared有额外开销。
  • 2)工作原理:利用引用计数的方法管理一片内存,每增加一个shared_ptr,count数加1,同理,每减少一个shared_ptr,count减1;这种引用也称为强引用。
  • 3)最后一个指向该内存对象的shared_ptr在什么情况下,会释放该对象呢?
    1. 这个shared_ptr被析构的时候。
    2. 这个shared_ptr指向其他对象时。

2 shared_ptr初始化

shared_ptr的初始化可以通过裸指针初始化,返回值初始化,和make_shared函数初始化。
1)通过裸指针与返回值初始化

shared_ptr<int> make(int value){
    return shared_ptr<int>(new int(value));
}

void test02(){
    shared_ptr<int> pi1(new int(100)); //裸指针初始化,pi指向一个值为100的int型数据
    shared_ptr<int> pi2 = make(200);//返回值初始化

    cout<< (*pi1.get()) << endl;
    cout<< (*pi2.get()) << endl;
}

int main(){

    test02();

    return 0;
}

2)通过make_shared函数初始化

  • 1)注意,裸指针可以初始化shared_ptr,但是不推荐,智能指针不要穿插用,否则会出问题。所以我们不建议使用的初始化,而是通过make_shared初始化。
  • 2)make_shared是标准库里的函数模板,安全,高效的分配和使用shared_ptr,它能够在动态内存中,分配并初始化一个对象,然后返回指向此对象的shared_ptr。更高效安全的原因:
//1 性能更好(高效)
/*
	 ①性能更好(高效):用new来构造shared_ptr指针,那么new的过程是一次堆上面的内存分配,
	 而在构造shared_ptr对象的时候,由于需要使用堆上面共享的引用计数(指针),又需要在堆上面
	 分配一次内存,即需要分配两次内存,而如果用make_shared函数,则只需分配一次内存,
	 所以性能会好很多。
	 
	 伪代码解释:下面可以看到,ptr传进来已经new一次,然后引用计数_pRefCount也会new一次
	 造成两次分配内存。
*/
template <class T>
class SharedPtr
{
public:
	//
	SharedPtr(T* ptr = nullptr): _ptr(ptr), _pRefCount(new int(1)), _pMutex(new mutex){
		// 如果是一个空指针对象,则引用计数给0
		if (_ptr == nullptr)
			*_pRefCount = 0;
	}

private:
	int*   _pRefCount; // 引用计数
	T*     _ptr;       // 指向管理资源的指针
	mutex* _pMutex;    // 互斥锁
}

//2 更安全
/*
	 ②更加安全:若我们使用裸指针对shared_ptr构造时,包含两步操作:
	(1)new一个堆内存。
	(2)分配一个引用计数区域管理该内存空间(指上面的_pRefCount)。
	 但是并没有保证这两个步骤的原子性,当做了第(1)步,没有做第二步如果程序抛出了异常,将导致内存泄露。
	 而make_shared内部有对这两个步骤合成一步进行处理,
	 因此更推荐使用make_shared来分配内存。

*/

//3 make_shared缺点
/*
	虽然make_shared针对裸指针更好,但它也有缺点。
	③缺点:make_shared一次性分配堆内存的做法,在释放的时候可能会导致内存延迟释放,
	因为如果有weak_ptr持有了指针,引用计数不会释放,而引用计数和实际的对象分配在同一块堆内存,
	因此无法将该对象释放,如果两块内存分开申请,则不存在这个延迟释放的问题。
*/

//4 总结
/*
	实际开发可能不会考虑这些,使用裸指针或者make_shared都行,但是大家一定要知道它们可能存在的问题。
*/

在上面介绍为何使用make_shared顶替裸指针初始化后,下面正式说明make_shared如何初始化shared_ptr。

#include <iostream>
#include <string>
#include <memory>
using namespace std;

shared_ptr<int> make(int value){
    return shared_ptr<int>(new int(value));
}

void test02(){
    // shared_ptr<int> pi1(new int(100)); //pi指向一个值为100的int型数据
    // shared_ptr<int> pi2 = make(200);
    // cout<< (*pi1.get()) << endl;
    // cout<< (*pi2.get()) << endl;

    shared_ptr<int> p1 = make_shared<int>(100);
    shared_ptr<string> p2 = make_shared<string>(5, 'a'); //类似于string mystr(5, 'a')
    shared_ptr<int> p3 = make_shared<int>();//默认初值为0,cout<< (*p3.get()) << endl;
    p3 = make_shared<int>(300); //指向新int,p3首先释放指向值为0的内存,然后指向这个新的300的内存
    auto p4 = make_shared<string>("I love you");

    cout<< (*p1.get()) << endl;
    cout<< p2.get()->data() << endl;
    cout<< (*p3.get()) << endl;
    cout<< p4.get()->data() << endl;
}

int main(){

    //test01();
    test02();

    return 0;
}

结果:
在这里插入图片描述

3)注意,shared_ptr不支持隐式转换。

//例1
shared_ptr<int> pi2 = new int(200); //不可以,智能指针是explicit,不可以进行隐式类型转换,必须直接初始化形式

//例2
shared_ptr<int> make(int value){
    return new int(value);   //error,无法把new得到的int *转换成shared_ptr
    return shared_ptr<int>(new int(value))//正确写法
}

3 shared_ptr的引用计数

引用计数就是多个shared_ptr共用一片内存的shared_ptr个数。当一个shared_ptr开辟一个内存,引用计数为1,然后用该shared_ptr初始化其它shared_ptr那么就是共用一个内存,引用计数为2,以此类推。而当一个shared_ptr指向其它shared_ptr的内存,或者shared_ptr生命周期结束被析构时,引用计数都会减少。

#include <iostream>
#include <string>
#include <memory>
using namespace std;

void Func1(shared_ptr<int> a)
{
    cout<<"函数1内:"<<endl;
    cout<<"值的引用数为: "<<a.use_count()<<endl;          // 2 调用完毕a被释放 引用计数减1
    cout<<"函数1结束。"<<endl;
}

shared_ptr<int> Func2(shared_ptr<int>& a)
{
    cout<<"函数2内:"<<endl;
    cout<<"引用的引用数为: "<<a.use_count()<<endl;         // 1引用不会增加引用计数
    cout<<"函数2结束。"<<endl;

    return a;
}

//主要测试参数为值,引用和返回值对引用计数的影响
void test03(){
    
    shared_ptr<int> sh1(new int(10));                  // 构造一个指向int类型对象的指针sh1,引用计数为1
    cout<<"Ref count: "<< sh1.use_count() << endl;

    {
        shared_ptr<int> sh2 = sh1;                     
        cout<<"Ref count: "<< sh1.use_count() << endl;
    }
    //sh2生命周期结束后
    cout<<"Ref count: "<< sh1.use_count() << endl;
    cout<<endl;

    //1 测试参数为值
    Func1(sh1);
	cout<<endl;
    
    //2 测试参数为引用
    Func2(sh1);
	cout<<endl;

    //3 测试接收匿名对象
    shared_ptr<int> sh3 = Func2(sh1);
    cout<<"sh3加入之后的引用数:"<<sh3.use_count()<<endl;

}

//测试引用计数增加减少
void test04(){
    auto p1 = make_shared<int>(100);
    auto p2(p1);
    auto p3 = Func2(p2);
    cout<<"Ref count: "<< p1.use_count() << endl;
    p3 = make_shared<int>(200); //p3指向新对象,计数为1,p1、p2指向对象计数恢复为2;
    p2 = make_shared<int>(300); //p2指向新对象,计数为1,p1指向对象的计数恢复为1;
    cout<<"p3,p2改变指向后,Ref count: "<< p1.use_count() << endl;

    {
        shared_ptr<int> p4 = p1;                     
        cout<<"Ref count: "<< p1.use_count() << endl;
    }
    cout<<"p4生命周期结束后,Ref count: "<< p1.use_count() << endl;
}

int main(){

    //test01();
    //test02();
    test03();

    cout<<endl;
    cout<<"====test04 begin===="<<endl;

    test04();

    return 0;
}

结果:
在这里插入图片描述

从上面结果可以得出:

  • 1)传值会使引用计数加1,传引用则不会。
  • 2)当返回值为shared_ptr类型,若我们使用变量接收,则引用计数会加1,不接收则不加。归根结底是因为返回值类型时,编译器会自动创建一个匿名对象返回。例如Func2(sh1);与shared_ptr sh3 = Func2(sh1);。
  • 3)使用同一片内存为其它shared_ptr赋值,会使引用计数加1。例如sh2 = sh1或者sh2(sh1);

上面是针对于会使引用计数增加的总结。下面总结引用计数减少的。

  • 1)改变指向其它内存的shared_ptr会减1。
  • 2)生命周期结束的shared_ptr会减1。当最终引用计数减为0时,就会释放该内存。

4 shared_ptr的其它成员函数

4.1 use_count()
use_count()是获取该片内存有多个shared_ptr个对象正在引用。

4.2 unique()
unique:是否该智能指针独占某个指向的对象,也就是若只有一个智能指针指向某个对象,则unique()返回true,多个返回fasle。(为空时也不属于独享)

4.3 reset()与shared_ptr的比较
reset()函数分为无参与有参的使用。

  • 1)无参时,调用该函数的shared_ptr对象的引用计数为0,而该片内存并不为0,只是减1。
  • 2)有参时,调用该函数的shared_ptr对象指向新的内存,引用计数加1。而原本的内存减1。
  • 3)由于reset被置空后,一般需要比较,所以将shared_ptr的比较放在reset一起,当然也可以使用use_count代替比较来判断是否被置空。
  • 4)下面例子可以看到,空指针也可以通过reset重新初始化。即sh1。

看例子。

//shared_ptr的比较 与 reset函数方法
void test05(){

	shared_ptr<int> sh1=make_shared<int>(3);                 
	cout<<sh1.use_count()<<endl;                             // count=1

	shared_ptr<int> sh3=sh1;                                 
	cout<<sh1.use_count()<<endl;                             // count=2

	//1 比较 重载了==与!=运算符  实际上当成指针比较就好了
	if(sh1!=NULL && sh3!=NULL){
		cout<<"sh1和sh3不为空!"<<endl;
	}

	//2.1 无参reset函数  使调用的shared_ptr指向一个空资源 只是该shared_ptr的引用计数变为0 其他的因为sh1调用reset而减1 
	sh1.reset();                                             // 使sh1指向的count为0
	cout<<sh1.use_count()<<endl;                             // count=0
    if (sh1 == nullptr){
        cout << "sh1被置空" << endl;
    }
	cout<<sh3.use_count()<<endl;                             // count=1

	//2.2 有参reset 使shared_ptr指向参数new出的资源对象
	sh1.reset(new int(5));                                   // sh1指向该参数new出的资源
	cout<<sh1.use_count()<<endl;                             // count=1

}

结果:
在这里插入图片描述

4.4 解引用的意思
解引用就是获取该裸指针的对象。
例如。

shared_ptr<int> p(new int(123));
cout << *p << endl;

4.5 get()
get():获取裸指针操作。考虑到有些函数的参数需要的是一个内置裸指针,而不是智能指针。例如上面的初始化使用过get函数。

4.6 swap()
swap():交换两个智能指针所指向的对象。

shared_ptr<string> ps1(new string("1111111"));
shared_ptr<string> ps2(new string("2222222"));
std::swap(ps1, ps2); //交换ps1指向222222222
ps1.swap(ps2);    //在交换ps1指向11111111

5 自定义删除回调函数(删除器)

分析:当传给shared_ptr构造函数不止一个对象时,例如是一个对象数组时,因为shared_ptr析构默认用的是delete a,只能delete掉一个对象,所以我们需要自定义回调函数,用于析构传进的对象数组。

5.1 使用函数模板作为自定义删除器

//用来释放malloc出来的函数对象
template<class T>
class FreeFunc{
public:
	void operator()(T* ptr)
	{
		cout << "free:" << ptr << endl;
		free(ptr);
	}
};

//用来释放new[]出来的函数对象
template<class T>
class DeleteArrayFunc {
public:
	void operator()(T* ptr)
	{
		cout << "delete[]" << ptr << endl;
		delete[] ptr;
	}
};

//用来释放文件描述符的函数对象
template<class T>
class ClosefdFunc{
public:
	void operator()(T* fd)
	{
		cout << "close fd" << fd << endl;
		fclose(fd);
	}
};

void test06(){

	FreeFunc<int>        Object1;
	shared_ptr<int>      sp1((int*)malloc(sizeof(int)*4), Object1);         // 回调函数是可调用对象,可以是普通的函数名或者函数对象或者lambda表达式

	DeleteArrayFunc<int> Object2;
	shared_ptr<int>      sp2(new int[4], Object2);

	ClosefdFunc<FILE>    Object3;
	shared_ptr<FILE>     sp3(fopen("myfile.txt","w"), Object3);

}

结果看到,三个自定义析构函数均被调用,并且可以发现文件对象会被编译器优先释放。
在这里插入图片描述

5.2 使用其它新特性作为删除器

  • 1)可以用default_delete来做删除器,这个是标准库里的模板类。
class A {
public:
    A() {};
    ~A() {};
};
 
void test07(){
    shared_ptr<A> p(new A[10], std::default_delete<A[]>());
}
  • 2)可以用C++新规则的简单写法。
void test08(){
    shared_ptr<A[]> p1(new A[10]);//A类在上面
    shared_ptr<int[]> p2(new int[10]);
    p2[0] = 12;
    p2[1] = 15;

    cout<<p2[0]<<endl;//输出12
    cout<<p2[1]<<endl;//输出15
}

5.3 模板+lambda表达式实现开发时常用的删除器
由于我们开发时很少使用到像5.1这种模板,因为比较长,且需要额外定义对象传入,所以我们开发时更倾向使用lambda表达式,代码更少。但是并不是不能使用5.1这种方法。

  • 1)不带删除数组的模板+lambda表达式删除器。
template <class T>
shared_ptr<T> my_make_shared(){
    return shared_ptr<T>(new T,[](T *ptr){delete ptr,ptr=nullptr;});
}

void test09(){
    //对于类中没有成员的,只有使用裸指针
    shared_ptr<int> sh1 = my_make_shared<int>();
    *sh1.get() = 2;
    cout<<*sh1.get()<<endl;

    //对于类中有成员的,可以直接使用智能指针
    shared_ptr<A> sh2 = my_make_shared<A>();
    sh2->SetI(100);
    cout<<sh2->GetI()<<endl;
}
  • 2)带删除数组的模板+lambda表达式删除器。下面我将lambda表达式换成default_delete函数,因为上面已经有lambda,当然你也可以换一下。
template<typename T>
shared_ptr<T> my_make_shared_array(size_t size)
{
    return shared_ptr<T>(new T[size], default_delete<T[]>());
}
void test10(){
    shared_ptr<A> parray = my_make_shared_array<A>(15);
    parray->SetI(200);
    //parray.get()[0].SetI(100);//也行,但直接使用智能指针更安全
    cout<<parray.get()[0].GetI()<<endl;//输出200
}

5.4 指定删除器额外说明
就算是两个shared_ptr指定了不同的删除器,只要他们所指向的对象类型相同,那么这两个shared_ptr也是属于同一类型,所以它们可以改变指向,改变指向后,若引用计数变为0,则先用就的删除器删除内存,然后它将使用新的删除器。
例如。

auto lambda1 = [](int *p)
{
    delete p;
};
 
auto lambda2 = [](int *p)
{
    delete p;
};
 
 
shared_ptr<int> p1(new int(100), lambda1);
shared_ptr<int> p2(new int(100), lambda2);
p2 = p1; //p2会先调用lambda2把自己所指向的对象释放,然后指向p1所指向的对象。p1所指向 
         //的对象引用计数为2,整个main执行完成后还会调用lambda1来释放p1,p2共同指向的 
         //对象

并且注意,我们5.3常用删除迭代器为何不使用make_shared,因为使用make_shared这种方法我们无法指定自己的删除器。所以只能使用new。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值