一、内存泄漏
在前文中我们提到由于c++没有垃圾回收器,内存泄漏问题会用智能指针解决,不妨先看一个没有智能指针防止内存泄漏的例子。
double Division(int a, int b)
{
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
int* p1 = new int[10];
int* p2 = nullptr;
//p2 new的过程中会抛异常
try
{
p2 = new int[20];
//函数Division会有除0错误抛异常
try
{
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl; // throw
}
//捕获除0错误 要释放p1 p2
catch (...)
{
delete[] p1;
cout << "delete:" << p1 << endl;
delete[] p2;
cout << "delete:" << p2 << endl;
throw; // 捕获什么抛出什么
}
}
//除了除0错误 其他错误捕获 释放p1并抛异常
catch (...)
{
delete[] p1;
cout << "delete:" << p1 << endl;
throw;
}
//函数没问题 依然释放p1 p2
delete[] p1;
cout << "delete:" << p1 << endl;
delete[] p2;
cout << "delete:" << p2 << endl;
}
会发现函数有时候还未执行完就会出现各种状况,使函数无法正常执行完毕,导致申请的空间不会因为函数的正常返回释放资源,就会导致内存泄露。
这时候每 try 一次,在 catch 里面就要手动释放一次资源,一旦申请资源过多,手动释放就会极其麻烦。
二、智能指针原理
原理一:RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做法有两大好处: 不需要显式地释放资源。 采用这种方式,对象所需的资源在其生命期内始终保持有效。
原理二:重载operator*和opertaor->,具有像指针一样的行为。
对象在出了作用域时就会调用析构函数,利用这一特性,我们可以封装一个指针类,构造时保存指针,析构时释放指针指向的资源。做到自动等对象销毁调用析构函数,从而防止内存泄漏。
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
T* GetPtr()
{
return _ptr;
}
~SmartPtr()
{
delete[] _ptr;
cout << "delete: " << _ptr << endl;
}
T& opreator* ()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
T* _ptr;
};
//智能指针版
void Func()
{
SmartPtr<int> sp1(new int[10]);
//如果sp2 new抛异常 sp1自动释放
SmartPtr<int> sp2(new int[20]);
//如果Division函数抛异常,跳到catch地方
//出作用域指针sp1 sp2自动释放
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
这样即使遇到除0错误会跳转到 catch 的地方也不用担心内存没有释放的问题。
三、智能指针的种类
1、c++98 auto_ptr
非常不推荐使用。当使用拷贝构造时会导致原来的指针失去对原有资源的管理权,导致悬空。很像移动构造,但是将亡值可以随意移动资源,对于这里的左值指针是万万不能随意转移管理权的。
最后想打印 sp1 指向的资源就会报错。
2、c++11 unique_ptr
不支持拷贝构造。
get():获取指针
operator bool:函数重载,若指针为空返回false
由于不支持拷贝构造,指向一份资源的只有一个指针,所以是 unique,但是很多时候需要多个指针指向同一块资源,所以引入 shared_ptr
3、c++11 shared_ptr
底层使用引用计数来实现多指针指向同一资源。
由于多指针指向同一份资源,资源只能释放一次,规定指针销毁引用计数-1,减到0调用析构函数(准确说是删除器)。
内存级示意图:
所以 make_shared 构造的对象可以减少内存碎片。
4、c++11 weak_ptr
辅助 shared_ptr 解决循环引用问题。文章后面详解。
四、模拟实现 shared_ptr
1、定制删除器
由于 shared_ptr 底层默认调用 delete 来删除指针指向的资源。
但是如果今天我用的是 FILE* 的文件指针,用 delete 就不能完成删除资源的工作。
所以我们需要了解定制删除器(本质就是函数指针,仿函数,lambda表达式)。
上图中构造函数中的 D del 就是定义删除器。
2、代码实现
#pragma once
#include<atomic>
#include<functional>
namespace bit
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
//构造时new一块空间存引用计数,初始化成1
, _pcount(new atomic<int>(1))
{}
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
//构造时new一块空间存引用计数,初始化成1
, _pcount(new atomic<int>(1))
,_del(del)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
//引用计数加1
(*_pcount)++;
}
int use_count()
{
return *_pcount;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//直接判断资源是否相同,避免自己给自己赋值
if (_ptr != sp._ptr)
{
//空间不是你this一个人的,你要修改先判断引用计数
//如果没有,引用计数不会到0,内存泄漏
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
return *this;
}
void release()
{
if (--(*_pcount) == 0)
{
//最后一个管理对象释放资源
_del(_ptr);
delete _pcount;
}
}
T& operator* ()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~shared_ptr()
{
//if (--(*_pcount) == 0)
//{
// //最后一个管理对象释放资源
// delete _ptr;
// delete _pcount;
//}
release():
}
private:
T* _ptr;
atomic<int*> _pcount;
//包装删除器
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
}
3、解读代码
(1)构造函数
构造时 new 一块空间存引用计数,初始化成1,这样之后拷贝的对象就能看到同一个引用计数。
(2)拷贝构造
初始化 + _pcount个数加1
(3)release函数
如果引用计数减到0就释放 _ptr _pcount
(4)赋值重载
先判断指向的资源是否相同,不同才能赋值 + _pcount个数加1
(5)析构函数
调用 release 函数
(6)私有成员
由于 ++ -- 操作不是原子的,为了保证线程安全,可以加锁,也可以直接定义 atomic 保证 _pcount 原子性。
定义一个返回值 void 参数 T* 的删除器,用 function 包装,默认用 delete 删除。
4、包装删除器用法
class A
{
public:
A(int a1 = 0, int a2 = 0)
:_a1(a1)
, _a2(a2)
{}
~A()
{
cout << "~A()" << endl;
}
private:
int _a1 = 1;
int _a2 = 2;
};
int main()
{
shared_ptr<A> sp1((A*)malloc(sizeof A), [](A* ptr) {free(ptr); });
shared_ptr<A> sp2(new A[10], [](A* ptr) {delete[] ptr; });
shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr); });
return 0;
}
最常用定义删除器就是 lambda 表达式
五、shared_ptr 循环引用问题
1、问题介绍
为什么没有调用到析构函数?
2、图解
3、c++11 weak_ptr
构造函数中有一个用 shared_ptr 构造 weak_ptr,weak_ptr 不支持RAII,不单独管理资源,辅助 shared_ptr 解决循环引用问题。
解决原理
weak_ptr 赋值,拷贝时指向资源,但不增加 shared_ptr 引用计数。
看到调用了析构函数。
expired 判断指针是否过期,本质看引用计数是否为0,防止引用计数为0时,weak_ptr 访问资源。