C++知识点 – 智能指针
文章目录
- C++知识点 -- 智能指针
- 一、智能指针的使用及原理
- 1.使用场景
- 2.RAII
- 3.智能指针的设计思想
- 4.智能指针的拷贝问题
- 二、auto_ptr
- 三、unique_ptr
- 四、shared_ptr
- 1.模拟实现
- 2.shared_ptr的循环引用
- 五、weak_ptr
- 六、定制删除器
- 七、内存泄漏
- 1.什么是内存泄漏
- 2.内存泄露的危害
- 3.如何避免内存泄漏
一、智能指针的使用及原理
1.使用场景
对于上面的场景,p1和p2在new申请空间后,div函数如果出现了除0错误,那么程序就会抛出异常,跳到接受异常的程序段继续执行,p1和p2申请的空间就没有被正常释放,造成了内存泄漏;
这种场景我们就可以使用智能指针来解决空间的释放问题。
2.RAII
RAII(Resource Acquisition Is Initialization)获取到资源立即初始化,是一种利用对象生命周期来控制程序资源的技术;
在对象构造时获取资源,接着控制对资源的访问使其在对象的生命周期内都有效,最后在对象析构时释放资源,我们实际上把管理一份资源的责任托管给了一个对象,好处在于:
(1)不需要显式地释放资源;
(2)采用这种方式,对象所需的资源在其生命周期内始终有效;
3.智能指针的设计思想
(1)利用RAII的思想设计delete资源的类;
(2)像指针一样的行为;
因此,智能指针实际上是一个对象,这个对象重载了operator->和operator*,具有像指针一样的行为;
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
: _ptr(ptr)
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
T* _ptr;
};
上面的代码就是一个简易的智能指针,能够实现资源的释放,以及像指针一样的行为;
double Div(int a, int b)
{
if (b == 0)
{
throw invalid_argument("除0错误");
}
return (double)a / b;
}
void test1()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << Div(2, 0) << endl;
}
int main()
{
try
{
test1();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
使用智能指针对上面的代码进行重写,让智能指针托管开辟的新资源,当sp1和sp2析构时,其析构函数会delete其管理的资源;
4.智能指针的拷贝问题
编译器自动生成的拷贝构造函数对于内置类型会完成浅拷贝,这里的拷贝需要的就是浅拷贝,因为深拷贝会将管理的资源在其他地方拷贝一份,违背了功能需求;
但是浅拷贝又带来了delete多次造成程序崩溃的问题,因此c++库中设计了几种智能指针来解决拷贝问题;
二、auto_ptr
这是c++98版本中提供的智能指针,auto_ptr对于拷贝的处理是管理权转移;
下面是它的模拟实现:
template <class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
: _ptr(ptr)
{}
auto_ptr(auto_ptr<T>& ap)
: _ptr(ap._ptr)
{
//管理权转移
ap._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
//检测是否为自己给自己赋值
if (this != &ap)
{
//释放当前对象中的资源
if (_ptr)
{
delete _ptr;
}
//转移ap中的资源到当前对象
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
T* _ptr;
};
auto_ptr对于拷贝的处理方式是管理权转移,对于拷贝构造,auto_ptr将资源的管理权转移给新的对象,原来的对象的指针置空;
对于赋值重载,auto_ptr清空等号左边的对象的资源,然后将等号右边的对象管理的资源转交给左边的对象,右边对象的指针置空;
auto_ptr对于智能指针拷贝的处理不是很好,所以很多项目都会禁止使用auto_ptr;
三、unique_ptr
c++11中开始提供unique_ptr,对拷贝的处理方式就是禁止拷贝;
下面是它的模拟实现:
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
: _ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
unique_ptr(const unique_ptr<T>& up) = delete;//只声明不实现
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
private:
T* _ptr;
};
unique_ptr将拷贝构造和赋值重载都进行了只声明不实现的操作,这样类中就不会生成默认的拷贝构造和赋值重载了,也就禁止了unique_ptr对象间的拷贝;
四、shared_ptr
shared_ptr支持拷贝,原理是通过引用计数的方式来实现多个shared_ptr对象之间共享资源:
(1)每个资源都维护着一份计数,用来记录该份资源被几个对象共享;
(2)在对象被销毁时,对象引用计数减一;
(3)如果引用计数是0,就说明自己是最后一个被销毁的对象,就必须释放该资源;
(4)如果引用计数不是0,就不能释放该资源;
1.模拟实现
如何实现多个对象共享一个引用计数呢?不能将引用计数设为static成员变量,因为这样所有的资源对象都是用一个引用计数;可以将引用计数的成员便变量设置成一个指针,只在对象构造的时候new一个引用计数,在拷贝和赋值的时候,只增加引用计数;
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pCount(new int(1)) // 只有在对象调用构造函数的时候,才会新建一个管理资源的引用计数
{}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pCount(sp._pCount) // 拷贝构造的时候不新建引用计数,只拷贝资源指针和计数指针,并且计数++
{
*(_pCount)++;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
--(*_pCount); //指针指向新的资源,原来指向的资源计数需要--
_ptr = sp._ptr;
_pCount = sp._pCount;
(*_pCount)++;
return *this;
}
private:
T* _ptr;
//引用计数
int* _pCount;
};
上面代码中的赋值重载是有问题的:
在这种情况下,sp1,2,3全部都赋值sp5,原先sp1,2,3指向的资源的引用计数就减为了0,但是上面的赋值重载函数并不能将原先的资源释放掉,就造成了内存泄露;
同时,还需要解决自己给自己赋值的问题(这里还要注意指向同一份资源之间的对象相互赋值);
void Release()
{
if (--(*_pCount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pCount;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr == sp._ptr) //判断指向的资源是不是同一块资源
{
return *this;
}
Release();
_ptr = sp._ptr;
_pCount = sp._pCount;
(*_pCount)++;
return *this;
}
整体模拟实现如下:
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pCount(new int(1)) // 只有在对象调用构造函数的时候,才会新建一个管理资源的引用计数
{}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pCount(sp._pCount) // 拷贝构造的时候不新建引用计数,只拷贝资源指针和计数指针,并且计数++
{
*(_pCount)++;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
void Release()
{
if (--(*_pCount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pCount;
}
}
~shared_ptr()
{
Release();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr == sp._ptr) //判断指向的资源是不是同一块资源
{
return *this;
}
Release();
_ptr = sp._ptr;
_pCount = sp._pCount;
(*_pCount)++;
return *this;
}
int use_count()
{
return *_pCount;
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
//引用计数
int* _pCount;
};
2.shared_ptr的循环引用
如上图所示,这种情况下,左右两个节点都由shared_ptr n1、n2管理,节点中的指针同样也是shared_ptr,左边节点的next指向右边节点,右边节点的prev指向左边节点,也就是next管着右边节点的内存块,prev管着左边节点的内存块,那么左右两个节点的引用计数都是2;
如果仅仅析构n1和n2,那么左右两节点的引用计数都会减为1,不会触发资源的回收,因为还有next和prev在指向着资源;只有在左边节点被释放,调用了析构函数时,next才会被释放,右边节点的引用计数才能减到0,才能够释放,显然无法直接释放左边的节点;
为了解决shared_ptr的循环引用问题,c++11引入了weak_ptr;
五、weak_ptr
weak_ptr不是常规的智能指针,没有RAII,不直接管理资源,weak_ptr主要用shared_ptr构造,用来解决shared_ptr的循环引用问题;
weak_ptr访问资源时,不增加引用计数,将节点成员的指针next和prev设置成weak_ptr就不存在循环引用的问题了;
模拟实现
template<class T>
class weak_ptr
{
public:
weak_ptr()
: _ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
: _ptr(sp.get())
{}
weak_ptr(const weak_ptr<T>& wp)
: _ptr(wp._ptr)
{}
weak_ptr<T>& operator=(const weak_ptr<T>& wp)
{
_ptr = wp._ptr;
return *this;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
T* _ptr;
};
六、定制删除器
如果自定义类型使用new [ ]申请空间,使用delete释放可能会出错;
delete[]是在开空间时多开4byte的空间在头部,储存new的对象的个数,在delete[]的时候就知道要调用多少次析构函数了;
shared_ptr和unique_ptr都支持定制删除器;
shared_ptr需要在调用构造函数初始化时传一个仿函数对象或者lambda表达式;
unique_ptr需要传模板参数为仿函数;
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
cout << "delete:" << ptr << endl;
delete[] ptr;
}
};
template<class T>
struct Free
{
void operator()(T* ptr)
{
cout << "delete:" << ptr << endl;
free(ptr);
}
};
int main()
{
//调用构造函数时传仿函数对象
std::shared_ptr<int> n1(new int[5], DeleteArray<int>());
std::shared_ptr<int> n2((int*)malloc(5 * sizeof(int)), Free<int>());
//调用构造函数时传lambda表达式
std::shared_ptr<int> n3(new int[5], [](int* ptr) {delete[] ptr; });
//unique_ptr在模板参数中传仿函数
std::unique_ptr<int, DeleteArray<int>> n4(new int[5]);
return 0;
}
七、内存泄漏
1.什么是内存泄漏
内存泄漏是指因为疏忽或错误导致程序未能释放已经不再使用的内存的情况;内存泄漏不是内存在物理上消失,而是因为设计错误而失去了对内存的控制(指针丢失);
如果内存还在,进程正常结束,内存也会释放;
2.内存泄露的危害
长期内存泄漏,将导致程序相应越来越慢,直到卡死;
3.如何避免内存泄漏
(1)养成良好的工程编码规范,申请的内存记着去释放;
(2)使用RAII的思想或者智能指针来管理资源;
(3)使用内存泄漏检测工具;