一般情况下,C++申请资源后都需要手动释放资源,一旦忘记资源的释放就会造成内存泄漏,为了解决内存泄漏问题,C++引入了RAII机制。
RAII是一种利用对象的生命周期来控制资源释放的技术。比如一个局部对象,出了作用域就被销毁,RAII利用这一特性将资源与对象绑定在一起,当局部对象释放时,绑定在其身上的资源也要被释放。
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
int* p1 = new int[10];
int* p2 = new int[10];
int* p3 = new int[10];
int* p4 = new int[10];
try
{
div();
}
catch (...)
{
delete[] p1;
delete[] p2;
delete[] p3;
delete[] p4;
throw;
}
delete[] p1;
delete[] p2;
delete[] p3;
delete[] p4;
}
int main()
{
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
// ...
}
return 0;
}
main函数调用func函数并捕获其中的异常,func函数申请资源后调用div函数并捕获其中的异常,div函数可能抛出异常。当资源被申请后,调用div函数时发生了异常,func函数就能捕获其异常,并释放之前申请的资源。
但程序还有问题,如果func函数只申请一次资源,申请失败时,main函数捕获申请失败的异常,这没有问题,但上面的func函数申请了四次资源,假如前三次申请资源成功,第四次申请资源失败,异常被抛出并被main函数捕获,但前三次申请的资源没有被释放,造成了资源泄漏。总不能在main函数中释放资源,p1到p4都是局部对象,出了作用域就销毁了,main函数想释放资源也不知道资源在哪。而使用RAII的思想就能解决这样的问题。
当申请资源时,就构造一个对象,在对象的声明周期内,资源能被正常使用,对象析构时,资源也随之释放。
智能指针就是RAII思想的一种具体实现。
智能指针的本质上就是通过一个类把指针封装起来,利用RAII 思想,使这个类代替指针的同时,避免内存泄漏的情况。
简单的说:智能指针就是对普通的裸指针进行了一层包装,包装之后,就使得这个指针更加智能,能够自动在合适时间帮你去释放内存。
C++标准库提供了四种智能指针的使用:
std::auto_ptr; c++98就有的一种智能指针,但是现在被遗弃,完全被std::unique_ptr取代。
下面三种都是C++11提供的新智能指针;
- std::unique_ptr:一种独占式智能指针,同一个时间内只能有一个指针指向该对象。
- std::shared_ptr:多个指针可以指向同一个对象的指针(最像指针的智能指针)。
- std::weak_ptr:一种辅助std::shared_ptr指针而存在的指针。
使用智能指针的时候,记得包含头文件#include
。
智能指针的使用方法和其它容器差不多,只是多了一些指针的特性,最主要的还是出现了-> 和* 操作符,但智能指针是不支持隐式构造的。
#include
using namespace std;
template
class My_Ptr
{
public:
My_Ptr(T* ptr)
:_ptr(ptr)
{}
~My_Ptr() {
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
int main() {
int* a=new int(10);
My_Ptr ptr(a);
cout << *ptr << endl;
return 0;
}
可以看出我们的指针可以有指针的特性,也可以在程序结束时自动销毁(这个其实很好理解吧,毕竟利用了类自动调用析构函数的特性)。
int main() {
int* a=new int(10);
My_Ptr ptr(a);
cout << *ptr << endl;
My_Ptr ptr2(a);
return 0;
}
int main() {
int* a=new int(10);
My_Ptr ptr(a);
cout << *ptr << endl;
My_Ptr ptr2(a);
return 0;
}
总结一下智能指针的特点:
1.不用显式地写出析构函数。
2.资源在智能指针的生命周期中始终有效。
3.可以像指针一样的使用。
4.最重要的是:具有RAII特性
上面的SmartPtr还存在着问题:拷贝构造对象和赋值时,多个SmartPtr指向同一块空间,当SmartPtr析构时便会造成资源的多次释放,导致程序崩溃。
比如我们将main函数中的内容改成这样:
int main() {
int* a=new int(10);
My_Ptr ptr(a);
cout << *ptr << endl;
My_Ptr ptr2(a);
return 0;
}
我们库中有四个智能指针,那么他们是怎么解决这个 这个问题的呢?我们一个个来看一下。
这个我都不想讲,谁用谁是大傻春。
auto_ptr对于拷贝问题的解决方案是:管理权的转移,比如将p1拷贝给p2,也就是将p1对于资源的管理权转移给了p2,即将p1的指针置空,带来的问题是:后面的代码不能使用p1对象。由于这个问题的存在auto_ptr指针被很多人诟病,并且许多公司明确要求不能使用auto_ptr指针,因为管理权转移导致了原指针不能使用,相比后续的智能指针,auto_ptr确实是个失败的设计。
#include
using namespace std;
int main() {
auto_ptr a1(new int(10));
auto_ptr a2(a1);
cout << *a1 << endl;
return 0;
}
oost作为C++的一个开源库,承担着C++新功能的开发,如果boost库中有好用的设计出现,C++的标准库便会将好用的设计引入,出现在下一次的更新中。
unique_ptr最初就是boost库中的scoped_ptr,C++的标准库汲取scoped_ptr中的精华并设计出了unique_ptr。unique_ptr解决智能指针拷贝问题的方案是:禁止拷贝.
可以看出,库中使用delete 禁止了unique的拷贝函数。
unique_ptr的模拟实现:
#pragma once
template
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(const unique_ptr& p) = delete; //直接用delete禁止生成就行了
unique_ptr& operator=(const unique_ptr& p) = delete;
~unique_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
在拷贝构造函数和赋值重载函数后加上delete,表示不能调用该函数,并且不能定义该函数。unique用这种简单粗暴的方式解决智能指针的拷贝问题,带来的缺陷是:unique独自占有资源,即不能使用拷贝构造和赋值,每一个unique都指向不同的资源。
shared_ptr允许拷贝智能指针,其采用了计数的方式进行拷贝,只有一个指针指向资源时,计数为1,两个指针指向,计数为2,以此类推。对象析构时,只要将计数减一,如果减一后的计数为0,说明该对象是指向资源的最后一个对象,需要完成资源的释放。
使用示例:
#include
using namespace std;
int main() {
shared_ptr s1(new int(10));
shared_ptr s2(s1);
cout << *s1 << endl;
cout << *s2 << endl;
return 0;
}
结果为:
我们都知道 shared_ptr 的底层使用了引用计数,那么它是怎么实现的呢? 实际上它用了一个int指针来进行引用计数,底层原理是这样的。
设_count为引用技术的int指针,_ptrx为每一个shared_ptr指针。
template
class shared_ptr
{
public:
shared_ptr(T* ptr)
: _ptr(ptr)
, _pCount(new int(1))
{}
shared_ptr(const shared_ptr& p)
{
_ptr = p._ptr;
_pCount = p._pCount;
(*_pCount)++;
}
//写一个函数判断是否应该释放空间
void Release()
{
if (--(*_pCount) == 0) // 当计数为0,需要释放pCount
{
if (_ptr) // 如果_ptr为空,只要释放pCount
{
delete _ptr;
_ptr = nullptr;
}
cout << "~shared_ptr()" << endl;
delete _pCount;
_pCount = nullptr;
}
}
shared_ptr& operator=(const shared_ptr& p)
{
// 指向资源不同时,使两指针指向同一资源,并且计数增加
if (_ptr != p._ptr) // 当指向资源相同时,没有必要进行赋值
{
Release(); // 先释放该指针之前指向的空间
_ptr = p._ptr;
_pCount = p._pCount;
*_pCount++;
}
}
~shared_ptr(){
Release();
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr; //实际上只是用了两个指针实现
int* _pCount;
};
use_count成员函数使用来统计有多少个shared_ptr指针指向同一份内存空间对象的.
#include
using namespace std;
int main() {
shared_ptr s1(new int(10));
shared_ptr s2(s1);
shared_ptr s3(s1);
shared_ptr s4(s1);
cout << s1.use_count() << endl;
cout << s4.use_count() << endl;
s1.reset();//删除函数
cout << s4.use_count() << endl;
return 0;
}
这个成员函数主要是判断:shared_ptr指针是否只有一个智能指针指向该对象,如果是:返回true,如果不是:返回false;
#include
using namespace std;
int main() {
shared_ptr s1(new int(10));
shared_ptr s2(s1);
shared_ptr s3(new int (1));
cout << s1.unique() << endl;
cout << s3.unique() << endl;
return 0;
}
'reset成员函数就是重置shared_ptr指针的的意思。
reset成员有两个重载版本: 第一个无参数的版本:重置该shared_ptr为空,同时引用计数减一,如果减到0就释放指针指向的内存空间.
第二个有参数的版本:重置shared_ptr指向为该参数的内存空间对象中,并且原来的内存空间对象的引用计数减一,如果减到0那么就释放该内存空间.
#include
using namespace std;
int main() {
shared_ptr s1(new int(10));
shared_ptr s2(s1);
shared_ptr s3(s1);
cout << s2.use_count() << endl;
s1.reset();//删除函数
cout << s2.use_count() << endl;
cout << *s3 << endl;
s3.reset(new int(20));
cout << *s3 << endl;
return 0;
}
C++的智能指针初始化的第二个参数,可以指定自定义的删除器,其实这个删除器就是一个函数指针,并且是单参数的函数指针,当然,你也可以传lambda表达式。
如果不指定第二个初始化的参数,那么就是使用默认的删除器,也就是直接delete的版本。
为什么要指定自己的删除器呢?
因为智能指针在管理数组指针时候,需要释放数组的内存,假如使用默认的删除器,也就是直接delete,那么就会导致内存泄漏了,所以需要自己指定自己删除器,去释放数组内存。
例如:
#include
using namespace std;
class A
{
public:
A()
{
cout << "A()构造函数执行" << endl;
}
~A()
{
cout << "A()析构函数执行" << endl;
}
};
int main()
{
shared_ptr p(new A[10]); //试图开辟10个A类的数组空间,用智能指针P去指向它;
//但是这会报错,报错原因就是默认删除器使用的delete p,
//这样只能析构一个数组元素,剩下的9个没有析构成功
//而我们需要的是delete[]p的方式释放内存,所以要自己指定删除器
return 0;
}
可以看出析构函数只执行了一次。
加入删除器后:
#include
using namespace std;
class A
{
public:
A()
{
cout << "A()构造函数执行" << endl;
}
~A()
{
cout << "A()析构函数执行" << endl;
}
};
int main()
{
shared_ptr p2(new A[10], [](A* p2) {
delete[] p2;
}); //用lambda表达式指定删除器
//这样就可以释放干净数组的内存了
//其实,删除器还有一种是C++ 标准库提供的类模板std::default_delete
//这种方式也可以用来删除数组
shared_ptr p3(new A[10], std::default_delete());
return 0;
}
在C++17提供了一种更加方便的方式来管理数组的,但是这种在C++11 和14都是不支持的,所以可能老的编译器会报错.
只在<>
尖括号 和()
小括号里面的类型都加上[ ]
中括号即可,如:
#include
using namespace std;
class A
{
public:
A()
{
cout << "A()构造函数执行" << endl;
}
~A()
{
cout << "A()析构函数执行" << endl;
}
};
int main()
{
shared_ptr p(new A[10]); //c++17就开始支持这种写法来管理数组
return 0;
}
- 首先我们得知道weak_ptr:是一种辅助shared_ptr的智能指针.(也就是说,weak_ptr本身是不可以被单独使用的, 不可以被单独使用的意思:weak_ptr
p(new int(10)) 这种方式是不可以创建weak_ptr对象的,这是错误的用法). - weak_ptr的对象只能指向一个由shared_ptr创建的对象,但是weak_ptr是不管理shared_ptr指针指向的对象内存的空间生存周期的.这个weak_ptr是不会增加shared_ptr的引用计数的。
- 也就是说shared_ptr所指向的对象该释放空间就释放空间,和weak_ptr没有关系,尽管weak_ptr还是指向该对象的内存空间,只要shared_ptr的引用计数为0,那么就会释放该对象内存空间.
我们知道weak_ptr就是用来辅助shared_ptr使用的,那么是如何辅助呢?
首先我们得认识什么是循环引用得问题。
我们先来假装创建连个list节点,让他俩互相指向。
#include
#include
using namespace std;
struct ListNode
{
int _data;
shared_ptr _prev;
shared_ptr _next;
~ListNode() {
cout << "~ListNode()" << endl;
}
ListNode() {
cout << "ListNode()" << endl;
}
};
int main()
{
shared_ptr node1(new ListNode);
shared_ptr node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;//这步会导致智能指针引用计数+1
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
代码会正常调用两次构造和两次析构吗?
很明显,当我执行这行代码的时候,并没有显示正确的两次析构函数,也就是说,这段代码出现了一个很严重的问题,那就是内存泄漏了。
这个就是循环引用带来的问题,导致内存泄漏,那到底什么是循环引用呢?
也就是shared_ptr管理资源内存时候,互相指向的问题,你的shared_ptr指向我的sahred_ptr,我的shared_ptr又指向你的shared_ptr。
循环引用分析:
- node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
- node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
- node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
- 也就是说_next析构了,node2就释放了。
- 也就是说_prev析构了,node1就释放了。
- 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev
- 属于node2成员,所以这就叫循环引用,谁也不会释放。
画个图更好理解上面的代码:
// 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
// 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和
_prev不会增加node1和node2的引用计数。
#include
#include
using namespace std;
struct ListNode
{
int _data;
weak_ptr _prev;
weak_ptr _next;
~ListNode() {
cout << "~ListNode()" << endl;
}
ListNode() {
cout << "ListNode()" << endl;
}
};
int main()
{
shared_ptr node1(new ListNode);
shared_ptr node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;//这步会导致智能指针引用计数+1
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}#include
#include
using namespace std;
struct ListNode
{
int _data;
weak_ptr _prev;
weak_ptr _next;
~ListNode() {
cout << "~ListNode()" << endl;
}
ListNode() {
cout << "ListNode()" << endl;
}
};
int main()
{
shared_ptr node1(new ListNode);
shared_ptr node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;//这步会导致智能指针引用计数+1
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
那 么原理是什么呢? 原理很简单,画个图就明白了。
实际上,weak_ptr 只是指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,你也可以相当于weak_ptr不是一个智能指针,只是一个普通指针,因此解决了循环引用的问题,weak_ptr更像是shared_ptr的一个助手而不是智能指针。