c++的智能指针(5) -- weak_ptr

发布于:2024-04-23 ⋅ 阅读:(22) ⋅ 点赞:(0)

概述

  • 我们在使用shared_ptr会出现以下的问题,会导致内存泄露。

代码1:  类内指针循环指向

#include <iostream>
#include <memory>

class B;

class A {
public:
	A() {
		std::cout << "Construct" << std::endl;
	}
	~A() {
		std::cout << "Destruct" << std::endl;
	}

	void setPtr(std::shared_ptr<B> b) {
		this->p1 = b;
	}
private:
	std::shared_ptr<B> p1;
};

class B {
public:
	B() {
		std::cout << "Construct" << std::endl;
	}
	~B() {
		std::cout << "Destruct" << std::endl;
	}

	void setPtr(std::shared_ptr<A> a) {
		this->p1 = a;
	}
private:
	std::shared_ptr<A> p1;
};

int main(void) {

	{
	std::shared_ptr<A> pa(new A());
	std::shared_ptr<B> pb(new B());

	pa->setPtr(pb);
	pb->setPtr(pa);
    }

	std::cin.get();
}

结果: 

 

问题:

我们代码中,定义了两个类,main中定义了两个指针指向,分别指向两个类的动态空间,从输出结果会发现,我们pa和pb指向的动态空间,在pa和pb释放的时候并没有释放掉。 
这很显然不符合我们使用智能指针的预期。


因为我们使用智能指针,指向动态开辟的空间,它在自己对象释放的时候并没有释放对应的动态空间。--  这样就会导致内存泄露

原因:

原因是什么呢?  
就是我们的A,B类中都存在有指向对方的智能指针,当我们在main中定义指向类A和类B动态空间的智能指针的时候,同时,我们使用setPtr函数,将其内部的智能指针进行了赋值,分别指向了对方。(A内的智能指针指向B的动态空间,B内的智能指针指向A的动态空间)

这时候,对于动态开辟的A类和B类的动态空间而言,都同时存在着两个智能指针管理(一个是main中定义的,一个是类中的成员),引用计数为2。


这时候,当main中定义的智能指针析构,指向两块动态空间的引用计数-1,但是因为之前引用计数为2,-1之后为1,不是0,所以其不会释放动态开辟的空间。


因为main中指向两块空间的指针已经析构,我们已经无法管理这两块空间,但是它们却没有被释放,所以就造成了内存的泄露

 如图:

解决方法:  

方法一:  在类内部将内部指针设置为空 
class A {
public:
	A() {
		std::cout << "Construct" << std::endl;
	}
	~A() {
		std::cout << "Destruct" << std::endl;
	}

	void setPtr(std::shared_ptr<B> b) {
		this->p1 = b;
	}

	void deletePtr() {
		p1 = nullptr;
	}
private:
	std::shared_ptr<B> p1;
};

class B {
public:
	B() {
		std::cout << "Construct" << std::endl;
	}
	~B() {
		std::cout << "Destruct" << std::endl;
	}

	void setPtr(std::shared_ptr<A> a) {
		this->p1 = a;
	}

	void deletePtr() {
		p1 = nullptr;
	}
private:
	std::shared_ptr<A> p1;
};

int main(void) {

	{
		std::shared_ptr<A> pa(new A());
		std::shared_ptr<B> pb(new B());

		pa->setPtr(pb);
		pb->setPtr(pa);

		pa->deletePtr();
		pb->deletePtr();
    }

	std::cin.get();
}

我们在类的内部添加了deletePtr()函数,主要用来将类中的智能指针设置为空,我们只需要在main中的智能指针析构的时候调用对应类中的deletePtr()函数就可以做到释放动态开辟的空间了。

但是,我们使用智能指针的目的是为了让其帮助我们管理动态空间,如果使用这种方式,就失去了其意义,显然这种方式并方便(因为需要我们自己去调用函数,才能成功释放空间)。 

方式二: 使用weak_ptr智能指针 
int main(void) {

	{
		std::shared_ptr<A> pa(new A());
		std::shared_ptr<B> pb(new B());

		pa->setPtr(pb);
		//pb->setPtr(pa);
    }

	std::cin.get();
}

我们对main中的代码稍作修改,就是将pb调用setPtr()函数给注释掉。
也就是,我们不让类B中的智能指针指向类A的动态空间,这样类A的动态空间的引用计数就为1,在指向类A空间的智能指针pa释放的时候,引用计数减为0,这样类A的动态空间就会被释放。

类A的空间释放之后,那其内部的指向类B动态空间的智能指针也被释放,引用计数-1,然后智能指针pb在main中也被释放,这样这块空间的引用计数为0,也就被释放了。

如图:

weak_ptr 

鉴于上面这种思路,c++11有提出了weak_ptr,用来解决shared_ptr这种循环引用的问题

