目录
智能指针的重要性
看完以上内容,相信小伙伴们已经清楚智能指针在校招面试中的重要性了,那么本文就是主要带你掌握智能指针的底层原理,帮助你深度理解智能指针以及学习智能指针的应用及原理
一、RAII思想
1、出现的问题
在我们平时向系统申请动态内存的时候,总是容易出现一些问题,比如说,申请资源之后,在不需要使用的时候,忘记释放资源了,即常见的new完之后,忘记delete,或者malloc之后忘记free了,在我们学习完异常的机制之后,我们知道,还有一种情况就是,在资源申请之后可能会出现异常什么的,那么当异常被捕获之后,就会直接跳转到异常捕获处,这种情况下也容易造成资源的不能释放,同样会导致内存泄露
2、RAII思想
RAII(Resource Acquisition Is Initialization):英文名称翻译为,资源获得即初始化,这个是指,当我们向系统申请成功之后,马上利用这个资源来初始化一个对象,初始化方法就是调用这个类的构造函数,参数为这个资源,因此,在这个对象有效的声明生命周期之内,这些资源都是有效的,当这个资源出生命周期之后,这个对象会调用这个类的析构函数完成对这个对象中的资源进行释放。通过这样的方法,我们知道,本质就是将获得的资源(向系统动态申请的资源)交给一个类的对象进行管理,获得资源时调用类的构造函数进行初始化,对象销毁时调用类的析构函数对对象中的资源进行释放,因此,这样一来我们就不用害怕资源的忘记释放了
使用RAII思想来控制一个类
template <class T>
class smart_ptr
{
public:
// RAII思想
// 构造函数
smart_ptr(T* ptr)
:_ptr(ptr)
{}
// 析构函数
~smart_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
private:
T* _ptr;
};
二、内存泄露
上述内容出现的问题一直在说最终的结果会导致内存泄露,那么什么是内存泄露呢??
常见的内存泄露分为两类:
1、堆内存泄露
堆内存指的是程序执行中依据须要分配通过malloc
/ calloc
/ realloc
/ new
等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
需要我们清楚的是,内存泄露不是内存真正泄露了,我们说的内存泄露指的是我们失去了对某一块内存的控制,这样就导致这块内存无法继续被使用,从而会导致资源的浪费,当失去控制的内存数量多了,就会导致系统可用的内存越来越少,最终就会导致程序卡死。
2、系统资源泄漏(这个暂时还没学习)
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
如何避免内存泄露??
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。
- 采用RAII思想或者智能指针来管理资源。
- 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵
三、智能指针的底层原理
智能指针的底层原理本质就是利用RAII思想
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效
实现一个简单的智能指针
简单使用该智能指针
// 实现智能指针
template <class T>
class MySmartPtr
{
public:
// RAII思想
// 构造函数
MySmartPtr(T* ptr)
:_ptr(ptr)
{
}
// 析构函数
~MySmartPtr()
{
if (_ptr)
{
delete _ptr;
}
}
// 实现像指针一样的行为
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
```cpp
int main()
{
// 普通的方法
// int* p = new int(1);
// delete p;
// 利用智能指针
smart_ptr<int> sp(new int(1));
return 0;
}
// 实现一个日期类
class Date
{
public:
Date(int year = 1,int month = 1,int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
int _year;
int _month;
int _day;
};
// 管理整形资源
int main()
{
MySmartPtr<int> sp1(new int(1));
cout << *sp1 << endl;
*sp1 = 2;
cout << *sp1 << endl;
// 实现了*所以可以通过*来改变智能指针中的内容
return 0;
}
// 管理日期类资源
int main()
{
MySmartPtr<Date> sp1(new Date(2022, 10, 30));
cout << sp1->_year << endl;
cout << sp1->_month << endl;
cout << sp1->_day << endl;
sp1->_year = 2000;
sp1->_month = 12;
sp1->_day = 24;
cout << sp1->_year << endl;
cout << sp1->_month << endl;
cout << sp1->_day << endl;
return 0;
}
分析:上述的代码中,如果采用的是普通方法来申请管理资源,则需要匹配地进行释放,如果是利用一个类(智能指针)来管理资源,则不需要进行释放,因为,当智能指针对象在出作用域之后会自动调用智能指针类中的析构函数完成对对象中的资源的释放
四、C++中的几种常见的智能指针:auto_ptr,unique_ptr,shared_ptr,weak_ptr
auto_ptr,unique_ptr,shared_ptr
三者的不同之处在于实现的拷贝和赋值运算符重载函数的不同
1、auto_ptr
:进行管理权的转移
auto_pt
r在拷贝构造函数的实现过程中会进行资源管理权的转移,即被拷贝对象中的对资源的管理权会转移到新对象上,这样的话,原来的对象就不能继续管理原来那一份资源,所以,如果不小心再通过原来的对象来访问,将会导致对空指针的访问出现程序崩溃的现象
auto_ptr的模拟实现
template <class T>
class auto_ptr
{
public:
// RAII思想
// 构造函数
auto_ptr(T* ptr)
:_ptr(ptr)
{}
// 析构函数
~auto_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
// 实现像指针一样的行为
// *
T& operator*()
{
return *_ptr;
}
// ->
T* operator->()
{
return _ptr;
}
//拷贝构造函数和赋值运算符重载函数
// 拷贝构造函数
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
// 赋值运算符重载函数
auto_ptr& operator=(auto_ptr<T>& sp)
{
_ptr = sp._ptr;
sp._ptr = nullptr;
return *this;
}
private:
T* _ptr;
};
练习使用auto_ptr管理整形资源
int main()
{
// 利用auto_ptr管理整形资源
hjt::auto_ptr<int> ap1(new int(1));
// *的练习
cout << *ap1 << endl;
*ap1 = 2;
cout << *ap1 << endl;
// 拷贝构造函数
hjt::auto_ptr<int> ap2(ap1);
cout << *ap2 << endl;
*ap2 = 3;
// 赋值运算符重载函数
// 创建一个对象
hjt::auto_ptr<int>ap3(new int);
ap3 = ap2;
cout << *ap3 << endl;
*ap3 = 4;
cout << *ap3 << endl;
return 0;
}
实验结果
上述代码属于正常情况,但是如果在拷贝或者赋值之后通过原来的对象对原来的资源进行访问则会出错
正常访问
对原来的对象进行访问
总结:
auto_ptr的拷贝和赋值运算符重载都是将自己对原来资源的管理权转移到新生成的对象,自己中的指针置成空指针,因为像auto_ptr类中的设计原理,一个资源只能由一个对象来进行管理,否则将会造成同一份资源析构释放多次,就会导致程序崩溃
2、unique_ptr:禁止该类的对象进行拷贝构造和赋值(利用C++11提供的关键字delete)
unique_ptr的模拟实现
C++98版本
template <class T>
class unique_ptr
{
public:
// RAII思想
// 构造函数
unique_ptr(T* ptr)
:_ptr(ptr)
{}
// 析构函数
~unique_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
// 实现像指针一样的行为
// *
T& operator*()
{
return *_ptr;
}
// ->
T* operator->()
{
return _ptr;
}
// 实现拷贝构造和赋值
// 其实禁止拷贝构造和赋值有C++98和C++11两种方法
// C++98:对拷贝构造函数和赋值运算符重载函数进行只声明不定义,并且将其置成私有访问
private:
unique_ptr(const unique_ptr<T>& sp);
unique_ptr<T>& operator=(const unqiue_ptr<T>& sp);
private:
T* _ptr;
};
C++11版本
template <class T>
class unique_ptr
{
public:
// RAII思想
// 构造函数
unique_ptr(T* ptr)
:_ptr(ptr)
{}
// 析构函数
~unique_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
// 实现像指针一样的行为
// *
T& operator*()
{
return *_ptr;
}
// ->
T* operator->()
{
return _ptr;
}
// 实现拷贝构造和赋值
// 其实禁止拷贝构造和赋值有C++98和C++11两种方法
// C++11
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
T* _ptr;
};
C++98中的防止拷贝和赋值是存在一定的风险的,所以一般建议使用C++11提供的关键字进行防止拷贝
练习使用unique_ptr
如果对unqiue_ptr进行拷贝或者赋值,将会报编译错误
3、shared_ptr:使用引用计数的方法来控制拷贝构造和运算符重载
代码实现
template <class T>
class shared_ptr
{
public:
// RAII 思想
// 构造函数
shared_ptr(T* ptr)
:_ptr(ptr)
,_pCount(new int(1))
{}
// 析构函数
shared_ptr()
{
Release();
}
// 释放资源
void Release()
{
if (--*_pCount == 0 && _ptr)
{
delete _ptr;
delete _pCount;
_ptr = nullptr;
_pCount = nullptr;
}
}
// 实现像指针一样的行为
// *
T& operator*()
{
return *_ptr;
}
// ->
T* operator->()
{
return _ptr;
}
// 实现拷贝构造函数和赋值运算符重载函数
// 拷贝构造函数
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pCount(sp._pCount)
{
(*_pCount)++;
}
// 赋值运算符重载函数
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
// 在进行赋值之前要记得将对象中原来的资源释放掉,否则将导致内存泄露
Release();
_ptr = sp._ptr;
_pCount = sp._pCount;
(*_pCount)++;
}
return *this;
}
int GetCount()
{
return *_pCount;
}
private:
T* _ptr;
int* _pCount; //记录每一份资源管理的对象的数目
};
练习使用shared_ptr
int main()
{
// 使用shared_ptr来管理整形资源
hjt::shared_ptr<int> sp1(new int(1));
cout << sp1.GetCount() << endl;
hjt::shared_ptr<int> sp2(sp1);
cout << sp2.GetCount() << endl;
hjt::shared_ptr<int> sp3(new int);
sp3 = sp2;
cout << sp3.GetCount() << endl;
return 0;
}
实验结果
总结
1、一般不推荐使用auto_ptr,很多公司也是明确禁止使用auto_ptr,因为在使用的过程中很容易对原来的对象进行访问从而造成程序崩溃
2、如果在使用的过程中不需要进行拷贝构造或者赋值,一般推荐使用unqiue_ptr比较安全
3、如果需要进行拷贝和赋值,则推荐使用shared_ptr,但是shared_ptr也有不足之处,即存在循环引用计数的问题