在C语言中,我们初识了指针这一概念,在C++中,我们不断对于指针进行研究和使用,指针的存在是C/C++的一大特色,但是由于非规范操作,可能会导致指针并没有被释放,从而导致了内存泄漏、野指针等问题。
C++中为了解决这一问题,推出了智能指针这一概念
在C++98中推出了auto_ptr,第一代智能指针,来进行对于指针资源管理
在C++11中相继推出三种智能指针,unique_ptr、shared_ptr、weak_ptr
在学习异常之前,我们知道出现内存泄漏一般是由于指针不规范使用导致,但是异常的出现,可跳出当前作用域这一行为,也可以导致内存泄漏的发生。
//智能指针
void Add()
{
int* ptr = new int[10];//我们在堆上动态开辟空间
//如果上述ptr开辟空间失败,抛异常,被func中的catch抓住,就不会执行delete ptr
delete ptr;
}
void func()
{
try {
Add();
}
catch(const exception& e)
{
cout << e.what() << endl;//输出异常
}
}
int main()
{
func();
return 0;
}
所以,我们就算有时候操作很规范,也会可能导致内存泄漏,智能指针的出现就会解决这一问题
我们先介绍一些内存泄漏的危害性,在工程项目中,如果出现了内存泄漏,且每一次泄漏一小部分,很难被察觉,那么就会在启动该项目的一周,一个月后,发现该项目的运行越来越卡顿,最后项目崩溃。
如果内存泄漏很慢,这是最难被发现的
如果泄漏很快,说不定项目启动当天就能发现,从而解决
由内存泄漏导致出现的项目崩溃,是为事故,轻则奖金扣除,总则打包回家!!!
C/C++程序中主要是两个方面可能会出现内存泄漏,堆内存泄漏和系统资源泄漏
堆内存泄漏
堆内存泄漏,就是我们在程序中使用malloc/calloc/realloc/new等关键字从堆中进行分配内存资源,在该资源使用完毕后,没有合理使用free/delete等删掉该资源(释放资源),从而导致的内存泄漏。
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放
掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
规避内存泄漏方法
1.规范使用代码(但是异常还是可能会有内存泄漏的风险)
2.使用RAII思想或者智能指针来进行管理资源(申请和释放)
3.使用第三方检测工具来实现对于程序的内存泄漏的检查
智能指针的实现,使用了RALII的原理,即利用对象生命周期来控制程序资源的思想,用类进行包装,从而通过类自带的构造和析构函数,实现对于资源的申请和释放,从而避免内存泄漏
在对象构造的时候,进行资源的申请;在对象析构的时候,对资源进行释放
namespace why{
//设计实现简单的智能指针
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr=nullptr)
:_ptr(ptr)
{
//ptr = new int;//实现在构造的时候,申请资源
cout << "构造函数,申请资源" << endl;
}
~auto_ptr()
{
cout << "析构函数,释放资源" << endl;
delete _ptr;//在析构的时候,释放资源
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
int main()
{
why::auto_ptr<string> aaa;//默认构造
why::auto_ptr<int> a(new int);//在构造的时候进行申请资源
auto_ptr<string> a(new string("xxxx"));
return 0;
}
c++98中推出的智能指针,可以对于资源进行管理,特点是可以实现管理权的转移(转移后,将被转移的指针指向为nullptr),但是在某种情况下会出现野指针的风险
int main()
{
//使用智能指针auto_ptr
auto_ptr<string> ptr1(new string("xxx"));
auto ptr2 = ptr1;
cout << ptr1.get() << endl;
cout << ptr2.get() << endl;//get函数,得到二进制地址
cout << *ptr1 << endl;
return 0;
}
所以说,auto_ptr在设计上并不完善,对于上述情况,会造成的野指针报错,从而,不推荐使用auto_ptr
模拟实现基本的auto_ptr功能
namespace why
{ //模拟实现auto_ptr
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr=nullptr)
:_ptr(ptr)
{}
T& operator*()
{
return *_ptr;
}
T& operator->()
{
return _ptr;
}
auto_ptr(const auto_ptr<T>& ptr)
:_ptr(ptr._ptr)
{
delete ptr._ptr;
//ptr._ptr = nullptr;
}
~auto_ptr()
{
delete _ptr;
}
private:
T* _ptr;
};
}
unique_ptr是在c++11之后推出的智能指针,除了不能赋值拷贝,和auto_ptr功能了类似
namespace why
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr=nullptr)
:_ptr(ptr)
{}
T& operator*()
{
return *_ptr;
}
unique_ptr(const unique_ptr<T>& up) = delete;//防止赋值拷贝,以及赋值拷贝构造
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
T* operator->()
{
return _ptr;
}
~unique_ptr()
{
delete _ptr;
}
private:
T* _ptr;
};
}
int main()
{
why::unique_ptr<string> ptr1(new string("xxx"));
//why::unique_ptr ptr2;
//ptr2 = ptr1; //赋值拷贝
//
auto ptr3 = ptr1;//赋值拷贝+构造=拷贝构造,编辑器优化直接调用拷贝构造
//auto ptr2(ptr1);
return 0;
}
C++11推出的智能指针,其特点是可以多个指针对象共享一块资源(指向同一空间),是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
namespace why
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr=nullptr)
:_ptr(ptr)
,_pnum(new int(1))//初始计数为1,表示有一个指针指向这一空间
{
cout << "构造函数的实现" << endl;
}
T& operator*()
{
return &_ptr;
}
T* operator->()
{
return _ptr;
}
shared_ptr(const shared_ptr<T>& sp)//拷贝构造函数
{
cout << "拷贝构造函数的实现" << endl;
if (this != &sp)
{
_ptr = sp._ptr;
_pnum = sp._pnum;
++(*_pnum);
}
}
shared_ptr<T> operator=(const shared_ptr<T> sp)//赋值拷贝是先执行拷贝构造,然后再执行赋值
{
cout << "赋值拷贝的实现" << endl;
if (this == &sp)
{
return *this;
}
if (--(*_pnum) == 0)
{
delete _ptr;
delete _pnum;
}
_ptr = sp._ptr;
_pnum = sp._pnum;
++(*_pnum);
return *this;
}
~shared_ptr()
{
if (--(*_pnum) == 0)//如果为0,表示直接释放即可,如果不为零,也会减去1
{
cout << "析构函数的实现" << endl;
delete _ptr;
delete _pnum;
}
}
int getPcount()
{
return *_pnum;
}
private:
T* _ptr;
int* _pnum;//用来计数,记录指向同一地址的指针,有几个,防止多次对于同一地址进行释放空间,发生异常
};
}
int main()
{
why::shared_ptr<string> sp(new string("Xxx"));
auto sp2(sp);
why::shared_ptr<string> sp3;
sp3 = sp2;
cout << sp2.getPcount() << endl;//设置方法来获得此时共同管理的对象个数
return 0;
}
shared_ptr的实现,使得可以多个指针共同访问同一地址,相对于unique_ptr区别就是能不能多个对象对于同一空间进行资源管理。
但是对于shared_ptr也存在缺点,可能会导致死循环(不能释放空间),如下代码所示
class node
{
public:
//假如A类的成员变量中需要指针的存在,且需要多个对象能访问同一空间地址,选择shared_ptr
shared_ptr<node> prev;
shared_ptr<node> next;
};
int main()
{
//如果此时我们想要满足实现类似于链表之间的关系
shared_ptr<node> a(new node);
auto b = a;
a->next = b;
b->prev = a;
cout << b.get() << endl;
cout << a.get() << endl;
return 0;
}
为了解决shared_ptr指针循环引用的问题,引入weak_ptr
weak_ptr拥有的功能和shared_ptr类似,只是不采取引用计数,即不会去管理指针的生命周期问题,只是单纯的去使用,访问该资源,不会去管理。
weak_ptr一般是配合shared_ptr来使用的一种智能指针,通过这一指针,我们可以解决shared_ptr循环引用的问题
class node
{
public:
weak_ptr _prev;//不参与管理,不会采取引用计数的方式,所以就算是指向下一个节点,也不会使得计数++
weak_ptr _next;
};
int main()
{
shared_ptr a;
shared_ptr b;
a->_next = b;
b->_prev = a;//此时,虽然互相指向,但是a和b的引用计数use_count都是1,即可以直接释放
return 0;
}
weak_ptr还有很多其他的作用和功能,但是其主要是配合shared_ptr来使用
use_count 函数得到引用计数的数值
expired 函数判断当前weak_ptr指向的shared_ptr是否被销毁,返回类型是bool,判断依据是use_count==0是否成立
lock 函数是如果expired == true,表示weak_ptr管理的shared_ptr已经被销毁,返回nullptr,反之返回指向的shared_ptr
对于new申请的空间,我们可以通过delete进行直接删除,对于malloc、calloc、realloc等动态申请的空间,我们引入另一种概念,删除器
template<class T>
struct func {
public:
void operator()(T* t)
{
cout << "free" << endl;
free(t);
}
};
template<class T>
struct arrdelete {
void operator()(T* t)
{
cout << "delete" << endl;
delete[] t;
}
};
int main()
{
func<string> f;
arrdelete<string> a;
//shared_ptr sp1(new string("xxx"), f);
shared_ptr<string> sp2(new string[10]{"xxx"}, a);//仿函数实现
shared_ptr<string> sp3(new string[10]{ "Xxxx" }, [](string* ptr) {delete[] ptr; });//lambda函数实现
return 0;
}
所以如果是对于malloc、calloc、realloc等动态申请空间的指针,我们对于其进行删除管理的时候,需要使用仿函数/lambda函数,lambda函数是最为方便的。
shared_ptr s(new string[10]);
这一种申请方式会编辑报错,需要设计删除器材,delete[]删除,或者是free函数删除(malloc、calloc、realloc函数申请的空间资源),建议使用下面的方式来实现删除器
shared_ptr<string> s(new string[10], [](string* s) {delete[] s; }); shared_ptr<string> s((string*)malloc(4), [](string* s) {free(s); });
智能指针的出现,在项目中具有重要意义,可以自动的控制对象的声明周期,合理使用智能指针就不会出现内存泄漏的问题,在平时练习c++的过程中对于智能指针的要求并不高,可以直接使用普通指针即可。
在智能指针中,auto_ptr已经被抛弃了(风险高),主要是使用c++11之后推出的三种指针,unqiue_ptr、shared_ptr、weak_ptr,进行对于管理动态申请的空间/对象,但是要注意的是对于malloc、calloc、realloc申请的空间要实现删除器,不然会导致运行报错。
//如果是内置类型,就不需要删除器的存在
shared_ptr<string*> s(new string*(nullptr));//如果泛型类型是指针,那么就不需要删除器的存在
shared_ptr<string> s(new string[10]);//对象s为string类型的指针,我们new出来10个类型为string的数组,将地址给s的_ptr,这一种情况就需要删除器的存在,以为T类型为string 进入析构函数为delete _ptr,不能删除数组地址,所以会运行报错
删除器的存在与否,决定于泛型中T的类型,析构函数为delete _ptr(T*_ptr) 是否能删除new出来或者malloc的空间
即delete _ptr是否能匹配
shared_ptr<string> s(new string[10]); //string* _ptr -> 构造函数 -> _ptr=new string[10]; //析构函数 -> delete _ptr ->delete 数组应该为delete[]_ptr 所以不能匹配,不能正确释放,即运行报错 //所以是否需要删除器,关键是T的类型和delete _ptr以及构造函数的参数(上文的new string[10])