前言
这一节讲智能指针
1. 为什么需要智能指针
上一节我们就讲过,因为new有抛异常,所以可能会抛异常跳过了delete,导致内存泄漏,所以采用智能指针
智能指针采用的技术是RALL,就是是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术
2. 智能指针的简单实现
struct Date
{
Date(int year=0,int month=0,int day=0)
:_year(year)
,_month(month)
,_day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
int _year;
int _month;
int _day;
};
template<class T>
class my_ptr
{
public:
my_ptr(T*ptr=nullptr)
:_ptr(ptr)
{}
~my_ptr()
{
delete _ptr;//会调用_ptr的析构函数
_ptr = nullptr;
}
private:
T* _ptr;
};
void func()
{
my_ptr<Date> p(new Date);
throw 1;
}
int main()
{
try
{
func();
}
catch (...)
{
cout << "ssss" << endl;
}
return 0;
}
void func()
{
my_ptr<Date> p(new Date);
//throw 1;
}
其实智能指针就是将你开辟好的指向空间的指针传给一个类,用这个类来构造,然后重载进行使用,最后会自动调用析构函数析构
看这个我们就知道了,不管是程序正常结束,不抛异常,还是抛异常了,这个类都是会析构的
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
再定义两个这种的函数,就可以像普通指针那样使用了
但是智能指针最重要的问题还是拷贝构造问题
根据普通指针的逻辑,智能指针的拷贝应该是浅拷贝,但是,浅拷贝的话,同一个地方又会析构两次,有问题,所以我们要重点解决这个问题
3. 智能指针的发展以及使用
库里面的智能指针在memory这个头文件里
3.1 auto_ptr
它解决拷贝问题就比较垃圾,它是直接权限转移,意思是拷贝过去,原来的就不能正常使用了
auto_ptr<Date> p1(new Date);
auto_ptr<Date> p2(p1);
p2->_day++;
auto_ptr<Date> p1(new Date);
auto_ptr<Date> p2(p1);
p2->_day++;
p1->_day++;
my_ptr(const my_ptr&ptr)
:_ptr(ptr._ptr)
{
ptr._ptr = nullptr;
}
因为它的构造函数是这样使用的,所以就无法使用以前得了
这个智能指针比较垃圾,最好不要使用
3.2 unique_ptr
unique_ptr这个智能指针就是明确规定了不能使用拷贝构造和赋值
这个智能指针就是说,不能拷贝和赋值,因为它底层实现就直接把拷贝和赋值deletel
my_ptr(const my_ptr& ptr) = delete;
my_ptr&operator=(const my_ptr& ptr) = delete;
就相当于这样
3.3 shared_ptr
shared_ptr就支持拷贝构造和赋值,这是因为这个模版底层是采用了引用计数的技术的,就是对一块开辟的空间计数,看有几个指向它,当有0个指向它的时候就是释放的时候
shared_ptr<Date> p1(new Date);
shared_ptr<Date> p2(p1);
3.4 删除器
接下来我们再来看一下删除器,就是这么释放内存的问题,系统默认的是delete,但有些东西是用delete无法释放的,比如开辟一个数组,要用delete[],打开一个文件,要用fclose
unique_ptr<Date> p1(new Date[5]);
这时候我们就要加入删除器这个东西了,相当于以前加入的仿函数
unique_ptr的删除器是作为模版参数传入的,auto_ptr我们就不再介绍了,因为好多公司都禁止使用
作为模版参数的话,那么就要传入一个类型,而不是对象
template<class T>
struct del
{
void operator()(T* d)
{
delete[]d;
}
};
unique_ptr<Date, del<Date>> p1(new Date[5]);
就这样传入删除器就可以删除其他类型的了
unique_ptr<Date[]> p1(new Date[5]);
但是针对delete[]的情形,模版有一个特化,就是第一个模版参数传入Date[]的时候,就会特化一个模版。用delete,而且shared_ptr也是这样的
shared_ptr<Date> p1(new Date[5], [](Date* p) {delete[]p; });
还要注意的就是,shared_ptr的删除器的传入是作为构造函数的参数传入的,所以传入的是对象,当然针对delete[],也可以特化
比如这个例子
shared_ptr<FILE> sp5(fopen("test.cpp", "r"), Fclose());
3.5 shared_ptr的其他函数的使用
先讲use_count,use_count就是返回引用计数的计数为多少而已
shared_ptr<Date> p1(new Date);
shared_ptr<Date> p2(p1);
shared_ptr<Date> p3(p1);
cout << p1.use_count() << endl;
注意一下,智能指针的构造是不支持像这样隐式类型转换的
再讲一下make_shared
make_shared的作用就相当于原来的make_pair
make_shared是一个模版,返回的是是一个shared_ptr
而且具有三个点,说明可以传多个参数
shared_ptr<Date> p = make_shared<Date>(1, 1, 1);
(1, 1, 1)就是去构造Date传的参数
这个有什么好处呢,如果没有用这个的话
那么Date开辟的空间,和计数(这个也是要开空间的)开辟的空间就不在一起,如果shared_ptr使用太多的话,就会出现内存碎片化的问题(小空间太多了),但是make_shared就是将这两块空间挨着一起开辟的,就不容易出现内存碎片化的问题。
4. shared_ptr的实现
由于前面几个智能指针太简单了,所以我们实现这个就可以了
namespace bit
{
template<class T>
class my_ptr
{
public:
//再添加删除器
/*my_ptr(T* ptr=nullptr)
:_ptr(ptr)
,_count(new int(1))
{
if (ptr == nullptr)
{
*_count = 0;
}
}*/
my_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _count(new int(1))//会自动走初始化列表,没写的也会走初始化列表
{
if (ptr == nullptr)
{
*_count = 0;
}
}
//要添加删除器的时候,必须还要留一个原来的一个参数的构造函数,因为构造是可以传一个参数和两个参数的(包含删除器)
//如果总共就写一个构造函数,就是下面这个构造函数,那么就传一个参数的时候,d的类型无法推导,是不会走这个构造函数的,
//相反还会走拷贝构造
template<class d>//自己推类型的
my_ptr(T* ptr, d del)
:_ptr(ptr)
,_del(del)
, _count(new int(1))
{
if (ptr == nullptr)
{
*_count = 0;
}
}
my_ptr(const my_ptr<T>& ptr)
{
_ptr = ptr._ptr;
_count = ptr._count;
++*(_count);
}
void release()//因为this指针一直传
{
--(*_count);
if ((*_count) == 0)
{
_del(_ptr);//会调用_ptr的析构函数
_ptr = nullptr;
}
}
my_ptr&operator=(const my_ptr& ptr)
{
//要先对以前的资源处理
release();
if (_ptr != ptr._ptr)
{
_ptr = ptr._ptr;
_count = ptr._count;
++*(_count);
}
return (*this);
}
~my_ptr()
{
release();
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
int use_count()
{
return *_count;
}
T* get()//get是获得底层的那个指针
{
return _ptr;
}
private:
T* _ptr;
int* _count;
//接受删除器
function<void(T* p)> _del = [](T* p) {delete p; };
};
}
5. weak_ptr
我们先来看一些程序
class fun
{
public:
fun()
{
}
~fun()
{
cout << "~func()" << endl;
}
shared_ptr<fun>* next;
shared_ptr<fun>* prev;
};
shared_ptr<fun> p1(new fun);
shared_ptr<fun> p2(new fun);
shared_ptr<fun> p1(new fun);
shared_ptr<fun> p2(new fun);
p1->next = p2;
shared_ptr<fun> p1(new fun);
shared_ptr<fun> p2(new fun);
p1->next = p2;
p2->prev = p1;
我们可以看出,最后一个程序析构失败了,为什么呢
第一个正常
第二个,因为后创建的p2,所以就会先析构
先析构p2
所以p2的计数就会变成1
然后析构p1,析构p1的话,计数变为0,变为0的话就会delete所对应空间
delete还会调用其析构函数,析构成员就是调用成员变量的析构函数,所以就变成了析构_next,那么右边的计数就会变为0,然后再delete右边,就这样两个fun都释放了
像prev这种没有指向对象,是不会去释放的,是不会去打印的
第三种场景就是相互指向
先析构p2,所以就只有计数变为1
不会delete空间,更不会调用其成员变量的析构函数
在析构p1,也只是计数变为1
然后就结束了
最终计数都没有变为0
最终都没有释放
就这样内存泄漏了
这种场景叫做循环引用
出现这个的原因就是内部的赋值,增加了计数
如果内部的计数不会增加计数的话,就不会内存泄漏
所以我们引用weak_ptr
class fun
{
public:
fun()
{
}
~fun()
{
cout << "~func()" << endl;
}
weak_ptr<fun> next;
weak_ptr<fun> prev;
};
int main()
{
shared_ptr<fun> p1(new fun);
shared_ptr<fun> p2(new fun);
p1->next = p2;
p2->prev = p1;
return 0;
}
weak_ptr可以来用shared_ptr来赋值,而且不会增加计数,其实weak_ptr的底层还是有计数的,是自己单独的计数。但不是shared_ptr的那个计数
比如你看它还有这个接口呢,主要是看有几个weak_ptr
接下来我们实现weak_ptr
我们这里不实现weak_ptr自己的计数
template<class T>
class weak_ptr
{
public:
weak_ptr()
{
;
}
weak_ptr(const my_ptr<T>&ptr)
:_ptr(ptr.get())
{}
weak_ptr& operator=(const my_ptr<T>& ptr)
{
_ptr = ptr.get();
return *this;
}
private:
T* _ptr;
};
}
bit::my_ptr<fun> p1(new fun);
bit::my_ptr<fun> p2(new fun);
p1->next = p2;
p2->prev = p1;
接下来我们在介绍几个weak_ptr的接口
这个函数主要是看weak_ptr指向的内容是否还有效
weak_ptr<Date> s2;
{
shared_ptr<Date> s3(new Date);
s2 = s3;
cout << s2.expired() << endl;
}
cout << s2.expired() << endl;
这个就表示里面的时候指向的有效,外面的时候指向的无效,因为s3析构了嘛。weak_ptr根本挡不住
这个函数的作用就是将weak_ptr指向的shared_ptr作为函数的返回值
shared_ptr<Date> s1;
weak_ptr<Date> s2;
{
shared_ptr<Date> s3(new Date);
s2 = s3;
cout << s2.expired() << endl;
s1 = s2.lock();
}
cout << s2.expired() << endl;
将weak_ptr指向的shared_ptr赋值给了s2,所以计数会增加,s2的生命周期还在,那么weak_ptr指向的就有效
6. 内存泄漏
在我们现在这个阶段,就算是内存泄漏了,也没事,因为程序正常结束了的话,就会系统自己去释放
但是如果程序没有正常结束呢,或者这个程序是长期运行的了?
内存泄漏很危险,内存泄漏太多,就会导致卡顿
最危险的是每次都泄漏一点点,那么时间久了就会很卡顿
总结
下一节讲类型转换和特殊类