weak_ptr 

  • weak_ptr是一个智能指针,是用于保存shared_ptr的非拥有引用(弱引用),其不能直接访问指向的对象,必须转换成shared_ptr才可以访问。
  • weak_ptr在构建的时候,不能让它单独指向一个空间,必须使用shared_ptr的对象来构造它(也就是其指向shared_ptr指向的空间)。

    当然也可以将weak_ptr对象初始化为空,也可以使用空的weak_ptr来初始化它。

    也可以使用别的weak_ptr对象来构造其对象。
  • weak_ptr允许使用shared_ptr的对象赋值给它,但也只能这样赋值。(NULL或者nullptr也不能复制给它)

std::shared_ptr<int> p = std::make_shared<int>(5);
std::weak_ptr<int> w;
w = p;

w = NULL; // error

代码: 

 

为什么使用weak_ptr不会造成上面所提到的问题
  • weak_ptr指向与shared_ptr相同的空间,并不会增加其引用计数。

    std::shared_ptr<int> p1 = std::make_shared<int>();
    std::cout << "p1的引用计数" << p1.use_count() << std::endl;   // 输出: 1

    std::weak_ptr<int> w1(p1);
    std::cout << "w1的引用计数" << w1.use_count() << std::endl;   // 输出: 1

 会发现尽管w1和p1指向同一块空间,引用计数不变。

  • weak_ptr是用来辅助shared_ptr使用的,即使其指向一片空间,也仅仅是知道这块空间在哪个位置,不能访问空间中的数据。



我们使用w1访问其指向空间的数据,会发现出错了。

weak_ptr转换成shared_ptr (lock()函数)

我们要想使用weak_ptr对象访问数据等,必须将其转化为shared_ptr.

c++提供了lock()函数,可以帮助我们将weak_ptr转化成shared_ptr。 


lock()函数返回一个shared_ptr对象。如下,我们使用w1调用lock()函数,可以定义一个shared_ptr的对象来接收其返回值。

std::shared_ptr<int> p1 = std::make_shared<int>(5);
std::cout << *p1 << std::endl;  

std::weak_ptr<int> w1(p1);
std::shared_ptr<int> p2 = w1.lock();
std::cout << *p2 << std::endl;    // 这样就可以使用转换的共享指针访问数据。

代码2: 使用weak_ptr解决上面的问题 

#include <iostream>
#include <memory>

class B;

class A {
public:
	A() {
		std::cout << "Construct" << std::endl;
	}
	~A() {
		std::cout << "Destruct" << std::endl;
	}

	void setPtr(std::shared_ptr<B> b) {
		this->p1 = b;
	}
private:
	std::shared_ptr<B> p1;
};

class B {
public:
	B() {
		std::cout << "Construct" << std::endl;
	}
	~B() {
		std::cout << "Destruct" << std::endl;
	}

	void setPtr(std::shared_ptr<A> a) {
		this->p1 = a;
	}

private:
	std::weak_ptr<A> p1;
};

int main(void) {
	{
		std::shared_ptr<A> pa(new A());
		std::shared_ptr<B> pb(new B());

		pa->setPtr(pb);
		pb->setPtr(pa);
	}

	std::cin.get();
}

结果: 

 我们将类A和类B中的一个的智能指针修改为weak_ptr(代码中将类B修改),观察结果我们会发现成功析构了动态空间。

就是因为weak_ptr不会使得引用计数+1,所以类A的引用计数为1,main中析构pa之后,引用计数就变成了0,释放空间。

当我们需要使用类B中的智能指针访问其指向的空间的时候,我们就可以将其转化为shared_ptr然后再去访问。

 

weak_ptr的empired()函数 

我们使用weak_ptr指向一块空间,这块空间也被一个智能指针shared_ptr管理着。

我们无法直接使用weak_ptr访问对应空间,需要使用lock()转化为shared_ptr然后才能访问,但是我们访问之前,是要确保这块空间是否存在的。

因为weak_ptr不会影响引用计数,所以它指向一块空间之后,这块空间的释放不由它决定。

所以当管理这块空间的shared_ptr的对象析构的时候,引用计数变为0,这块空间已经被释放了,但是weak_ptr还不知道我们将它转换为shared_ptr访问其指向的空间会出问题(导致程序中断)。(因为这个空间已经释放了)

  • bool empired();  // 用来判断weak_ptr指向的空间是否被释放。 

    如果被释放,返回true;如果没有被释放,返回false。
建议:
所以在我们使用weak_ptr访问其空间的时候(要使用lock()转化为shared_ptr才能访问),防止程序突然中断(因为其访问的空间被释放了),我们在访问的时候需要调用empired()函数判断一下weak_ptr指向的空间是否已经被释放了如果释放了,就不要访问。没有释放就可以访问。(if判断)

代码: 


int main(void) {
	std::weak_ptr<int> w;

	{
		std::shared_ptr<int> p = std::make_shared<int>(5);
		std::weak_ptr<int> w = p;
	}

	if (w.expired()) {  // expired()被释放返回true
		std::cout << "weak_ptr对应的空间已经被释放" << std::endl;
	}
	else {
		std::shared_ptr<int> p = w.lock();
		std::cout << *p << std::endl;
	}

	std::cin.get();
}