目录
内存泄漏
介绍
分类
堆内存泄漏
系统资源泄漏
检测内存泄漏的方式
智能指针
引入
介绍
原理
引入
RAII原则
指针性质
拷贝
auto_ptr
介绍
代码
boost库
unique_ptr
介绍
代码
介绍
删除器
代码
问题(循环引用)
weak_ptr
介绍
代码
内存泄漏
介绍
内存泄漏是指在计算机程序中分配的动态内存(通常是堆内存)未被释放或回收的情况
这意味着程序在分配内存后,却没有及时释放它,使系统中的可用内存逐渐减少,最终可能导致程序运行变慢,系统崩溃,或者需要重新启动
分类
堆内存泄漏
- 程序执行中必须要通过malloc / calloc / realloc / new等方式,从堆中分配的一块内存
- 用完后必须通过调用相应的 free或者delete 删掉
- 假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak
系统资源泄漏
- 指程序使用系统分配的资源,比方套接字、文件描述符、管道等
- 从我们浅薄的linux知识可以知道,系统的各种结构是需要被管理起来的,这也就需要用一些资源去管理
- 但如果没有使用对应的函数释放掉,就会导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定
检测内存泄漏的方式
在linux下内存泄漏检测:Linux下几款C++程序中的内存泄露检查工具_c++内存泄露工具分析-CSDN博客在windows下使用第三方工具:VS编程内存泄漏:VLD(Visual LeakDetector)内存泄露库-CSDN博客其他工具:https://www.cnblogs.com/liangxiaofeng/p/4318499.html
智能指针
引入
前面已经介绍了内存泄漏,但之前我们遇到的内存泄漏问题大多都是因为自己疏忽了,补上delete就行
但在接触了c++中的异常机制后,内存泄漏的问题就变得不好处理了
因为捕捉到异常后,会改变当前的执行流,可能会跳出好几层,一旦跳出后,之前申请到的资源就不好释放了
即使你可以在捕捉到异常后,先释放资源再抛出,一旦套了好几层写代码可累死
所以,智能指针就被研究了出来
介绍
是C++中用于管理动态内存分配的对象的指针,它们可以帮助开发人员避免内存泄漏和资源管理的复杂性
主要提供了auto_ptr,unique_ptr,shared_ptr和weak_ptr这四种指针
原理
引入
- 还记得我们遇到的问题吗,是遇到抛异常的情况会不好释放资源,而且有时候我们也会忘记释放
- 究其根本我们必须要手动释放
- 如果我们可以让资源自动释放,尤其是出了作用域之后自动释放,也就是借助对象的特性
- 我们的对象都是出作用域后自动析构
- 而这个特性其实就是RAII原则
RAII原则
RAII 是一种编程范式,代表资源获取即初始化
- 它是一种用于资源管理的重要原则,尤其在C++中广泛应用
- RAII 的核心思想是,资源(如内存、文件句柄、数据库连接等)的获取和释放应该与对象的生命周期相关联
- 资源获取即初始化:在对象被创建时,资源也应该被分配
- 当对象超出作用域时,析构函数会自动调用,从而释放资源
- 这样能确保资源不会泄漏,即使在出现异常的情况下也能正确处理资源
指针性质
除此之外,智能指针也需要具有指针的特性
我们虽然让他借助对象的特性,但也不能失去指针的性质
所以,我们需要在类内部重载->和*
拷贝
智能指针之间最显著的区别就是处理拷贝的方法
最先在c++98就有auto_ptr的出现,但是在当时被骂惨了,现在公司也明确不能使用这玩意,就是因为它处理拷贝的方式很怪
auto_ptr
介绍
- 具有独占所有权的特性,也就是在拷贝或赋值时会接管内存的所有权,从而避免了多个智能指针同时管理同一块内存的情况
- 但可能有些人不知道这个特性,使用了被拷贝对象,这样就可能导致不可预测的行为
代码
#include "head.h" //拷贝时,将被拷贝对象置空 namespace my_auto_ptr { template <class T> class auto_ptr { public: auto_ptr(T *p) : _ptr(p) { } auto_ptr(auto_ptr<T> &p) : _ptr(p._ptr) { p._ptr = nullptr; } auto_ptr(auto_ptr<T> &&p) : _ptr(p._ptr) { p._ptr = nullptr; } auto_ptr<T> &operator=(auto_ptr<T> &p) { _ptr = p._ptr; p._ptr = nullptr; return *this; } T &operator*(){ return *_ptr; } T *operator->(){ return _ptr; } ~auto_ptr() { delete _ptr; } private: T *_ptr; }; }
然后,在boost库中,提供了更加实用的的scoped_ptr和shared_ptr和weak_ptr
boost库
Boost C++ 库是一个开源的、高质量的C++库集合,它扩展和增强了C++语言的功能,提供了许多工具和组件,用于各种领域的应用开发
Boost库的目标是成为C++标准库的候选扩展,因此它的设计非常高质量,且符合现代C++编程标准
而它也不负众望的被c++标准库采用了
c++11提供了unique_ptr和shared_ptr和weak_ptr,其中unique_ptr对应boost 的scoped_ptr
并且这些智能指针的实现原理是参考boost中的实现的
unique_ptr
介绍
- 它是一种独占所有权的智能指针,意味着只有一个实例可以拥有和管理特定资源,它负责在对象不再需要时自动释放资源
- 虽然和auto_ptr产生的是一样的结果,但处理方式不同,unique_ptr是直接禁止拷贝,而不是像auto_ptr那样,不禁止却不能多个指向一份资源
代码
#include "head.h" //不允许拷贝 namespace my_unique_ptr { template <class T> class unique_ptr { public: unique_ptr(T *p) : _ptr(p) { } unique_ptr(unique_ptr<T> &p) = delete; //直接定义为删除的函数 unique_ptr(unique_ptr<T> &&p) : _ptr(p._ptr) { p._ptr = nullptr; } unique_ptr<T> &operator=(unique_ptr<T> &p) = delete; //同理 T &operator*() { return *_ptr; } T *operator->() { return _ptr; } ~unique_ptr() { delete _ptr; } private: T *_ptr; }; }
shared_ptr
介绍
- shared_ptr才是我们的重头戏,因为只有他支持了正常的拷贝操作,是我们最为实用的智能指针
- 它允许多个智能指针共享同一个资源,这意味着它可以用于协同管理资源,特别是在涉及共享拥有权的情况下非常有用
- 他内部维护了一个引用计数,可以记录当前资源有多少个指针引用,当引用计数降至零时,资源会被自动释放
- 如何保证引用计数可以让每份资源对应一个计数值呢?
- 如果是int类型成员变量,每个指针就有独立的引用计数了,毫无意义,我们需要让指向一片资源的共享引用计数
- 所以可以考虑动态开辟一个引用计数,在资源被申请时开辟,拷贝时直接拷贝指针即可
删除器
- 外部开辟空间的方式有很多种,我们必须得依据开辟方式来确定析构方式
- 所以我们可以考虑直接向类中传递析构方式(因为在内部无法判断是哪种)
- 传递也有两种方式,给类传还是给构造函数传
- 由于库中是给构造函数传的,所以我们也这样做
- 但是,我们需要一个统一的类型来接收删除器啊
- 哎~之前学过的适配器就可以用上了,因为需要传的都是一个指针,且没有返回值
- 所以适配器的类型就是 -- function<void(T *)>
代码
#include "head.h" #pragma once // 可以拷贝,但在循环引用的情况下,无法使用 // 外部开辟空间有多种方式:new/new[]/malloc,所以需要传入删除器 namespace my_shared_ptr { template <class T> class shared_ptr { public: shared_ptr(T *p = nullptr) : _ptr(p) { _count = new int(1); // 每份资源对应一个计数 } shared_ptr(const shared_ptr<T> &p, function<void(T *)> del) //不传默认是delete : _ptr(p._ptr), _count(p._count),_del(del) { ++(*_count); } shared_ptr(shared_ptr<T> &&p) : _ptr(p._ptr),_del(p._del) { --(p._count); p._ptr = nullptr; _count = new int(1); } shared_ptr<T> &operator=(shared_ptr<T> &p) { if (p._ptr == this->_ptr) // 防止自赋值(空间会提前释放)/引用同一片资源对其赋值(效率低) { return *this; } if (--this->_count == 0) // 如果this指向的空间已经没有人引用了,需要手动释放(因为当前该指针的生命周期还没有结束) { _del(_ptr); delete _count; } _ptr = p._ptr; _count = p._count; _del=p._del; ++(*_count); return *this; } T &operator*() const { return *_ptr; } T *operator->() const { return _ptr; } T *get() const //给weak_ptr使用的 { return _ptr; } ~shared_ptr() { if (--(*_count) == 0) //只有当引用计数为0时才释放空间 { _del(_ptr); } } private: T *_ptr; int *_count; // 引用计数 function<void(T *)> _del = [](T *p) { delete p; }; // 删除器 }; }
问题(循环引用)
看着似乎shared_ptr完美无缺了,但是,当遇到下面这种情况时,会发生内存泄漏
struct ListNode { int _data; shared_ptr<ListNode> _prev; shared_ptr<ListNode> _next; ~ListNode(){ cout << "~ListNode()" << endl; } }; int main() { shared_ptr<ListNode> node1(new ListNode); shared_ptr<ListNode> 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; }
本身,他俩各自引用计数都为1,但是!!!其内部还有俩shared_ptr指针,然后互相一指,让他俩计数都变成2
最后,当俩对象析构后,引用计数仍为1,这样就会导致内存泄漏
为什么呢?
- node1如果要释放,需要_p释放,也就是node2析构,但node2被node1指着,只有当node1释放才行,这就又回到最开始了
- 也就是node1释放需要node2先释放,而node2也一样,它释放需要让node1先释放,两者成为一种纠缠态
- 这也被叫做"循环引用"问题
- 所以,为了解决这个问题,提出了weak_ptr
weak_ptr
介绍
用于解决潜在的循环引用问题,它允许你共享资源的引用,但不会增加资源的引用计数,从而避免了循环引用导致的内存泄漏
代码
#include "shared_ptr.hpp" // 由shared_ptr构造,没有其他功能 namespace my_weak_ptr { template <class T> class weak_ptr { public: weak_ptr() : _ptr(nullptr) {} weak_ptr(const my_shared_ptr::shared_ptr<T> &p) : _ptr(p.get()) {} weak_ptr<T> &operator=(const my_shared_ptr::shared_ptr<T> &p) { _ptr = p.get(); return *this; } T &operator*() { return *_ptr; } T *operator->() { return _ptr; } ~weak_ptr() { _ptr=nullptr; } private: T *_ptr; }; }