• C++学习之路-智能指针


    哇哦,终于学到了智能指针!!

    智能指针存在的意义

    可以改善传统指针的一些不便之处。那么传统的指针有哪些不便之处呢?

    • 需要手动管理内存
    int *p = new int();
    //...
    delete p; //手动释放
    
    • 1
    • 2
    • 3
    • 容易发生内存泄漏(忘记释放、出现异常无法释放等)
    int *p = new int();
    //...
    //delete p; //忘记释放
    
    • 1
    • 2
    • 3
    try {
    	int *p = new int();
    	throw 0;
    	delete p; //出现异常,导致没有释放
    }
    catch (int &exception) {
    	cout << "done exception" << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 释放之后产生野指针
    int *p = new int();
    delete p;
    p = nullptr; //如果不置位空指针,就会变成野指针
    
    • 1
    • 2
    • 3

    智能指针就是为了解决上述问题而生的。

    智能指针的种类

    • auto_ptr:属于C++98标准,在C++11中已经不推荐使用(有缺陷:不能用于数组)
    • shared_ptr:属于C++11标准
    • unique_ptr:属于C++11标准

    auto_ptr怎么用

    auto_ptr在之后的开发不推荐使用,但是毕竟是智能指针之父,还是要了解一下内部原理。

    定义一个Person类,写了一个成员变量,构造函数和析构函数。然后声明一个test函数,里面包括:申请堆空间,操作对象,释放堆空间,野指针清零。

    class Person {
    
    public:
    	int m_age;
    	Person() {
    		cout << "Person()" << endl;
    	}
    	~Person() {
    		cout << "~Person()" << endl;
    	}
    };
    
    void test() {
    	Person *p = new Person();
    	p->m_age = 10;
    	p->m_age = 20;
    	delete p;
    	p = nullptr;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    我们在main函数中调用完test函数,就会完成上述步骤:包括创建对象时调用构造函数,释放对象时调用析构函数。

    int main(int argc, char *argv[])
    {
    	test();
    
    	getchar();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    但,这是传统指针的做法。有很多麻烦我们也看到了,比如:必须在所有对象操作完之后才释放,还要清空指针等等。

    那如果我们使用智能指针,会达到什么样的效果呢?我们重写test函数

    void test() {
    	auto_ptr<Person> p(new Person());
    	p->m_age = 10;
    	p->m_age = 20;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这里说明一下智能指针的构造方式:

    auto_ptr<泛型> p(new 泛型());
    注意一个细节:p并不是指针,而是对象类型,是auto_ptr类的对象。且p后面的括号必须传入一个地址,也就是相当于p要“指向”的内存区域

    这样的写法可以理解为:智能指针p指向了堆空间的泛型对象

    在这里插入图片描述

    我们可以看到,我们并没有释放p所指向的那块对象内存。依然完成了析构函数的调用。这是怎么回事呢?

    智能指针之所以可以自动的delete对象内存,是因为智能指针内部有这样一个功能:当智能指针销毁的时候,所指向的对象也被delete。这个功能是在auto_ptr类的内部写好的。

    也就是说:p在内存中销毁了,指向的对象内存就自动回收了。因为p定义在test函数内部,所以函数调用完毕之后,p的内存立马释放,p就销毁了,然后new Person就自动回收了。

    了解了原理,我们自己实现一个智能指针:

    智能指针auto_ptr的自实现

    智能指针的神奇之处,就在于智能指针对象销毁的时候,所指向的对象内存也跟着销毁。这是怎么办到的呢?

    几处细节:

    • 需要使用模板类,因为将来我的智能指针指向的类应该是泛型的
    • 智能指针内部需要有一个泛型的指针,指向即将传进来的泛型对象堆空间(T *m_object = new T() )
    • 由于我们在使用智能指针的时候,需要将new T传入智能指针对象的构造函数,因此构造函数必须是T类型的指针作为参数。然后需要将传进来的new T赋值给m_object,因此这里选择初始化列表赋值即可
    • 智能指针对象销毁则释放所指向的堆空间。首先智能指针对象销毁,那就会调用智能指针对象的析构函数,所以释放所指向的堆空间的操作需要在该析构函数里完成。直接在析构函数里delete掉刚才指向new T的指针m_object即可
    template<class T>
    class SmartPointer
    {
    	T *m_object; //接收传进来的new T()
    public:
    	SmartPointer(T *obj);
    	~SmartPointer();
    	
    private:
    
    };
    
    template<class T>
    SmartPointer<T>::SmartPointer(T *obj):m_object(obj)
    {
    	//暂时不用做任何事情
    	//外面new Person()传进来,赋值给形参Person *obj --> Person *obj = new Person() --> Person *m_object = new Person()
    	//这就完成了对象的堆空间内存申请,指向堆空间的是Person指针m_object
    	//因此,智能指针对象p销毁的时候,会调用~SmartPointer(),只要在该析构函数中释放掉Perosn  *m_object
    	//就可以实现:智能指针对象销毁时立马释放所指向的堆空间内存
    }
    
    template<class T>
    SmartPointer<T>::~SmartPointer()
    {
    	if (m_object == nullptr) return;
    	delete m_object;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    但,想要复刻智能指针,还差一步。智能指针对象既然是指向了T对象内存,那就应该可以通过箭头“->”访问T类型的成员。我们知道,想要通过箭头“->”访问T类中的成员,就必须是T类型的指针。所以,这里在智能指针类里重载一下运算符“->”即可

    // p->就会返回T类型的指针,那就可以通过箭头调用T中的成员
    T *operator->() {
    	return m_object;
    }
    
    • 1
    • 2
    • 3
    • 4

    这样才完整了。

    void test() {
    	SmartPointer<Person> p(new Person());
    	p->m_age = 10; //能箭头调用Person类里的成员变量,那就必须是Person *类型
    }
    
    • 1
    • 2
    • 3
    • 4

    总结:智能指针不过就是对传统指针的再次封装!

    通俗的讲智能指针:在创建时本质是智能指针类的对象,但我们完全可以当做指向T类对象的“指针”。

    auto_ptr内存变化

    我们在test函数内部设置了一个断点

    在这里插入图片描述

    此时,p还没有被销毁,指向0x00000022ba67f6930,里面存放m_age = 10。

    在这里插入图片描述

    当断点执行完test函数之后,按理说p应该被销毁,指向的内存也应该被回收

    在这里插入图片描述

    然而p并没有被销毁,也没有被置为nullptr,甚至p指向的对象内存空间也没有被回收(但是,析构函数确实被调用了 ),这是怎么回事呢?

    在这里插入图片描述

    share_ptr怎么用

    不推荐使用auto_ptr,不仅过时了,而且还存在一些问题。share_ptr的出现就是为了解决auto_ptr的问题。

    auto_ptr存在的问题

    auto_ptr智能指针不可以指向数组对象。

    我们看到auto_ptr的源码,delete时就是默认的直接detele 指针。我们知道释放数组对象,必须是delete[],所以auto_ptr就注定了不能指向数组。

    在这里插入图片描述

    share_ptr可以指向数组

    share_ptr解决了auto_ptr不能指向数组的问题。要注意,在泛型的时候,要声明为T[ ],才可以指向数组对象。

    void test() {
    	shared_ptr<Person[]> p(new Person[5]);
    	p[4].m_age = 10;
    }
    
    • 1
    • 2
    • 3
    • 4

    多个 share_ptr可以指向同一个对象

    多个 share_ptr可以指向同一个对象,当最后一个share_ptr在作用域范围内结束时,对象才会被自动释放。

    {
    	shared_ptr<Person> p1(new Person());
    	shared_ptr<Person> p2 = p1;
    	shared_ptr<Person> p3 = p2;
    	shared_ptr<Person> p4 = p3;
    	//p1-p4全部指向new 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这样写就代表,p1~p4智能指针对象都指向了Person对象。我们通过内存也可以查看这一点:他们指向的内存区域地址都是一样的,由于没有初始化m_age,因此m_age的内容是0xcdcdcdcd,也就是堆空间的初始化内容。

    在这里插入图片描述

    多个指向对象的智能指针 share_ptr全部销毁后,对象内存才会被回收

    cout << 1 << endl;
    {
    	shared_ptr<Person> p4;
    	{
    		shared_ptr<Person> p1(new Person());
    		shared_ptr<Person> p2 = p1;
    		shared_ptr<Person> p3 = p2;
    		p4 = p3;
    		//p1-p4全部指向new 
    	}
    	cout << 2 << endl; //此时p1~p3全部销毁了,但是p4还没有被销毁,因此对象内存就不会被回收
    }//执行完最后一个智能指针p4所在的作用域之后,p4销毁,至此全部指向Person对象的智能指针销毁
    cout << 3 << endl;//此时,Person对象的内存才被回收
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    可以通过已经存在的share_ptr智能指针初始化一个新的share_ptr智能指针

    shared_ptr<Person> p4;
    {
    	shared_ptr<Person> p1(new Person());
    	shared_ptr<Person> p2 = p1; //利用已经存在的p1初始化新的p2
    	shared_ptr<Person> p3 = p2; //同理
    	p4 = p3;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    share_ptr原理

    • 一个share_ptr会对一个对象产生强引用(strong reference)
    • 被指向的对象会存在一个与share_ptr对应的强引用计数,记录这当前对象被多少个share_ptr强引用着(被share_ptr指着)

    使用智能指针下的use_count()成员函数获得当前对象被多少个智能指针指向

    {
    	shared_ptr<Person> p1(new Person());
    	cout << p1.use_count() << endl; //1,因为上句代码执行完,有1个智能指针指向Person(p1)
    
    	shared_ptr<Person> p2 = p1;
    	cout << p2.use_count() << endl; //2,因为上句代码执行完,有2个智能指针指向Person(p1,p2)
    
    	shared_ptr<Person> p3 = p2;
    	cout << p1.use_count() << endl;  //3,使用p3或是p1调用都行,只要是已经存在的智能指针,且指向的是一个对象
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    打印1、2、3,没有问题
    在这里插入图片描述

    {
    	shared_ptr<Person> p3;
    	{
    		shared_ptr<Person> p1(new Person());
    		cout << p1.use_count() << endl; //1
    
    		shared_ptr<Person> p2 = p1;
    		cout << p2.use_count() << endl; //2
    
    		p3 = p2;
    		cout << p3.use_count() << endl;//3
    
    	}
    	cout << p3.use_count() << endl;//1,因为p1、p2都销毁了,到这的时候,只有p3还在,所以打印1
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    打印1、2、3、1,没有问题
    在这里插入图片描述

    • 当有一个share_ptr销毁时(比如作用域结束),对象的强引用计数就会-1
    • 也就是说:当一个对象的强引用计数为0时,该对象就要被回收内存了。(因为没有任何的share_ptr指向了,说明智能指针也都销毁了)

    share_ptr的循环引用问题

    我们知道了有一个share_ptr指向对象的话,该对象的强引用计数就加1。当强引用计数为0的时候,对象才会被回收内存空间。

    但是一旦出现循环引用问题,share_ptr智能指针就会发生内存泄漏,是一个Bug。那什么是循环引用呢?举个例子就明白了了

    首先声明两个类:Person和Car。Person里有Car类型的智能指针,意味着人拥有车;Car里面有Person类型的智能指针,意味着车可以载人。实际需求中一定存在这种交叉包含的类。

    class Person {
    public:
    	shared_ptr<Car> m_car;
    	Person() {
    		cout << "Person()" << endl;
    	}
    	~Person() {
    		cout << "~Person()" << endl;
    	}
    };
    
    class Car {
    public:
    	shared_ptr<Person> m_person;
    	Car() {
    		cout << "Car()" << endl;
    	}
    	~Car() {
    		cout << "~Car()" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    紧接着,我们创建指向Person对象的智能指针person_ptr,创建指向Car对象的智能指针car_ptr。

    {
    	shared_ptr<Person> person_ptr(new Person()); //创建指向Person对象的智能指针person_ptr
    	shared_ptr<Car> car_ptr(new Car()); //创建指向Car对象的智能指针car_ptr
    
    	person_ptr->m_car = car_ptr; //相当于shared_ptr m_car = car_ptr
    	car_ptr->m_person = person_ptr;  //相当于shared_ptr m_person = person_ptr
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    然后我们使用已经创建好的Car类型智能指针初始化person_ptr->m_car(也就是初始化shared_ptr< Car> m_car,但是是初始化在Person对象里的m_car)。同理,使用已经创建好的Person类型智能指针初始化car_ptr->m_person(也就是初始化shared_ptr< Person> m_person,但是是初始化在Car对象里的m_person)

    我们通过内存可以看到,car_ptr和person_ptr->m_car(shared_ptr< Car> m_car)指向的相同区域,也就是该Car对象强引用计数为2。
    在这里插入图片描述

    用一幅图来表达上述循环引用的逻辑:

    在这里插入图片描述

    当离开作用域之后,栈空间存在的person_ptr和car_ptr被销毁,但是堆空间的强引用还在,因此各自的强引用计数为1,不是为0。

    在这里插入图片描述
    所以,就导致了,作用域结束后,Person对象和Car对象的析构都不会被调用

    在这里插入图片描述

    那怎么解决这个问题呢?通过使用弱引用智能指针weak_ptr去解决这个问题

    weak_ptr解决循环引用问题

    我们只需在循环引用的两个类中,任意一个类里面声明智能指针为弱引用就可以解决这个问题。

    class Person {
    public:
    	weak_ptr<Car> m_car;
    	Person() {
    		cout << "Person()" << endl;
    	}
    	~Person() {
    		cout << "~Person()" << endl;
    	}
    };
    
    class Car {
    public:
    	shared_ptr<Person> m_person;
    	Car() {
    		cout << "Car()" << endl;
    	}
    	~Car() {
    		cout << "~Car()" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    一旦,变成弱引用,在作用域结束后,强引用计数不会保留,直接变成0。所以Car对象直接消失,Car对象消失了,就没有指针指向Person对象了,那么Person对象也会消失。就实现了对象内存全部回收。

    在这里插入图片描述

    总结:如果存在循环引用的需求,智能指针一定不要全部用shared_ptr,必须将其中一个类里面的智能指针设置为weak_ptr。

    unique_ptr

    unique字面意思:唯一的。相对于share_ptr,可以确保同一时间只有一个unique_ptr智能指针指向对象。另外,unique_ptr也会对对象产生一个强引用。

    不可以这样:

    在这里插入图片描述

    只能这样:

    在这里插入图片描述

    同一时间(同一作用域),只能使用一个unique_ptr指向对象。反过来说,不同的作用域就可以使用不同的unique_ptr指向这个相同的对象了。即,我们可以在当前作用域将unique_ptr的指向权移交给另外一个作用域的unique_ptr智能指针。

    C++通过std::move函数转移unique_ptr的指向权

    unique_ptr<Person> p0;
    {
    	unique_ptr<Person> p1(new Person());
    	p0 = std::move(p1); //将Person对象由p1指向转为p0指向
    }
    // 转为p0指向该Person对象,p1将唯一指向权转交给p0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    总之:想要唯一指向就选择unique_ptr,其余都使用share_ptr就行。

  • 相关阅读:
    MySQL中的行锁
    MySql 执行count(1)、count(*) 与 count(列名) 区别
    Qt扩展-KDDockWidgets 的使用
    大二暑假 + 大三上
    儿童玩具在美国市场需要注意什么?
    Android开发基础——Fragment实践
    Robotframework 的简介及其工作原理~
    springboot系列(七):如何通过mybatis-plus实现接口增删改查|超级详细,建议收藏
    记录|C#主界面设计【Web风格】
    Scala | 宽窄依赖 | 资源调度与任务调度 | 共享变量 | SparkShuffle | 内存管理
  • 原文地址:https://blog.csdn.net/weixin_45452278/article/details/126651385