C++智能指针

C++智能指针

主要分为三个:shared_ptr、unique_ptr、weak_ptr,他们主要是为了减少因为各种原因忘记导致释放资源的问题。

shared_ptr

// shared_ptr的创建,shared_ptr目的是共享一个指针(或者说一块地址),我们创建的形参要是一个指针类型的数据
// 首先明确一点:shared_ptr的构造函数时有explicit修饰的,代表其在调用构造函数时不能实参是不能
// 进行隐式转换的,形参什么类型实参就是什么类型。
// shared_ptr<int> p = new int(10);这种写法时错误的,他是不能隐式的将int类型的指针转换成shared_ptr类型的。
// 定义时不初始化
shared_ptr<int> p;
p = shared_ptr<int>(new int(10);
// 定义时初始化
shared_ptr<int> p1(new int(10));
shared_ptr<int> p2(p1);
shared_ptr<int> p3 = shared_ptr<int>(new int(10));
// 还有一个知识点就是,new一个指针和make_shared两种方式的区别
shared_prt<int> p4 = shared_ptr<int>(new int(10));
shared_ptr<int> p5 = make_shared<int>(10);
// 我们知道shared_ptr中有两个指针,一个指向数据(数据块),一个指向引用计数(控制块),
// 如果我们采用第一种方法,会进行两次内存申请,首先会在堆内申请数据块的内存也就是new int(10)这步,
// 然后再去申请shared_ptr的内存,将两个指针初始化,因此这里我们需要传进去一个指针,因为数据块的
// 内存地址是由new返回的,shared_ptr只会申请控制块的内存,且前面说了shared_ptr的构造函数是带有explicit的,
// 不能进行隐式转换,只能返回一个指针,而不能给个值让shared_ptr申请内存后返回地址看代码也能看出一些。

// 第二种会同时为两个块申请一个足够大的内存,一次申请数据块和控制块的内存,然后直接初始化,这里也能
// 看出,它不需要传入指针,他会申请足够大的内存,分配给两个块,因此需要传入一个值,让他去分配内存地址,
// 有点类似vector中的push_back和emplace_back()之间的区别(一个是创建后复制进去,一个是再内存中调用构造函数创建,不用复制进去)

// shared_ptr的使用
int a = 10;
shared_ptr<int> ptra = make_shared<int>(a);
shared_ptr<int> ptra2 = make_shared<int>(a);
cout << ptra.use_count() << endl;	// 输出2,ptra指向的的a这块内存,这块内存被引用两次,因此输出2;

int b = 20;
int *pb = &a;
shared_ptr<int> ptrb = make_shared<int>(*pb);
cout << ptrb.use_count() << endl;	
// 输出1,而不是3,因为ptrb是指向pa的内存,pa指向的内存保存的是a的地址,而不是a保存的地址,
// 他们的关系是 pa -> a -> 10, 因此输出的是1.
cout << ptra.use_count() << endl;	// 输出2, 没变指向a这块地址的智能指针没有增加
cout << ptra2.use_count() << endl;	// 输出2
shared_ptr<int> ptrb2 = make_shared<int>(b);
// 重新指向一个新内存
ptra2 = ptrb2; 	// ptra2重新赋值,指向*pa
pb = ptrb2.get();
cout << ptrb.use_count() << endl; 	// 输出1,还是没变
cout << ptrb2.use_count() << endl; 	// 输出2, ptra2指向了ptrb2指向的内存,引用计数加一
cout << ptra.use_count() << endl;	// 输出1, ptra2变了,只有ptra指向了a
cout << ptra2.use_count() << endl;	// 输出2, 和ptrb2指向同一内存

// shared_ptr还可以自定义删除器,它的作用是当你使用自定义类型时,普通的delete将无法完成它原有的作用时,
// 使用自定义删除器代替delete完成释放内存的作用,再调用删除器后再释放shared_ptr的内存。因此使用自定义类
// 型且智能指针指向它时,可以自定义删除器,使得内存得以正常释放
// 方法实在调用shared_ptr的构造函数时,在参数列表中添加,但是在调用make_shared时不能传入,举个例子(相关数据结构在下面):
// shared_ptr<Connection> uptr2 = make_shared<Connection>("abcd", close);// 这里close时被认为传给Connection构造函数的第二个参数,而不是被识别成删除器。
// 有几种方法,可以参考这篇博客:https://blog.csdn.net/DumpDoctorWang/article/details/88598015

// 方法一,调用普通函数
class Connection {
public:
    explicit Connection(string name) :_name(name) {
    }
    string get_name() const {
        return _name;
    }
private:
    string _name;
};
// 声明删除器
// 删除器的返回的类型必须是void,因为delete本身就是没有返回类型的
// 其次,需要传入一个对应类型,也就是shared_ptr指向的类型指针,
// 这里shared_ptr的是Connection*,因此这里也需要传入Connection*
void close(Connection* connection) {
    cout << string("关闭") + connection->get_name() + "管理的连接中..." << endl;
    //关闭连接的代码
    // .....
    cout << "关闭完成。" << endl;
}

// 使用方法(在main中调用):
Connection c("abc");
shared_ptr<Connection> sptr(&c,close);

// 方法二:使用可调用的类或者struct结构体
struct del {
public:
    void operator()(int* ptr) {
        cout << "调用删除器" << endl;
        delete ptr;
        ptr = nullptr;
    }
};
// 使用方法(在main中调用):
// 这里传入的是del类型的匿名对象,del()调用了默认构造函数生成了一个del对象
shared_ptr<int> stpr2(new int(10), del());

//方法三:使用lambda表达式
shared_ptr<int> utpr3 (new int(20), [](int* p) -> void {
        cout << "调用删除器" << endl;
        delete p;
        p = nullptr;
    });
// 还可以在外面定义lambda表达式
auto f = [](int* p) {
    cout << "调用删除器" << endl;
    delete p;
    p = nullptr;
};
shared_ptr<int> stpr3(new int(20), f);

//方法四:使用std::function
// 使用function需要包含头文件#include <functional>
// 模板类型时函数类型(函数类型的构成有:返回类型,参数类型,参数个数)
// void close(Connection* connection)的签名就是void (Connection*),close函数在上面定义过了
std::function<void(Connection*)> func_close(close);
shared_ptr<Connection> stpr4(new Connection("abcde"), func_close);
// functoin和lambda相结合也可
function<void(int*)> func2_close([](int* p) -> void {
    cout << "调用删除器" << endl;
    delete p;
    p = nullptr;
    });
shared_ptr<Connection> stpr5(new Connection("abcde"), func_close);

// 易错点:不要智能指针和普通指针混合使用
int func(shared_ptr<int> ptr);
int* x = new int(1024);
func(shared_ptr<int>(x));   // 此时当函数运行完成,传进去的智能指针会进行销毁,导致x指针指向的地址失效
// int* y = x;              // 错误, x已被销毁

// 易错点:不要使用get函数初始化另一个智能指针或者尾智能指针赋值
shared_ptr<int> ptr (new int(10))
void func2(int* p);
shared_ptr<int> ptr(new int(10));
int* p = ptr.get();
func2(p);		// 执行完后p指向的地址的值会被销毁
int foo = *p;	// 这里foo的值会是一些乱七八糟的值,因为p的值已被销毁

// 可以再main定义一个函数
void func2(int * p) {
    shared_ptr<int> ptr(p);
}

unique_ptr

// unique_ptr拥有它指向的对象,强调唯一的所有权,当他占有这块内存时,其他unique_ptr是不能占有这块内存的,
// 直接表现就是无法通过拷贝构造函数和拷贝赋值运算符去拷贝另一个unique_ptr,当然如何你直接回避unique_ptr,直接通过指针,那就没办法了,但是这样会出现重复销毁指针的错误
int* p = new int(10);
unique_ptr<int> uptr(p);
unique_ptr<int> uptr2(p);
// 这段代码编译能通过,但是运行完,开始销毁时就会出现错误

// 先说unique_ptr的创建,他的构造函数和shared_ptr相同,是由explicit修饰的,需要传入一个指针
unique_ptr<int> uptr3 = unique_ptr<int>(new int(10));	//unique_ptr<int> uptr3(new int(10));相同的效果

// 前面说了unique_ptr强调所有权,他还能将所有权转交给其他uniue_ptr,但之后这个指针就失去了这块内存的所有权,所有权具有唯一性,unique_ptr转交所有权的方法就是调用release方法
unique_ptr<int> uptr4 = unique_ptr<int>(new int(10));	
unique_ptr<int> uptr5 (uptr4.release());		// 此时uptr4指向的地址所有权转交给uptr5,它自己则指向空

// unique_ptr还可以放弃当前指向的内存,而指向另一块内存,unique_ptr通过reset函数完成
int* q = new int(20);
uptr5.reset(q);		
uptr5.reset(new int(30));

// unique_ptr还可以单方面放弃指针,有几种方式可以实现
unique_ptr<int> uptr6 = unique_ptr<int>(new int(10));
unique_ptr<int> uptr7 = unique_ptr<int>(new int(20));
unique_ptr<int> uptr8 = unique_ptr<int>(new int(10));
unique_ptr<int> uptr9 = unique_ptr<int>(new int(10));

uptr6 = nullptr;
uptr7.release();	// 释放指针所有权也可以达到置为空的目的
uptr8.reset();		
uptr9.reset(nullptr);

// 最后,unique_ptr也能自定义删除器,也是上面和shared_ptr相同的四种方法,不同的在于unique_ptr需要
// 在模板参数中声明删除器的类型,相关数据结构和上面的shared_ptr相同,这里只放关键代码

//方式一:调用普通函数
void close(Connection* connection) {
    cout << string("关闭") + connection->get_name() + "管理的连接中..." << endl;
    //关闭连接的代码
    // .....
    cout << "关闭完成。" << endl;
}
// 传入的是函数,因此我们使用关键字decltype得出close的函数类型,在加上指针,则可以形成函数指针,这里为:void*
// 函数指针的内容可以看这篇博客https://blog.csdn.net/A_easy_learner/article/details/127595504,也可以直接看书
unique_ptr<Connection, decltype(close)*> uptr(new Connection("abc"), close);

// 方式二:使用可调用的类或者struct结构体
struct del {
public:
    void operator()(int* ptr) {
        cout << "调用删除器" << endl;
        delete ptr;
        ptr = nullptr;
    }
};
// 因为传入的是结构体,del()的仿函数提供了delete的功能,而它的类型就是结构体本身
unique_ptr<int, del> uptr2(new int(10), del());

// 方式三:使用lambda表达式
auto f = [](int* p) {
        cout << "调用删除器" << endl;
        delete p;
        p = nullptr;
    };
// 使用lambda时不能使用匿名函数,否则无法找到函数指针类型,因此需要先定义lambda函数
// 首先,我们得搞清上面这个东西是什么,如果看过函数指针和lambda的就能知道,f其实是一个函数指针,因为后面的
// lambda是一个函数,auto根据表达式得出f的类型就是函数指针,因此我们用decltype(f)返回的类型就是函数指针,
// 因此无需再后面加上*。
unique_ptr<int, decltype(f)> utpr4(new int(20), f);

// 方式四:
std::function<void(Connection*)> func_close(close);
// 直接由decltype推算出func_close的类型,也就是function<void(Connection*)>
unique_ptr<Connection, decltype(func_close)> uptr5(new Connection("abcde"), func_close);

weak_ptr

weak_ptr是一种不控制所指向对象的智能指针,不能单独去绑定对象,但可以指向shared_ptr管理的对象。将weak_ptr绑定到一个shared_ptr是不会改变shared_ptr的引用计数的,而且当shared_ptr引用计数为0时,虽然此时weak_ptr还在指向他管理的对象,但还是会被销毁。突出了一个weak。(有点像海王和他池塘里的鱼儿们的故事)

// 还是先说weak_ptr的创建
shared_ptr<int> sptr(new int(10));
weak_ptr<int> wptr;		// 定义未初始化
wptr = sptr;
weak_ptr<int> wptr2(sptr);	// 定义且初始化

// 更换指向的shared_ptr对象
shared_ptr<int> sptr2 = make_shared<int>(20);
wptr = sptr2;

// 将weak_ptr置为空
wptr.reset();

// 查看shared_ptr的引用计数
cout << wptr2.use_count() << endl;	// 输出:1

// 查看shared_ptr指向的对象是否还存在,也就是shared_ptr是否还存在,通过use_count判断shared_ptr的引用计数
// 若是返回0则表示shared_ptr指向的对象不存在,否则为存在。
cout << wptr.expired() << endl;		<<输出:1,为false,表示shared_ptr指向的对象不存在
cout << wptr2.expired() << endl;	<<输出:0,为true,表示shared_ptr指向的对象还存在

// 直接获取shared_ptr对象,有则返回,没有则返回nullptr
// 如果expired为false,返回wptr2指向的shared_ptr指向的对象的shared_ptr对象,
// 也就是那个shared_ptr拷贝过来的shared_ptr
shared_ptr<int> test1 = wptr2.lock();	
// 如果expired为true,返回指向nullptr的shared_ptr
shared_ptr<int> test2 = wptr.lock();	

// use_count可用于expired的判断,而expired可用于lock的判断
use_cout() -> expired() -> lock()

weak_ptr和shared_ptr的关系

通常weak_ptr用来和shared_ptr搭档,weak_ptr用于检测shared_ptr指向的数据的存在性,避免访问不存在的数据,导致程序出错(使用expired函数);还可以通过lock函数赋值一份shared_ptr出来使用。

shared_ptr的结构

在典型的实现中,shared_ptr只保有两个指针:

  • get()所返回的指针
  • 只想控制块的指针

控制块是动态分配的一个对象,其中包含:

  • 指向被管理对象的指针或被管理对象本身
  • 删除器(类型擦除)
  • 分配器(类型擦除)
  • 占有被管理对象的 shared_ptr 的数量
  • 涉及被管理对象的 weak_ptr 的数量

前面也有提到相关的内容,还记得make_shared和构造器这两种构造函数的区别吗?前者是只分配一次内存,内存大小课容纳这两个指针,后者是先构造shared_ptr指向的对象的指针,其次在构造控制块的指针,分配两次内存;前者数据部分和控制块部分是一个整体,后者这是分开的。

weak_ptr和shared_ptr的销毁之间的那点事

我们知道,weak_ptr是通过shared_ptr的引用计数进行各种操作的,因此他是能控制shared_ptr的控制块部分的。那我们来看看在销毁过程中发生了什么?当shared_ptr的引用计数为0时,会对数据部分(shared_ptr指向的对象)执行析构函数然后释放资源,等到weak_ptr不在使用shared_ptr的控制块时才会对控制块部分执行析构然后释放资源。但是我们知道由make_shared构造得来的shared_ptr中的两个指针其实是放在一个大内存内的,不能单独释放单个指针,因此会导致即使shared_ptr的引用计数为0时,weak_ptr依旧在使用shared_ptr的控制块导致数据块部分始终不能得到析构和释放,而通过构造函数得到的shared_ptr却可以单独析构释放数据部分。这也是一次分配内存导致的缺点。

https://blog.csdn.net/DumpDoctorWang/article/details/88598015
https://www.cnblogs.com/shengjianjun/p/3691928.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值