关于shared_ptr智能指针的简单理解

关于shared_ptr智能指针的简单理解

  • 什么是智能指针
  • 使用智能指针的优点
  • shared_ptr的源码分析(超简化版)
  • 对shared_ptr智能指针的简单使用
    • 初始化`shared_ptr`
    • 通过`shared_ptr`调用对应类的函数
    • 关于通过用`shared_ptr`生成数组的问题
  • 关于使用`shared_ptr`的注意事项
    • 不要轻易使用get()进行原始指针操作
    • 在使用移动语义后再访问内存会怎么样

什么是智能指针

智能指针是一个模板函数,为我们封装好了一系列的方法来进行内存的自动管理。本质上我们也可以通过编写自己的模板类来实现和智能指针类似的功能,后续会提到相关的内容。


使用智能指针的优点

在使用智能指针之前,我们可以回顾一下以前我们在使用指针对内存进行操作。
比如现在我们编写了一个Test类,定义如下:

class Test
{
     
public:
	int TestGet() {
     
		return Num;
	}
	void showNum() {
     
		cout << Num << endl;
	}
	void TestSet(int num) {
     
		this->Num = num;
	}
	Test() {
     
		Num = 0;
		cout << "调用默认构造函数Test()" << endl;
	}
	Test(int Num) {
     
		this->Num = Num;
		cout << "调用构造函数Test(num)" << endl;
	}
	virtual ~Test() {
     
		cout << "调用析构函数~Test()" << endl;
	}

private:
	int Num;
};

之后我们new一块内存,并用一个指针指向它

Test* ptr1 = new Test;

下面我们来分析可能造成的三种情况:

1.没有使用delete语句

  • 如果不使用delete语句,这块内存就不会被释放。如果在编程的过程中不小心忘了,那这块内存就被浪费掉了。

2.内存泄漏

  • 第一种情况还不是最可怕的情况,若此时改变了ptr1指针的指向,比如:ptr1 = nullptr; 就会出现内存泄漏的情况,此时没有任何指针指向之前申请出来的内存,那么这块内存就无法被我们正常delete释放了。

3.多次析构

  • 假如我们足够小心,可以记得及时使用delete函数。下面我们再来看一种情况:
  • 现在我们在复制的时候不小心采用了浅复制的方法,即两个指针指针指向了同一块内存空间:
    Test* ptr2 = ptr1;
    同时我们在析构的时候不小心析构了两次,即:
    delete(ptr1);
    delete(ptr2);
    那么程序就会出错。

这里可能会有疑问:啊,为啥我要用两个指针指向同一个内存呢,我直接重新深复制new一块出来不就行了?雀氏帅!

但假设现在有两个场景:

  • 1.你不小心这么做的,就像刚刚的例子那样,你并不知道那是浅复制。
  • 2.假设这块内存是一块公共资源,每一个指针都代表不同的进程(或者简单点说:不同的程序),每个进程只是想访问这块公共资源而已,如果每个进程都要重新复制一份,那开销就非常大了。

所以有的时候不可避免的要这么做,最后经过一堆大佬的商讨和出手之后,在C++11之后我们就迎来了智能指针了(我们也有垃圾回收了,以后JAVA这方面喷不了把,哈哈哈哈(开个玩笑,都是好语言))

OK,那现在分析智能指针的优点就非常容易了:

  • 首先,智能指针share_ptr能够自动帮我们释放内存。(对应上面的1、2情况)
  • 其次,对于share_ptr智能指针来说(还有其他两类智能指针,以后再介绍),只有在最后一个智能指针不再指向这块内存的时候,它才会帮我们释放内存。简单点说就是没人用了才释放。(对应上面的3情况)

OK,那智能指针这么牛皮,那它是怎么实现的呢?——下面分析一波shared_ptr


shared_ptr的源码分析(超简化版)

雀氏这玩意源码优点复杂,我们就简单看看它的实现好了。
前面都在说智能指针,现在看看shared_ptr作为智能指针中的一种,有什么神奇的地方。和unique_ptr不同,shared_ptr既然名字中都有个share,所以它可以被多个指针指向,那么它是如何实现当最后一个指向这块内存的指针不再指向这块内存后,这块内存才被释放的呢?————通过引用计数
这个和操作系统里面的信号量机制其实挺像的:

template<class T>
class shared_ptr
{
     
public:
	T* get() const;	//获取原始指针
  	T use_count() const; //获取引用计数
  	返回类型 operator->(参数); //重载运算符->
  	...
private:
	T* px;
	shared_count pn;
};

假如这块内存有三个指针指着,那么pn就等于3,这个T* px就是T类型的原始指针,可以通过get方法获取,同时这个模板重载了->运算符,这样可以使得我们使用智能指针可以像使用普通指针那样使用
还是重点说说引用计数,shared_ptr提供的引用计数非常厉害,好像是线程安全的,当然我们现在理解的话可以先简单点理解它就是记录有多少个指针指向这块内存的。当有新的指针指向这块内存,pn就加1;当旧指针不再指向这块内存的时候,pn就减1。
(猛的童鞋可以看看源码解析)
为了加深理解,下面可以对shared_ptr进行简单的测试使用。

