概述
- 我们在使用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();
}