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

  • Post author:
  • Post category:其他



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



智能指针存在的意义

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

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

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



智能指针的种类


  • 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;
}

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

int main(int argc, char *argv[])
{
	test();

	getchar();
	return 0;
}

在这里插入图片描述

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

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

void test() {
	auto_ptr<Person> p(new Person());
	p->m_age = 10;
	p->m_age = 20;
}

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


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;
}

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

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

这样才完整了。

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


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

通俗的讲智能指针:在创建时本质是智能指针类的对象,但我们完全可以当做指向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;
}



多个 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 
}

这样写就代表,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对象的内存才被回收

在这里插入图片描述



可以通过已经存在的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;
}



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,没有问题

在这里插入图片描述

{
	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、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;
	}
};

紧接着,我们创建指向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<Car> m_car = car_ptr
	car_ptr->m_person = person_ptr;  //相当于shared_ptr<Person> m_person = person_ptr
}

然后我们使用已经创建好的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;
	}
};

一旦,变成弱引用,在作用域结束后,强引用计数不会保留,直接变成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

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



版权声明:本文为weixin_45452278原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。