上海 企业网站建设,网站怎么开通微信支付,免费电子版个人简历模板,龙象建设集团有限公司网站#x1f308; 个人主页#xff1a;谁在夜里看海. #x1f525; 个人专栏#xff1a;《C系列》《Linux系列》 ⛰️ 丢掉幻想#xff0c;准备斗争 目录
引言
内存泄漏
内存泄漏的危害
内存泄漏的处理
一、RAII思想
二、智能指针
1.auto_ptr
实现原理
模拟实现
弊端… 个人主页谁在夜里看海. 个人专栏《C系列》《Linux系列》 ⛰️ 丢掉幻想准备斗争 目录
引言
内存泄漏
内存泄漏的危害
内存泄漏的处理
一、RAII思想
二、智能指针
1.auto_ptr
实现原理
模拟实现
弊端
2.unique_ptr
实现原理
模拟实现
3.shared_ptr
实现原理
模拟实现
循环引用问题
4.weak_ptr 引言
上一篇关于异常处理的文章我们提到异常处理是存在内存泄漏风险的由于异常捕获会导致程序运行时执行流的跳转并且在某些资源释放之前就进行了跳转此时就会引发内存泄漏来看下面这段代码 当我输入3 0时程序会抛出除零错误并跳过了 delete p1; delete p2; 语句因为异常发生时程序的执行流会跳转到 catch 块导致析构函数没有执行引发内存泄漏。
内存泄漏
什么是内存泄漏呢内存泄漏是指程序在运行的过程中动态分配的内存被占用但没有得到释放从而导致资源不能被回收最终可能导致系统性能下降甚至崩溃。
内存泄漏的危害
1.如果内存泄漏非常严重程序将消耗所有的可用内存导致操作系统或程序本身的崩溃。
2.内存泄漏意味着分配的内存空间无法被回收不仅浪费内存空间还可能会影响其他进程。
3.在一些长期运行的系统服务器、嵌入式设备等中内存泄漏会导致系统持续消耗内存而不释放久而久之会导致系统性能下降并最终导致系统崩溃。
4.内存泄漏往往是隐蔽性的在大规模且复杂的程序中调试和定位内存泄漏非常困难这时可能需要借助一些外部的工具。
内存泄漏的处理
一般分为两种①事先预防型 ②事后查错型
事后查错型例如借助外部工具
我们下面要介绍的就是对内存泄漏的事先预防处理办法采用RAII思想以及智能指针
一、RAII思想
RAII 全称 Resource Acquisition Is Initialization中文翻译资源获取即初始化它强调通过对象的生命周期来管理资源将资源的获取与释放与对象的创建与销毁相一致RAII设计原则可以更好地管理动态资源有效避免内存泄漏。还是用上述例子来直观感受一下
templateclass T
class smartptr {
public:smartptr(T data){cout smartptr(T data) endl;_ptr new T(data);}~smartptr(){cout ~smartptr() endl;delete _ptr;}private:T* _ptr;
};int div()
{int a, b;cin a b;if (b 0)throw invalid_argument(除0错误);return a / b;
}
int main()
{try{smartptrint p1(1);smartptrint p2(2);div();}catch (exception e){cout e.what() endl;}return 0;
}
我们可以看到RAII思想实际上就是将资源的获取与释放封装到一个类中在构造函数中获取资源在析构函数中释放资源。我们不需要显式地释放资源并且将资源与对象的生命周期绑定 输入3 0时抛出除零异常执行流跳转的同时try块内部生命周期结束此时会调用内部对象的析构函数完成了资源的释放避免了资源泄露。
二、智能指针
上述smartptr还不能被称作智能指针因为它不具备指针的行为我们还需要在类内部重载解引用*、访问-等操作使其能像指针一样使用
templateclass T
class smartptr {
public:smartptr(T data T()){_ptr new T(data);}~smartptr(){delete _ptr;}T operator*(){return *_ptr;}T* operator-(){return _ptr;}
private:T* _ptr;
};struct Date {int _year;int _month;int _day;
};int main()
{smartptrDate p1;p1-_year 2024;p1-_month 11;p1-_day 9;cout (*p1)._year - (*p1)._month - (*p1)._day endl;return 0;
}
智能指针采用RAII原理来管理动态分配的内存也是将资源的管理与对象的生命周期绑定并且可以像普通指针那样进行*、-等操作。
下面我们来介绍一下C98提供的auto_ptr智能指针
1.auto_ptr
auto_ptr是C98标准引入的一种智能指针是RAII思想的体现其核心功能是自动管理动态分配的内存确保指针在超出作用域时资源被正确释放下面是它的主要实现原理
实现原理
构造函数
auto_ptr的构造函数接受一个原始指针裸指针并将其封装成auto_ptr对象内部的指针
auto_ptrint p(new int(10)); // 构造函数析构函数
auto_ptr的析构函数会在对象生命周期结束时自动调用delete操作符释放指针所指向的内存
~auto_ptr() {delete _ptr; // 释放内存
}拷贝构造函数
与普通对象的拷贝构造函数不同auto_ptr在拷贝时会转移其资源所有权给新对象。因此拷贝构造后原对象会变成一个空指针指向nullptr
auto_ptr(const auto_ptr other) : _ptr(other._ptr) {other._ptr nullptr; // 将原对象的指针设为 nullptr避免重复释放
}赋值操作符
auto_ptr的赋值操作符也会转移资源所有权先释放当前对象的资源然后将传入的指针复制到当前对象并将传入指针置空
auto_ptr operator(const auto_ptr other) {if (this ! other) {delete _ptr; // 先释放当前资源_ptr other._ptr;other._ptr nullptr; // 将 other 的指针置空}return *this;
}成员访问
通过重载*、-实现指针的解引用与访问成员操作
auto_ptrint p(new int(10));
*p 20; // 访问值模拟实现
下面是对auto_ptr的简单模拟实现 templateclass Tclass auto_ptr{public:// 构造函数auto_ptr(T* ptr):_ptr(ptr){}// 拷贝构造函数auto_ptr(auto_ptrT other):_ptr(other._ptr){// 管理权转移other._ptr nullptr;}// 赋值函数auto_ptr operator(const auto_ptrT other){// 检测是否给自己赋值if (*this ! other){// 释放当前资源if (_ptr)delete _ptr;_ptr other._ptr;other._ptr nullptr;}return *this;}// 析构函数~auto_ptr(){if(_ptr)delete _ptr;}T operator*(){return *_ptr;}T* operator-(){return _ptr;}private:T* _ptr;};弊端
auto_ptr在拷贝和赋值时隐式地转移了资源所有权会导致一些潜在的资源管理问题。
auto_ptr被拷贝时源对象指针被置空此时无法再进行访问
auto_ptrint p1(new int(10)); // 创建 p1
auto_ptrint p2 p1; // p1 的资源转移给 p2p1 变为 nullptr
cout *p1 std::endl; // 错误p1 已为空指针这种隐性的资源转移使得auto_ptr在传递和返回时不直观比如我们在函数调用对象时希望资源可以被复制并共享但是auto_ptr不允许资源的共享可能会导致意料之外的资源转移
void foo(auto_ptrint ptr) {// ptr 的资源所有权已经转移到 foo 函数的局部变量中
}int main() {auto_ptrint p1(new int(10));foo(p1); // p1 的资源被转移到 foop1 变为空指针cout *p1 std::endl; // 错误p1 已为空指针
}auto_ptr在实际使用过程中会引发许多不易察觉的错误这并不是我们想要的智能指针为了避免上述问题C11引入了更多新的智能指针例如unique_ptr
2.unique_ptr
为了避免所有权转移导致的一系列潜在问题unique_ptr采用了一种简单粗暴的办法禁止拷贝禁止一切拷贝行为从根源上解决了问题。
实现原理
禁止拷贝行为 编译器会禁止unique_ptr的拷贝构造与拷贝复制操作试图拷贝将会报错
std::unique_ptrint p1(new int(10));
// 编译错误禁止拷贝构造
std::unique_ptrint p2 p1; // 错误拷贝构造被删除明确的资源所有权转移
禁止拷贝并不意味着对资源所有权转移的全面封杀实际上还是可以对资源进行转移的只不过auto_ptr是隐式地转移而unique_ptr是显式地进行转移
unique_ptrint p1(new int(10));
unique_ptrint p2 move(p1); // 明确转移资源所有权// p1 现在为空指针p2 拥有资源
cout *p2 endl; // 输出 10
// cout *p1 endl; // 错误p1 现在为空指针模拟实现
那么unique_ptr在底层是怎么实现对拷贝的禁止的呢其实是用到了delete关键字在成员函数后面加上 delete表示禁止该成员函数的使用通过delete删除拷贝构造函数与拷贝赋值函数从而禁止了拷贝行为的发生 templateclass Tclass unique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){if (_ptr)delete _ptr;}// 指针操作T operator*(){return *_ptr;}T* operator-(){return _ptr;}// 不支持拷贝构造、拷贝赋值unique_ptr(const unique_ptrT other) delete;unique_ptr operator(const unique_ptrT other) delete;private:T* _ptr;};
C11还提供了一种智能指针它支持拷贝行为并且允许多个对象共享资源即拷贝赋值并不会将原指针置空而是让它们指向同一块空间
3.shared_ptr
shared_ptr支持拷贝构造以及拷贝赋值它们可以共享资源各自进行操作但要考虑一个问题RAII的思想是将资源的获取和释放与对象的生命周期绑定当我通过函数传参的方式将一个对象赋值给了另一个对象会导致资源的提前释放函数结束这样外部指针就悬空了共享的资源只需要进行一次释放即可那么我们怎么知道何时释放资源呢通过计数器的方式实现
实现原理
shared_ptr通过引用计数来实现资源的正确释放确保在所有共享该资源的shared_ptr对象都销毁后才释放资。
当另一个shared_ptr对象拷贝构造或者赋值时引用计数增加表示多了一个对象在共享资源当shared_ptr对象析构或被赋值到新的对象时引用计数减少表示减少一个共享资源的持有者。 模拟实现
实现过程如下 templateclass Tclass shared_ptr{public:shared_ptr(T* ptr):_ptr(ptr),_pCount(new int(0)){* _pCount; // 增加计数}shared_ptr(shared_ptrT other):_ptr(other._ptr),_pCount(other._pCount){cout shared_ptr(shared_ptrT other) endl;* _pCount; // 增加计数}shared_ptr operator(shared_ptrT other){// 检测是否给自己赋值if (_ptr ! other._ptr){cout shared_ptr operator(const shared_ptrT other) endl;--* _pCount; // 减少当前资源的计数// 释放当前资源if (*_pCount 0){delete _ptr;delete _pCount;}_ptr other._ptr;_pCount other._pCount;* _pCount; // 增加新资源的计数}return *this;}~shared_ptr(){--* _pCount; // 减少计数if (*_pCount 0){// 共享对象全部销毁进行析构cout ~shared_ptr() endl;delete _ptr;delete _pCount;}}T operator*(){return *_ptr;}T* operator-(){return _ptr;}private:T* _ptr;int* _pCount;};
shared_ptr适用于绝大多数场景但是在某些场景下会引发循环引用问题此时资源不能得到正确释放
循环引用问题
来看下面这段代码
struct ListNode
{int _data;shared_ptrListNode _prev;shared_ptrListNode _next;~ListNode() { cout ~ListNode() endl; }
};
int main()
{shared_ptrListNode node1(new ListNode);shared_ptrListNode node2(new ListNode);cout node1.use_count() endl;cout node2.use_count() endl;node1-_next node2;node2-_prev node1;cout node1.use_count() endl;cout node2.use_count() endl;return 0;
}
我们将节点的_prev和_next定义成shared_ptr智能指针并定义了两个节点节点2的_prev指向节点1节点1的_next指向节点2: 这个时候会造成什么结果呢我们来看运行结果 node1的_next和node2共享资源所以它们的计数为2node2的_prev和node1共享资源所以它们的计数也为2从运行结果可以看出节点空间没有得到释放没有打印~ListNode()内容这是为什么呢
由于Node1和Node2的相互引用它们的任意一个要想释放空间都得建立在对方已经释放空间的基础上于是乎两者都不能正常进行空间释放这就是循环引用问题。
4.weak_ptr
C11提供了一种弱引用智能指针weak_ptr它的出现就是为了解决循环引用问题的其原理是weak_ptr是对对象的弱引用不会增加计数不会阻止资源的释放。 由于互相指向时计数没有增加所以最后析构函数正常调用资源得到释放。 以上就是对RAII思想及智能指针的介绍与个人理解欢迎指正~
码文不易还请多多关注支持这是我持续创作的最大动力