C++智能指针总结三——shared_ptr与weak_ptr
1.往期回顾
2.为什么要使用shared_ptr
这是因为,无论是auto_ptr还是unique_ptr都是排它型的,即只允许一个智能指针对象引用控制一块动态内存。那如果我们要使用多个指向同一块动态内存的指针时,显然auto_ptr与unique_ptr都不能做到。因此,为了实现多个智能指针引用同一块动态内存的目的,C++标准新增了shared_ptr。
3.shared_ptr的使用
1.shared_ptr原理
shared_ptr内部实现了一个计数器,用于记录引用同一块动态内存的智能指针数量,一旦有新的智能指针指向了已被引用的动态内存,引用计数加一。一旦有智能指针生命周期到了调用析构函数销毁时,引用计数减一,如果检测到此时引用计数为零,则销毁智能指针的同时释放动态内存。
看一个例子:
#include <Windows.h>
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
class DEMO {
public:
DEMO(int data) {
this->data = data;
cout << "创建text" << endl;
cout << "data=" << data << endl;
}
~DEMO() {
cout << "析构text" << data << endl;
}
private:
int data;
};
int main(void) {
{
shared_ptr<DEMO> ptr1(new DEMO(1)); //引用计数加一
{
shared_ptr<DEMO> ptr2(ptr1); //引用计数加一
{
shared_ptr<DEMO> ptr3(ptr2); //引用计数加一
{
shared_ptr<DEMO> ptr4(ptr3); //引用计数加一
cout << "ptr4.count=" << ptr4.use_count() << endl;
}
cout << "ptr3.count=" << ptr3.use_count() << endl; //引用计数减一
}
cout << "ptr2.count=" << ptr2.use_count() << endl; //引用计数减一
}
cout << "ptr1.count=" << ptr1.use_count() << endl; //引用计数减一
} //引用计数减一
system("pause");
return 0;
}
结果:
shared_ptr ptr2(ptr1),即使用ptr1初始化ptr2 。
.use_count()方法可以返回智能指针的引用计数。
开始一共有四个指针指向同一块动态内存,所以最开始引用变量是4,而后随着指针生命周期的结束,引用变量逐渐减一,直至引用变量减为0,释放动态内存,执行DEMO对象的析构函数。
2.shared_ptr的使用
主要补充一些shared_ptr新增的API,至于与auto_ptr,unique_ptr一样的方法,就不赘述了。
1.构造
#include <Windows.h>
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
int main(void) {
shared_ptr<int> ptr1; //初始化为空的shared_ptr,可以指向int类型的对象
shared_ptr<int []> ptr2; //初始化为空的shared_ptr,可以指向int类型的数组
shared_ptr<int[]> ptr3(new int[5]{ 1,2,3,4,5 }); //使用参数列表初始化动态数组,C++17后支持
system("pause");
return 0;
}
2.使用make_shared 初始化对象
#include <Windows.h>
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
int main(void) {
shared_ptr<int> ptr1 = make_shared<int>(5);
system("pause");
return 0;
}
推荐使用make_shared初始化对象,因为效率更高。
使用shared_ptr初始化与赋值要明白引用计数的变化,其实只要看有几个智能指针对象指向了同一块内存,引用计数就是几。
4.使用shared_ptr的陷阱
先看一段代码:
#include <Windows.h>
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
class GRIL;
//创建BOY类
class BOY {
public:
BOY() {
cout << "执行BOY类的构造方法" << endl;
}
~BOY() {
cout << "执行BOY类的析构方法" << endl;
}
void GetGril(shared_ptr<GRIL>& gril_ptr) { //BOY获取GRIL方法
gril = gril_ptr;
}
private:
shared_ptr<GRIL> gril; //BOY类里的shared_ptr指针
};
//创建GRIL类
class GRIL {
public:
GRIL() {
cout << "执行GRIL类的构造方法" << endl;
}
~GRIL() {
cout << "执行GRIL类的析构方法" << endl;
}
void GetBoy(shared_ptr<BOY>& boy_ptr) { //GRIL类获取BOY方法
boy = boy_ptr;
}
private:
shared_ptr<BOY> boy; //GRIL类里的shared_ptr指针
};
int main(void) {
{
shared_ptr<BOY> ptr1(new BOY); //分配一个boy类对象
shared_ptr<GRIL> ptr2(new GRIL); //分配一个gril类对象
ptr1->GetGril(ptr2);
ptr2->GetBoy(ptr1);
}
system("pause");
return 0;
}
执行结果为:
从结果可以看到,动态分配的BOY与GRIL类对象并未被释放,这是为什么呢?
其实这是由于BOY类与GRIL类对象里的shared_ptr指针相互引用造成的。看一张图:
动态创建完BOY类与GRIL类的对象后,它们各自的智能指针的引用计数为一,又因为BOY类与GRIL类里各有一个指向对方的shared_ptr指针,执行完它们各自的.GetBoy()与.GetGril()方法后。会形成如上图的关系。
当shared_ptr1与shared_ptr2的生命周期到了,被销毁后,会变成如下图的关系:
此时引用计数为一不为零,所以任何一个动态内存都无法释放,其中一个无法释放,就导致另一个无法释放,就造成了类似于死锁的情况。两个都不能释放。就造成了内存泄漏。
再补充一种无法释放的情景:
#include <Windows.h>
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
//创建BOY类
class BOY {
public:
shared_ptr<BOY> boy1;
BOY() {
cout << "执行BOY类的构造方法" << endl;
}
~BOY() {
cout << "执行BOY类的析构方法" << endl;
}
};
int main(void) {
shared_ptr<BOY> ptr1(new BOY);
ptr1->boy1 = ptr1;
system("pause");
return 0;
}
结果:
这种情况同样会造成内存泄漏。
总结来说无法释放内存的原因是,由于对象是动态分配的,而对象本身又含有shared_ptr指针,释放对象需要shared_ptr的释放使引用计数减为零,而shared_ptr的释放又需要对象的释放,两者互相等待对方先释放,往往是两者都无法释放。
5.“死锁”的解除——使用weak_ptr
weak_ptr指针——弱指针,它是为了解决shared_ptr的弊端而被设计出来辅助shared_ptr完成工作的指针。它不能使用 * 与 -> 运算符。但它可以通过lock 获得一个可用的 shared_ptr 对象。weak_ptr不会引起引用计数的变化。除非它通过lock 获得一个可用的 shared_ptr 对象,这时引用计数会加一,shared_ptr被销毁后,引用计数会减一。
看一个例子:
#include <Windows.h>
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
//创建BOY类
class BOY {
public:
weak_ptr<BOY> boy1;
BOY() {
cout << "执行BOY类的构造方法" << endl;
}
~BOY() {
cout << "执行BOY类的析构方法" << endl;
}
void demo() {
cout << "count=" << boy1.use_count() << endl;
shared_ptr<BOY> tmp = boy1.lock(); //当需要使用引用动态内存时,可使用.lock()方法构造一个临时的shared_ptr,因为是临时变量,用完后会析构掉。
cout << "count=" << tmp.use_count() << endl;
}
};
int main(void) {
shared_ptr<BOY> ptr1(new BOY);
ptr1->boy1 = ptr1;
ptr1->demo();
system("pause");
return 0;
}
结果:
当weak_ptr需要使用引用动态内存时,可使用.lock()方法构造一个临时的shared_ptr,此时引用变量会加一。因为是临时变量,用完后会析构掉。计数减一。使用weak_ptr可以避免shared_ptr导致的动态内存无法释放的问题。