测试环境为:Windows/VS2017
前言
最近在学习的过程中,遇到了智能指针这个知识点,之前只知道智能指针能够自动释放资源,但是对于其它的细节都一无所知,今天就来研究一下智能指针。
智能指针,运用了RAII(Resource Acquisition is initialization)
的思想,简单来说就是,使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。说白了就是:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),在构造函数中,通过赋值等操作,让该指针管理资源,并在析构函数里编写delete语句释放指针指向的内存空间。
那么,为什么要有智能指针呢?我们来看一段代码。
int main()
{
ch a = 0;
FILE *pOut = fopen("test.txt", "wb");
if (pOut == nullptr) {
cout << "error for pOut" << endl;
exit(0);
}
cin >> a;
if (a != 'A') {
return 0;
}
fclose(pOut);
return 0
}
这段代码有什么问题呢?在if
的判断语句中,少写了fclose(pOut)
, 如果在判断输入的a
是不是 A
的时候,直接return 了,那么就会存在内存泄露的问题,有的人可能会说,这些基本的东西怎么可能会忘记呢?但是智者千虑必有一失,有时可能只是因为一点点的疏忽,就会导致问题的产生。因此,智能指针的出现是很必要的。
STL中为我们提供了四种类型的智能指针:
auto_ptr
:管理权限的转移
unique_ptr
:唯一管理,防止拷贝
shared_ptr
:引用计数机制
weak_ptr
:weak_ptr
不能单独使用,只能在shared_ptr
中使用,具体的使用,我们在后面再介绍。
我们可以模拟STL库中智能指针最基本的思想,当我们再遇到上面代码的问题时,就很好处理了:
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
T& operator*()
{
return *_ptr;
}
T& operator->()
{
return *_ptr;
}
~AutoPtr()
{
if (_ptr) {
delete _ptr; //不考虑其他类型的资源,假设都是new出来的
}
}
private:
T* _ptr;
};
STL中的智能指针的最基本的思想就类似于上面的这种,不同的地方是,STL库中的智能指针都进行了各种功能的增强,以及各种缺陷的解决,我们来依次剖析。
auto_ptr
C++98版本的库中提供了auto_ptr
,但在C++11中被废弃,同时不推荐使用它。
首先,我们先看一下auto_ptr
的文档介绍:
文档中介绍,当两个 auto_ptr
对象之间发生赋值操作时,将转移所有权,这意味着失去所有权的对象被设置为不再指向该元素(它被设置为空指针)。
因此,我们可以知道 auto_ptr
的原理实际为:管理权限的转移。我们可以使用一段代码来验证它。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
auto_ptr<int> autoPtr(new int(5));
cout << *autoPtr << endl;
auto_ptr<int> autoPtrCopy1(autoPtr); //用第一个auto_ptr对象构造第二个
cout << autoPtr.get() << endl;
cout << *autoPtrCopy1 << endl;
auto_ptr<int> autoPtrCopy2 = autoPtrCopy1; //用第二个给第三个赋值
cout << autoPtrCopy1.get() << endl;
cout << *autoPtrCopy2 << endl;
return 0;
}
运行结果:
我们通过调试和监视来看在程序运行的过程中发生了什么。
我们可以看到:
当用autoPtr
去构造 autoPtrCopy1
时,autoPtr
所管理的资源被转移到 autoPtrCopy1
上,并且autoPtr
被置为空。
当autoPtrCopy1
赋值给autoPtrCopy2
时,autoPtrCopy1
所管理的资源被转移到 autoPtrCopy2
上,并且autoPtrCopy1
被置为空。
这种转移管理权限的做法原理其实就是:每份资源只能由一个auto_ptr
来管理。但是在本版本的auto_ptr
中,并没有对这种机制进行严格的限制,只是告诉编程人员当进行管理权转移的操作后,原来的指针会被置为空,具体的操作需要编程人员自行处理。这样就有可能产生对空指针操作的问题。
我们在代码中增加逻辑判断,就可以解决对空指针解引用的问题:
#include <iostream>
#include <memory>
using namespace std;
int main()
{
auto_ptr<int> autoPtr(new int(5));
cout << *autoPtr << endl;
auto_ptr<int> autoPtrCopy1(autoPtr); //用第一个auto_ptr对象构造第二个
if (autoPtr.get() != nullptr) {
//auto_ptr 为我们提供了get() 接口,让我们来自行判断当前指针是否管理了资源。
cout << *autoPtr << endl;
}
cout << autoPtr.get() << endl;
cout << *autoPtrCopy1 << endl;
auto_ptr<int> autoPtrCopy2 = autoPtrCopy1; //用第二个给第三个赋值
if (autoPtrCopy1.get() != nullptr) {
//auto_ptr 为我们提供了get() 接口,让我们来自行判断当前指针是否管理了资源。
cout << *autoPtrCopy1 << endl;
}
cout << autoPtrCopy1.get() << endl;
cout << *autoPtrCopy2 << endl;
return 0;
}
同时,auto_ptr
还提供了其他的接口:
release 可以将auto_ptr
对象与其管理的资源断开联系,即将auto_ptr
内部指针设置为空,但是不影响它管理的资源,说简单点就是不使用auto_ptr
管理资源了。当使用这个接口后,可能就需要手动释放资源了,因此应该小心内存泄漏问题。
测试代码:
#include <iostream>
#include <memory>
using namespace std;
int main()
{
int* p = new int(5);
auto_ptr<int> autoPtr(p);
cout << *p << endl;
cout << *autoPtr << endl;
autoPtr.release();
cout << "autoPtr.release();" << endl;
cout << *p << endl;
cout << autoPtr.get() << endl;
return 0;
}
运行结果:
reset ,实现的功能为:释放auto_ptr
管理的资源,同时,如果指定了新的资源,就指向新的资源,否则指向空。
测试代码:
#include <iostream>
#include <memory>
using namespace std;
int main()
{
int* p = new int(5);
auto_ptr<int> autoPtr(p);
cout << *p << endl;
cout << *autoPtr << endl;
cout << "autoPtr.reset();" << endl;
autoPtr.reset(); //本次 reset 没有传参
cout << *p << endl;
cout << autoPtr.get() << endl;
cout << "autoPtr.reset(new int(6));" << endl;
autoPtr.reset(new int(6)); //本次reset 重新传入了一个值
cout << *autoPtr << endl;
return 0;
}
运行结果:
这里的p
在第一次reset
的时候就已经被释放了,已经是野指针了,因此再解引用是不对的。
unique_ptr
我们在前面说过,auto_ptr
的思想是:资源只能被一个auto_ptr
对象管理,也就是管理权的转移。同时,它也在文档中说明,当进行了管理权的转移后,原本的对象将被置为空。至于后续的对原对象的操作,则需要编程人员自行进行判断和处理。这种处理方式就有可能导致对空指针进行操作,进而引起内存奔溃的问题。
因此,在C++11中,提供了一种新的智能指针unique_ptr
,首先,我们先了解一下unique_ptr
的介绍。
我们可以看到,在unique_ptr
中,对资源唯一管理性的理念更明确了。即,每份资源,只允许一个unique_ptr
对象来管理。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
unique_ptr<int> autoPtr(new int(5));
cout << *autoPtr << endl;
unique_ptr<int> autoPtr1(autoPtr);
unique_ptr<int> autoPtr2(autoPtr);
return 0;
}
运行结果:
通过错误提示我们可以知道,库中删除了拷贝构造函数和同类对象的赋值运算符重载,这样就不会存在资源管理转移的问题,那也就不会有对空指针操作的问题了。
我们可以查看unique_ptr
的拷贝构造函数和同类对象的赋值运算符重载在库中的实现:
在这里用到了C++11的语法,直接删除掉了函数:
这里需要注意的是,在unique_ptr
中还有一个特别的地方。
测试代码:
#include <iostream>
#include <memory>
using namespace std;
unique_ptr<int> fun1()
{
unique_ptr<int> a(new int(5));
cout << a.get() << endl;
return a;
}
int main()
{
unique_ptr<int> a = fun1();
cout << a.get() << endl;
return 0;
}
运行结果:
两个管理的是同一片空间。
我们再来看unique_ptr
提供的其他接口:
在unique_ptr
中,引入了删除器的思想:
因为不同类型的资源的释放方法是不同的,因此不能只用delete
来进行删除 ,应该针对每种类型的资源,都创建自己的删除函数,而编译器并不知道用户指向的资源是什么类型,因此,就多添加一个参数,用来将删除方法传入,这里的传参方法是仿函数的方法。在unique_ptr
中,使用的默认的删除方法是 delete
。因此,如果是其它类型的资源就需要自己定义删除函数,然后在定义unique_ptr
对象的时候,一同传入。
这个删除函数会在 unique_ptr
的析构函数中调用。同时可以使同 get_deleter()
接口实现单独访问。
测试用例:
#include <iostream>
#include <memory>
using namespace std;
struct delete_ptr_malloc
{
delete_ptr_malloc()
{}
template<class T>
void operator()(T *p) {
if (p) {
cout << "*p == " << *p << " free p" << endl;
free(p);
}
}
};
int main()
{
int* a = (int*)malloc(sizeof(int));
*a = 5;
unique_ptr<int, delete_ptr_malloc> autoPtr(a); //先让autoPtr 管理 a
cout << *autoPtr << endl;
int* b = (int*)malloc(sizeof(int));
*b = 6;
autoPtr.reset(b); //对autoPtr 进行reset 让它管理 b
cout << *autoPtr << endl; //输出新的值
int* c = (int*)malloc(sizeof(int));
*c = 7;
autoPtr.get_deleter()(c); //直接通过 autoPtr.get_deleter() 单独访问
return 0;
}
运行结果:
unique_ptr
重载了bool 运算符,可以直接用unique_ptr
对象进行逻辑判断,判断的依据为unique_ptr
指向的资源是否为空。
测试用例:
#include <iostream>
#include <memory>
using namespace std;
int main()
{
unique_ptr<int> autoPtr(new int(5));
//借助reset 完成操作,reset 的默认参数为空,即释放原来的空间,并指向空
autoPtr.reset();
if (autoPtr) {
cout << "autoPtr is not empty" << endl;
}
else {
cout << "autoPtr is empty" << endl;
}
return 0;
}
运行结果:
release 和 reset 的功能与auto_ptr
中的功能相似,不同之处在于reset
中会调用我们传入的删除器,这里不再过多介绍。
swap 函数顾名思义,它实现的功能就是两个unique_ptr
管理资源的交换,交换完成后,两个对象管理的资源都变成了之前对方管理的资源。
测试用例:
#include <iostream>
#include <memory>
using namespace std;
int main()
{
unique_ptr<int> autoPtr0(new int(5));
unique_ptr<int> autoPtr1(new int(6));
cout << "*autoPtr0 = " << *autoPtr0 << endl;
cout << "*autoPtr1 = " << *autoPtr1 << endl;
autoPtr0.swap(autoPtr1);
cout << "autoPtr0.swap(autoPtr1);" << endl;
cout << "*autoPtr0 = " << *autoPtr0 << endl;
cout << "*autoPtr1 = " << *autoPtr1 << endl;
return 0;
}
运行结果:
这里需要注意的是,交换的两个对象管理的资源必须是同类型的
。
shared_ptr
通过上面的介绍,我们知道C++11中引入了unique_ptr
,它的原理是保证管理同一份资源的 unique_ptr 始终只有一个
。这样虽然可以起到管理资源的作用,但是如果对于应该有多个指针维护的资源,就会产生问题,因此,C++11中还引入了另外一种智能指针,即shared_ptr
,它与unique_ptr
不同的是,它支持多个 shared_ptr
管理同一份资源,只有当所有管理那份资源的shared_ptr
对象都被销毁的时候,才会释放资源。
首先,我们了解一下shared_ptr
的说明:
shared_ptr
实现的原理很简单,就是引入了计数器,每当有一个指针指向资源的时候,计数器的计数就会增加,当需要销毁对象时,如果计数器的值为0,那么就释放资源,否则计数器的值减一。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> autoPtr(new int(5));
cout << "autoPtr.use_count() = " << autoPtr.use_count() << endl;
cout << "autoPtr.get() = " << autoPtr.get() << endl;
shared_ptr<int> autoPtr1 = autoPtr;
//让autoPtr1 也管理 autoPtr 管理的资源
cout << "\nshared_ptr<int> autoPtr1 = autoPtr" << endl;
cout << "autoPtr.get() = " << autoPtr.get() << endl;
cout << "autoPtr1.get() = " << autoPtr1.get() << endl;
cout << "autoPtr.use_count() = " << autoPtr.use_count() << endl;
cout << "autoPtr1.use_count() = " << autoPtr1.use_count() << endl;
autoPtr.reset();
cout << "\nautoPtr.reset()" << endl;
//reset 会释放智能指针管理的资源,并指向新的资源(新资源默认为空)
//但是在shared_ptr 中 还需要考虑count 的值
//因为count的值为2,因此autoPtr只会指向空,并不会释放资源
cout << "autoPtr.get() = " << autoPtr.get() << endl;
cout << "autoPtr1.use_count() = " << autoPtr1.use_count() << endl;
cout << "*autoPtr1 = " << *autoPtr1 << endl;
return 0;
}
我们这里,用autoPtr
和 autoPtr1
管理了同一份资源,当我们对 autoPtr
进行 reset
操作时,autoPtr
的 count
会进行 减一
操作,并判断是否为0
,这里判断的结果为 1
。因此,只是解除autoPtr
对 资源的管理,并且让autoPtr
管理新的资源,因为这里没有传入新的资源,因此,autoPtr
指向默认的空。
运行结果:
我们再来了解shared_ptr
提供的其他接口:
shared_ptr
的 接口大部分与前两种相似,因此这里不做过多解释,我们需要对shared_ptr
中新添加的接口进行了解。
use_count 因为shared_ptr
中引入了计数器,因此这个接口的作用就是返回,当前对象管理的资源共被几个同类型对象管理。
unique 判断当前shared_ptr
管理的资源是否只被当前对象一个管理,如果是的话,返回true,否则返回false。
owner_before 这个函数暂时还没有研究懂,等后面再更新
以上就是shared_ptr 几种常见的接口,在shared_ptr
的构造函数中还有另外的很多种构造方式,等到使用时,具体查询库函数即可。
根据shared_ptr
的介绍来看,我们知道它是可以用多个对象来维护同一份资源,我们把它应用到具体的代码中来,假设我们用shared_ptr
管理一个双向链表:
#include <iostream>
#include <memory>
using namespace std;
struct Node
{
int a;
shared_ptr<struct Node> next;
shared_ptr<struct Node> prev;
~Node()
{
cout << "~Node()" << endl;
}
};
int main()
{
shared_ptr<struct Node> n1(new Node);
shared_ptr<struct Node> n2(new Node);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->next = n2;
n2->prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
运行结果:
等到链表指完之后,我们可以看到每个节点的use_count
都为2
,我们画一个图理解一下:
等到函数到达结尾,准备析构的时候,首先判断 n2
,n2
的 use_count
是 2
减 1
后 不进行析构,这样 n2
的成员也就不进行析构,直接退出。再判断 n1
,n1
的 use_count
是 2
减 1
后 不进行释放,n1
的成员也不进行析构,直接退出。这样就形成了以下这种情况:n1
和 n2
进行析构,它们的 use_count
变为1
,也就是,只是还有n1->next
指向 n2
和 n2->prev
指向 n1
,这时,只要n1->next
析构完成了,n2
就析构了,只要n2->prev
析构完成了,n1
就析构了。但是,它们各自的成员变量,必须是在它们自己析构之后,才能释放。这样到最后两个对象都没有释放。这就是循环引用问题。这个问题该如何解决呢?
weak_ptr
这个时候,weak_ptr
就应运而生了。我们只需要对上述代码进行一些小小的更改,就可以解决问题:
#include <iostream>
#include <memory>
using namespace std;
struct Node
{
int a;
weak_ptr<struct Node> next; //我们将Node中的两个指针类型变为weak_ptr
weak_ptr<struct Node> prev;
~Node()
{
cout << "~Node()" << endl;
}
};
int main()
{
shared_ptr<struct Node> n1(new Node);
shared_ptr<struct Node> n2(new Node);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->next = n2;
n2->prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
运行结果:
这个原理是什么呢?
我们查看weak_ptr
的介绍可以发现weak_ptr
被称为弱管理参数,我们再看它的赋值运算符重载中的介绍:
如果,给weak_ptr
赋值的对象不为空,那么weak_ptr
就成为该对象的一部分,能访问该对象的资源,且不增加use_count
。
这就解释了为什么将struct Node
中的指针类型更改后,就可以成功释放两个对象了。
在weak_ptr 中,多了两个接口:
expired ,用于返回weak_ptr
指向的资源是否已经被释放,如果已经释放就返回true,否则返回false。
lock, 用于获取所管理的对象的强引用(shared_ptr). 如果 expired 为 true, 返回一个空的 shared_ptr; 否则返回一个 shared_ptr, 其内部对象指向与 weak_ptr 相同。
测试用例:
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> sp1(new int(5));
shared_ptr<int> sp2(new int(6));
weak_ptr<int> wp1;
wp1 = sp1;
sp2 = wp1.lock(); //返回强引用 sp1;
cout << wp1.expired() << endl;
sp1.reset(); //清空sp1
sp1 = wp1.lock(); //返回的是sp2 恢复sp1
sp1.reset(); //清空sp1
sp2.reset(); //清空sp2
cout << wp1.expired() << endl; //wp1 已经过期
sp1 = wp1.lock(); //过期后返回的是空
if (sp1) {
cout << "sp1 is not empty" << endl;
}
else {
cout << "sp1 is empty" << endl;
}
return 0;
}
运行结果:
智能指针的使用选择
- 如果需要多个对象共同维护资源,就使用
shared_ptr
,如果有造成循环引用的可能,就在对应位置修改为weak_ptr
维护。 - 如果只需要单个对象管理,就使用
unique_ptr
即可。
以上就是我在目前在智能指针学到的知识,不足之处还望指正。