目录
若没有了解过异常,请先看异常章节
智能指针解决的问题
场景1:malloc和free之间的代码抛出异常
若给一个对象申请空间后,释放空间之间的代码抛出了异常,那么可能会导致内存泄漏
- 解决办法是利用try catch的就近原则配合异常再抛出共同解决
#include <iostream>
void Throw()
{
throw "Func error";
}
void Func()
{
int* Array = nullptr;
try
{
int* Array = new int[100];
Throw();
}
catch (...)
{
delete[] Array;
std::cout << "delete success!" << std::endl;
throw;
}
delete[] Array;
std::cout << "delete success!" << std::endl;
}
int main()
{
try
{
Func();
}
catch (const char* strerr)
{
std::cout << strerr << std::endl;
}
//运行结果:
//delete success!
//Func error
return 0;
}
- 这种方式虽然能解决问题,但开发效率极低
场景2:new自身抛出异常
给一个对象new空间时,new本身可能会抛出异常,换句话来说每调用一次new就要对new抛出的异常进行捕获
- 这种场景也能解决,但代码量激增,开发效率低
- new抛出的异常可以使用std::exception类来捕获
异常不安全版本:
void Func()
{
char* arr1 = new char[10];
char* arr2 = new char[10];
char* arr3 = new char[10];
delete[] arr1;
delete[] arr2;
delete[] arr3;
}
int main()
{
Func();
return 0;
}
异常安全版本:
#include <iostream>
#include <stdexcept>
void Func()
{
char* arr1 = nullptr;
char* arr2 = nullptr;
char* arr3 = nullptr;
try {
arr1 = new char[10];
}
catch (const std::exception& e) {
std::cerr << "Exception during arr1 allocation: " << e.what() << std::endl;
throw;
}
try
{
arr2 = new char[10];
}
catch (const std::exception& e)
{
std::cerr << "Exception during arr2 allocation: " << e.what() << std::endl;
delete[] arr1;
throw;
}
try
{
arr3 = new char[10];
}
catch (const std::exception& e)
{
std::cerr << "Exception during arr3 allocation: " << e.what() << std::endl;
delete[] arr1;
delete[] arr2;
throw;
}
delete[] arr1;
delete[] arr2;
delete[] arr3;
}
int main() {
try
{
Func();
}
catch (const std::exception& e)
{
std::cout <<"what:" << e.what() << std::endl;
}
return 0;
}
可以对比一下上述两段代码,明显能看出仅仅依靠try catch捕获异常,所需要的代码量是非常大的
智能指针的原理
RAII
RAII(资源获取即初始化)是一种主要在C++编程中使用的编程惯用法。它的核心思想是将资源的生命周期绑定到对象的生命周期,以确保资源在不再需要时能够自动释放。这样可以有效地管理诸如内存、文件句柄和网络连接等资源。
示例:
#include <iostream>
template<class T>
class SmartPtr
{
public:
//RAII
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
std::cout << "delete" << std::endl;
delete[] _ptr;
}
protected:
T* _ptr;
};
void Func()
{
SmartPtr<char> p1(new char[10]);
SmartPtr<char> p2(new char[10]);
SmartPtr<char> p3(new char[0x7ffffff1]);//开辟空间太大,会抛异常
}
int main()
{
try
{
Func();
}
catch (const std::exception& e)
{
std::cout << "What : " << e.what() << std::endl;
}
//运行结果:
//delete
//delete
//What : bad allocation
return 0;
}
- 当资源与对象进行绑定时我们不再需要显式去delete,而是利用对象生命周期到了以后自动调用析构函数的特点
- 当执行到p3,抛出异常后,虽然执行流是直接跳转到main的,但Func函数的栈帧还是正常结束的,栈帧结束,对象生命周期就到了,所以抛出异常以后会自动释放p1,p2。
指针的行为
智能指针的原理包括了RAII设计模式,但不仅仅是RAII,智能指针还需要让对象可以像指针一样使用,主要包括两个方面:
- 智能指针支持*解引用数据
- 智能指针支持->访问类成员
本质上,智能指针就是一个类,但它封装了"*"和"->"的重载
#include <iostream>
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr,bool IsArray = false)
:_ptr(ptr)
,_IsArray(IsArray)
{}
~SmartPtr()
{
std::cout << "delete" << std::endl;
if (_IsArray)
{
delete[] _ptr;
}
else
{
delete _ptr;
}
}
//指针的行为
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
protected:
T* _ptr;
bool _IsArray;
};
void Func()
{
//像指针一样解引用数据
SmartPtr<int> p1(new int(0));
*p1 += 10;
//像指针一样访问结构体成员
SmartPtr<std::pair<std::string, int>> p2(new std::pair<std::string, int>("",0));
p2->first = "sort";
p2->second = 2;
}
int main()
{
try
{
Func();
}
catch (const std::exception& e)
{
std::cout << "What : " << e.what() << std::endl;
}
//运行结果:
//delete
//delete
return 0;
}
智能指针的拷贝
智能指针拷贝 vs 数据结构拷贝
- 智能指针本质是模仿指针的行为,指针赋值给另外一个指针,这两个指针指向的空间是同一个,所以智能指针要求的拷贝是浅拷贝
- 数据结构利用资源存储管理数据,资源是自己的,每一个数据结构对象的资源都是相对独立的,所以数据结构要求的拷贝是深拷贝
智能指针浅拷贝的问题
智能指针的生命周期到了会自动释放资源,那么当发生浅拷贝时会导致一份资源被释放两次,此时程序会崩溃
实际上,为了解决智能指针浅拷贝的问题,就不得不提到智能指针的发展史
C++98:auto_ptr
auto_ptr是最早的标准智能指针之一,它在 C++98 标准中引入。
它在解决智能指针浅拷贝问题时是直接把资源的管理权进行转移
- 举个例子,假设智能指针p1被拷贝给智能指针p2,那么auto_ptr会把p2指向p1的资源,p1指向空,此时p1的资源由p2管理
- 使用auto_ptr要包含头文件memory
- auto_ptr中的成员函数get可以获取对象中的成员
#include <iostream>
#include <memory>
int main()
{
std::auto_ptr<int> p1(new int(100));
std::auto_ptr<int> p2(p1);
std::cout << "p1 : " << p1.get() << std::endl;
std::cout << "p2 : " << p2.get() << std::endl;
//运行结果
//p1 : 00000000
//p2: 00B7F868
return 0;
}
- 一般会发生资源转移的部分是右值引用的移动语义,本质原因是因为右值的生命周期很短的原因导致右值的资源转移是合理的,但这里的智能指针是一个左值,所以这里的资源转移是非常不合理的,如果后续代码还要解引用p1,程序会崩溃
- 对于auto_ptr个人建议是尽量不要使用
boost库智能指针
由于c++98的auto_ptr的缺陷实在是太大,C++标准委员会痛定思痛,总结原因时认为出现auto_ptr的原因是因为实践的不够,所以在C++11之前,C++98之后,提出了一个boost库,这个库的目的主要是为C++标准进行探路,之后的C++标准中的东西首先是在boost库中,如果经过实践觉得还不错那么会发布到C++标准中,如果实践觉得不行就不发布。
实际上C++11中的东西都是首先被纳入boost库中的,如右值引用,线程库...,其中就包括智能指针,并且boost库中的智能指针非常多
而之后C++11标准中的智能指针都是参考了boost库中的某些智能指针
- unique_ptr参考了boost库中的scoped_ptr
- shared_ptr参考了boost库中的shared_ptr
C++11:unique_ptr
unique_ptr在头文件memory中
unique_ptr解决浅拷贝的问题很简单,就是禁止你进行拷贝和赋值!
C++11:shared_ptr
shared_ptr在头文件memory中
shared_ptr采用的是引用计数的方式进行浅拷贝,也就是记录一下当前资源有多少个对象进行管理
- 若有多个对象在管理资源,一个对象析构时资源不被释放,引用计数--
- 若只有一个对象在管理资源,析构时资源被释放
使用shared_ptr的接口use_count可以查看有多少个对象在管理同一个资源
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> p1(new int(10));
std::cout << p1.use_count() << std::endl;
std::shared_ptr<int> p2(p1);
std::cout << p1.use_count() << std::endl;
//运行结果:
//1
//2
return 0;
}
shared_ptr的引用计数的需求是每一个资源配备一个引用计数,因此可以记录一个引用计数的指针
- 只有当构造时,才是一个新的资源到来了,为这个资源开辟一个引用计数
- 当拷贝时,不仅仅让资源指向相同,还要让引用计数的指针指向同一个
shared_ptr模拟实现
#include <iostream>
namespace yyf
{
template<class T>
class shared_ptr
{
public:
//构造时表示新的资源来了,为这个资源开辟一个引用计数
shared_ptr(T* ptr)
:_ptr(ptr)
, _ptr_count(new int(1))
{}
//拷贝构造时表示没有新的资源,只是浅拷贝,指向同一个资源和引用计数
shared_ptr(const shared_ptr& s)
{
_ptr = s._ptr;
_ptr_count = s._ptr_count;
(*_ptr_count)++;
}
//析构时,根据引用计数判断是否析构
~shared_ptr()
{
std::cout << _ptr << "count--" << std::endl;
if (--(*_ptr_count) == 0)
{
ClearCount();
}
}
void ClearCount()
{
std::cout << _ptr << ":delete" << std::endl;
delete _ptr;
delete _ptr_count;
}
shared_ptr<T>& operator = (const shared_ptr<T>&s)
{
if (s._ptr !=_ptr)
{
std::cout << _ptr << "count--" << std::endl;
if (-- * (_ptr_count) == 0)
{
ClearCount();
}
_ptr = s._ptr;
_ptr_count = s._ptr_count;
++(*_ptr_count);
}
return *this;
}
int use_count()
{
return *_ptr_count;
}
protected:
T* _ptr;
int* _ptr_count;
};
};
int main()
{
yyf::shared_ptr<int> p1(new int(100));
std::cout << "p1 count:" << p1.use_count() << std::endl;
yyf::shared_ptr<int> p2(p1);
std::cout << "p1 and p2 of count:" << p1.use_count() << std::endl;
yyf::shared_ptr<int> p3(new int(1));
std::cout << "p3 count:" << p3.use_count() << std::endl;
p3 = p1;
std::cout << "p1 and p2 and p3 count:" << p3.use_count() << std::endl;
return 0;
}
循环引用
shared_ptr的缺陷:循环引用
场景:链表结点的循环引用
#include <iostream>
#include <memory>
struct ListNode
{
int _val;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
ListNode(int val = 0) :_val(val), _next(nullptr), _prev(nullptr)
{}
~ListNode()
{
std::cout << "delete" << std::endl;
}
};
int main()
{
std::shared_ptr<ListNode> p1(new ListNode);
std::shared_ptr<ListNode> p2(new ListNode);
p1->_next = p2;
p2->_prev = p1;
//运行结果:无输出
return 0;
}
- 无输出表示的是两个ListNode都没有被析构,造成了内存泄漏
问题分析
循环引用内存泄漏的原因:文字描述
- 创建完p2结点后,p1和p2是两个智能指针,它们之中都有引用计数,且引用计数都为1
- 执行完p1->_next = p2后,此时p2中的ListNode有两个资源管理对象,一个是p2,一个是p1的next,这个资源的引用计数为2
- 执行完p2->_prev = p1后,此时p1的ListNode资源也有两个资源管理对象,一个是p1,一个是p2中的_prev指针,该资源引用计数为2。
- 出了作用域以后,p2先释放,p2中的引用计数--变为1。p1后释放,p1中的引用计数--变为1
- 由于p1和p2的引用计数都没到0,两个结点的资源都不释放,造成内存泄漏
假设p1中的结点为结点1,p2中的结点为结点2:
- 结点1的释放前提是结点2中的prev释放
- 结点2中的prev的释放前提是结点2的释放
- 结点2的释放前提是结点1的next的释放
- 结点1中的next的释放前提是结点1的释放
- 综上所述,这是一个循环,所以这整个过程我们称为循环引用
循环引用内存泄漏的原因:图示
循环引用出现的场景
只要出现两个智能指针互相管理对方的资源,那么就会出现循环引用的问题,这是shared_ptr在特定场景下的缺陷
解决办法:weak_ptr
weak_ptr是专门用于解决循环引用设计的,它的关键点及其解决思路:
- weak_ptr不是采用的RAII设计模式
- weak_ptr只接收资源,不加引用计数
- weak_ptr没有析构函数,它不参与资源的管理
- weak_ptr提供了一个shared_ptr的构造,也可以把它理解为一个没有引用计数的shared_ptr
- weak_ptr的默认构造是把内部的指针置为空
有了weak_ptr后,只需要把链表结点的两个指针next和prev设计为weak_ptr的即可,它们照样能接收shared_ptr的值
#include <iostream>
#include <memory>
struct ListNode
{
int _val;
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
ListNode(int val = 0) :_val(val)
{}
~ListNode()
{
std::cout << "delete" << std::endl;
}
};
int main()
{
std::shared_ptr<ListNode> p1(new ListNode);
std::shared_ptr<ListNode> p2(new ListNode);
p1->_next = p2;
p2->_prev = p1;
//运行结果:
//delete
//delete
return 0;
}
weak_ptr的模拟实现
#include <iostream>
#include <memory>
namespace yyf
{
template <class T>
class weak_ptr
{
public:
weak_ptr() : _ptr(nullptr)
{
}
weak_ptr(const std::shared_ptr<T> &s)
{
_ptr = s.get();
}
weak_ptr<T> &operator=(const std::shared_ptr<T> &s)
{
_ptr = s.get();
return *this;
}
T &operator*()
{
return *_ptr;
}
T *operator->()
{
return _ptr;
}
protected:
T *_ptr;
};
}
定制删除器
定制删除器解决的问题
- 默认情况下,智能指针在析构时会调用析构函数,有了定制删除器后,智能指针析构时调用的是定制删除器
- 有些时候我们不仅仅会new一个对象,我们也会new一个数组
- fopen打开文件时,对于文件来说我不需要释放,我只需要关闭
对于上述的场景,就需要一个定制删除器
std中的智能指针(unique_ptr和shared_ptr)中包含了特殊的构造函数,这个构造函数可以传入的可调用对象,可调用对象用于析构时使用
定制删除器的使用
#include <iostream>
#include <memory>
template<class T>
struct DelArray
{
void operator()(T* p)
{
delete[] p;
}
};
struct ListNode
{
int _val;
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
ListNode(int val = 0) :_val(val)
{}
~ListNode()
{
std::cout << "delete" << std::endl;
}
};
int main()
{
std::shared_ptr<ListNode> ptr(new ListNode[5], DelArray<ListNode>());//传入可调用对象
return 0;
}
- 传入的可调用对象可以是函数指针,可以是lambda,上面只是以仿函数为例
shared_ptr + 定制删除器模拟实现
template<class T>
class shared_ptr
{
public:
//定制删除器版的构造
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
, _ptr_count(new int(1))
, _del(del)
{}
shared_ptr(T* ptr)
:_ptr(ptr)
, _ptr_count(new int(1))
{}
shared_ptr(const shared_ptr& s)
{
_ptr = s._ptr;
_ptr_count = s._ptr_count;
_del = s._del;
(*_ptr_count)++;
}
~shared_ptr()
{
std::cout << _ptr << "count--" << std::endl;
if (--(*_ptr_count) == 0)
{
ClearCount();
}
}
void ClearCount()
{
_del(_ptr);
delete _ptr_count;
}
shared_ptr<T>& operator = (const shared_ptr<T>& s)
{
if (s._ptr != _ptr)
{
if (-- * (_ptr_count) == 0)
{
ClearCount();
}
_ptr = s._ptr;
_ptr_count = s._ptr_count;
_del = s._del;
++(*_ptr_count);
}
return *this;
}
int use_count()
{
return *_ptr_count;
}
protected:
T* _ptr;
int* _ptr_count;
std::function<void(T*)> _del = [](T* ptr) {
delete ptr;
};
};