对shared_ptr智能指针的简单使用


初始化shared_ptr

主要有以下种方法:(不限于此)

  • 1.使用share_ptr的构造函数进行初始化

    shared_ptr<Test> ptr1(new Test); 
    
  • 2.使用share_ptr的复制构造函数进行初始化(右值版本只是改变使用权,详解请见C++11移动语义的相关内容)

    shared_ptr<Test> ptr2(ptr1);
    // 右值版本
    //使用移动语义调用右值版本的复制构造函数
    shared_ptr<Test> ptr3 = std::move(ptr1);
    
  • 3.使用std::make_shared模板进行初始化

    //括号里的内容是初始化的值
    shared_ptr<Test> ptr4 = make_shared<Test>(8);
    
  • 4.使用reset方法

    //将指针的原先控制权限释放,即ptr3不再拥有访问原来资源的权限
    //同时ptr3指向一块新的内存(也可以不new)ptr3.reset()单纯释放权限
    ptr3.reset(new Test(30));
    

通过shared_ptr调用对应类的函数

直接通过->调用即可,如:

ptr1->TestSet(20);
cout << ptr1->TestGet() << endl;

关于通过用shared_ptr生成数组的问题

我们知道,在自己申请一个数据的时候,释放需要用到delete[]才能将对应内存释放干净,即:

Test* pp = new Test[4];
delete[] pp;

但是如果只使用shared_ptr构造函数种默认的删除器,则做不到这一点。什么叫删除器呢?
删除器就是释放内存的时候采取什么方式进行释放,默认情况是直接delete。

//如果定义一个数组,不重新定义删除器就会析构错误
//shared_ptr ptr4(new Test[4]);
//使用lambda表达式构造一个匿名函数更加方便
//其中[]是要在内部定义的变量,()里的为参数,{}里的为函数体内容
shared_ptr<Test> ptr5(new Test[4], [](Test* tmp) {
     
	cout << "这是ptr5的删除器" << endl;
	delete[] tmp;
}); //通过自己定义的lambda表达式即可重新定义删除器

//当然,也可以使用C++提供的std::default_delete()
shared_ptr<Test> ptr6(new Test[3], std::default_delete<Test[]>());

关于使用shared_ptr的注意事项


不要轻易使用get()进行原始指针操作

这里涉及两种情况:

  • 1、对原始指针使用strcpy_s等会改变指针指向的函数
    比如以下定义了一个char*类型的shared_ptr,之后通过get函数取出原始指针后进行strcpy_s函数调用:
//直接用智能指针作为函数参数是不行的
shared_ptr<char> ptr1(new char[20],std::default_delete<Test[]>()); 
char a[10] = "abcdefghi";
strcpy_s(ptr1, a, 10);

//但是用这个之后,智能指针就会指向一块新的内存了
//再用ptr1.use_count()就会无效
strcpy_s(ptr1.get(),10, a); //会改变内部指针的指向
cout << ptr1 << endl; //error

  • 2、对原始指针使用delete
    这里我作死试了一下,直接对原始指针delete,发现内存会被析构两次。
//test
shared_ptr<Test> ptr1(new Test); 
shared_ptr<Test> ptr2(ptr1);
delete(ptr2.get()); //在出作用域后报错,析构两次同一块内存

为什么会出现这样的情况?

  • 在我们自己delete的时候,相当于我们已经把这块内存进行释放了,等到没有指针再指向这块内存的时候(这里是shared_ptr出作用域后),模板函数会调用它的Destory函数delete这部分内存。但由于内存在之前已经被我们手动释放了,所以再delete就会报错了。
  • 这里想一想,当我们delete之后,之前指向这块内存的智能指针还能再用吗?

在使用移动语义后再访问内存会怎么样

还是一样,先定义一个ptr1的智能指针,之后通过移动语义将ptr1对这块内存的使用权限转移给ptr3,即:

shared_ptr<Test> ptr1(new Test); 
//使用移动语义调用右值版本的复制构造函数
shared_ptr<Test> ptr3(std::move(ptr1));

之后再通过->运算符调用类方法就会报错:

cout << ptr1->TestGet() << endl; //error
ptr1->TestSet(20); //error

但是呢会发现一个很有趣的现象,你依然可以用ptr1模板的成员函数use_count()去查看这块内存的引用计数情况:

cout << ptr1.use_count() << endl; //值为0

其实很好理解,因为智能指针本身是模板,只不过通过类的封装和运算符的重载让他看起来像个指针而已,但本质上它依然是个模板

当使用 -> 运算符的时候,它会调用模板成员函数获得指针指向的内存对象的this指针,但前面使用移动语义的时候,指针会向编译器声明不再使用这块内存,因此获取到的this指针为nullptr,因此无法通过this指针访问类的方法。

而通过 . 运算符访问的是智能指针的模板函数,因此是可以的。

你可能感兴趣的:(C++11,c++)