C++ 18智能指针:告别内存泄漏的利器

智能指针使用场景分析

如果没有智能指针,按照传统的方式,开辟空间后,会在最后释放空间, 那么下面的这串代码,为了防止抛异常后,资源未被释放,就需要进行catch...捕获但如果有arrayn,n为无穷大,那么就要进行n-1次捕获,不仅代码繁琐,且如果少了一次,还会造成内存泄漏

cpp 复制代码
double Divide(int a, int b)
{
    if (b == 0)
    {
        throw "Divide by zero condition!";
    }
    return (double)a / (double)b;
}
 
void Func()
{
    // 异常可能导致array1和array2内存泄漏
    int* array1 = new int[10];
    int* array2 = new int[10];  // 如果这里抛出异常,array1会泄漏
    
    try
    {
        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;
    }
    catch (...)
    {
        // 需要手动释放所有资源
        cout << "delete []" << array1 << endl;
        delete[] array1;
        cout << "delete []" << array2 << endl;
        delete[] array2;
        throw;
    }
    
    // 正常路径也需要释放
    cout << "delete []" << array1 << endl;
    delete[] array1;
    cout << "delete []" << array2 << endl;
    delete[] array2;
}

解决方法

RAII的设计思想

RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是
⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏 RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问, 资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题
俗话:即将资源交给一个类来管理,在这个类生命结束时,会自动调用析构函数

cpp 复制代码
template<class T>
class SmartPtr
{
public:
    // RAII:构造函数获取资源
    SmartPtr(T* ptr) : _ptr(ptr) {}
    
    // RAII:析构函数释放资源
    ~SmartPtr()
    {
        cout << "delete[] " << _ptr << endl;
        delete[] _ptr;
    }
    
    // 重载运算符,模拟指针行为
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }
    T& operator[](size_t i) { return _ptr[i]; }
    
private:
    T* _ptr;
};
 
// 使用RAII智能指针
void Func()
{
    SmartPtr<int> sp1 = new int[10];
    SmartPtr<int> sp2 = new int[10];
    
    for (size_t i = 0; i < 10; i++)
    {
        sp1[i] = sp2[i] = i;
    }
    
    int len, time;
    cin >> len >> time;
    cout << Divide(len, time) << endl;
    // 无论是否发生异常,资源都会自动释放
}

C++标准库

C++标准库中的智能指针都在<memory>, 除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,原理上⽽⾔主要是解 决智能指针拷⻉时的思路不同。

|------------|-------------------|---------------|---|
| auto_ptr | 拷贝时转移所有权 | 不推荐使用 | |
| unique_ptr | 独自占有所有权,不可拷贝,仅可移动 | 不需要拷贝的场景 | |
| shared_ptr | 共享所有权,支持拷贝,引用计数 | 需要拷贝,共享所有权的场景 | |
| weak_ptr | | | |

似乎shared_ptr比unique_ptr,但shared_ptr有额外空间,效率更低,且内存碎片泄漏的风险

解决内存碎片化的解决方法
shared_ptr 除了⽀持⽤指向资源的指针构造,还⽀持 make_shared ⽤初始化资源对象的值
直接构造。
虽然开辟的字节个数一样,但make_shared将原本开辟的两个空间,合并在一块,减少内存块

cpp 复制代码
int main()
{
    std::shared_ptr<Date> sp1(new Date(2024, 9, 11));
    shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
    shared_ptr<Date> sp4;
    return 0;
}

operator bool

cpp 复制代码
int main()
{
    std::shared_ptr<Date> sp1(new Date(2024, 9, 11));
    shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
    shared_ptr<Date> sp4;

    // if (sp1.operator bool())
    if (sp1)   //true
        cout << "sp1 is not nullptr" << endl;

   //if (!sp4)
    if (!sp4.operator bool())   //false
        cout << "sp4 is nullptr" << endl;

    //shared_ptr<Date> sp5 = new Date(2024, 9, 11);
    //unique_ptr<Date> sp6 = new Date(2024, 9, 11);

   return 0;
}

智能指针的原理及实现

auto_ptr的思路是拷⻉时转移资源管理权给被拷⻉对象,这种思路是不被认可
的,也不建议使⽤。unique_ptr的思路是不⽀持拷⻉。

cpp 复制代码
template<class T>
class unique_ptr {
	unique_ptr(T*ptr)
		:_ptr(ptr)
	{}
		~unique_ptr()
	{
		delete _ptr;
	}
     // 像指针一样使用
     T& operator*() { return *_ptr; }
     T* operator->() { return _ptr; }

	//拷贝与赋值不可使用
	unique_ptr(const unique_ptr<T>&sp) = delete;
	unique_ptr<T> operator=(const unique_ptr<T>&sp) = delete;//类型相同
	unique_ptr(const unique_ptr<T>&& sp)
		:_ptr(sp._ptr)
	{
		sp._ptr = nullptr;
	}
	unique_ptr<T>& operator=(unique_ptr<T>&& sp)
		 {
			 delete _ptr;//不释放,则旧空间就再也没有机会了,内存泄漏
			_ptr = sp._ptr;
			 sp._ptr = nullptr;
		 }
private:
	T* _ptr;
};

重点 shard_ptr的设计(引用计数设计)
这里是一份资源包含多个对象,每个对象都共用一个引用计数,一份资源需要一份引用计数,所以静态成员无法实现,只能在堆上进行开辟空间,new一个引用计数出来

cpp 复制代码
template<class T>
class shared_ptr {
public:
	shared_ptr(T*_ptr)
		:_ptr(ptr)
		,_pcount(new int(1))
		//没有删除前,_del依旧走初始化列表,私有已经定好
	{ }
	template<class D>
	shared_ptr(T* ptr, D* del)
		: _ptr(ptr)
		, _pcount(new int(1))
		,_del(del)
	{ }
	shared_ptr(shared_ptr<T>&sp)
		:_ptr(sp._ptr)
		,_pcount(sp._pcount)
		, _del(del)

	{
		++_pcount;
	}
	void release()
	{
		if (--(*_pcount) == 0)
		{
			_del(_ptr);//在外面释放_ptr
			delete _pcound;
			__pcount = _ptr = nullptr;
		}
	}
	~shared_ptr()
	{
		release();
	}
	shared_ptr<T>& operator =(shared_ptr<T>& sp)
	{
		if (._ptr != sp._ptr)//防止为同一份资源进行赋值
		{
			if (--(*_pcount) == 0)
			{
				release();
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++_pcount;
			_del = sp._del;
		}
		return*this;
	}
	T* get() const
		 {
			 return _ptr;
		 }
	int use_count() const
	{
		return *_pcount;
	}
	// 像指针一样使用
	T& operator*() { return *_ptr; }
	T* operator->() { return _ptr; }

	shared_ptr<>
private:
	T* _ptr;
	int* _pcount;
	functional<void(T*)> _del = [](T* ptr) {delete ptr; };//lambda无返回值,传的参数为T*
};

删除器

智能指针析构时默认是进⾏delete释放资源,这也就意味着如果不是new出来的资源,交给智能指
针管理,析构时就会崩溃

因为new[]经常使⽤,所以为了简洁⼀点,
unique_ptr和shared_ptr都特化了⼀份[]的版本,使⽤时 unique_ptr<Date[]> up1(new Date[5]);shared_ptr<Date[]> sp1(new Date[5]); 就可以管理new []的资源。

cpp 复制代码
template<class T>
void DeleteArrayFunc(T* ptr)
{
	delete[] ptr;
}
int main()
{
    std::shared_ptr<Date> sp1(new Date);
    std::shared_ptr<Date[]> sp2(new Date[10]);

    // 定制删除器 都可以,相对建议lambda
    std::shared_ptr<Date> sp3(new Date[10], [](Date* ptr) {delete[] ptr; });
    std::shared_ptr<Date> sp4(new Date[5], DeleteArrayFunc<Date>);

    std::shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
    shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
        //cout << "fclose:" << ptr << endl;
        fclose(ptr);
        });

    std::unique_ptr<Date> up1(new Date);
    std::unique_ptr<Date[]> up2(new Date[10]);
    // 定制删除器 建议仿函数
    std::unique_ptr<FILE, Fclose> up3(fopen("Test.cpp", "r"));

    auto fcloseFunc = [](FILE* ptr) {fclose(ptr); };
    std::unique_ptr<FILE, decltype(fcloseFunc)> up4(fopen("Test.cpp", "r"), fcloseFunc);

    return 0;
}

上面的代码仔细观察,外我们似乎可以发现 unique_ptr的删除器使用更为麻烦,是因为使用该类型的删除器要在模板参数进行声明

定制该类型的删除器,使用仿函数时,不用传两个参数,模板会自动推导

但这里使用lambda需要传两个模板参数,是因为lambda的类型无法自动推导,且模板参数还要使用decltype这个专属语法词

cpp 复制代码
// 定制删除器 建议仿函数
    std::unique_ptr<FILE, Fclose> up3(fopen("Test.cpp", "r"));
    auto fcloseFunc = [](FILE* ptr) {fclose(ptr); };
    std::unique_ptr<FILE, decltype(fcloseFunc)> up4(fopen("Test.cpp", "r"), fcloseFunc);

shared_ptr循环引⽤问题

shared_ptr在大部分情况下适用,但若遇到循环引用就会导致资源未被释放,内存泄漏,我们结合代码看下面两张图

cpp 复制代码
struct ListNode
{
	int _data;

	/*ListNode* _next;
	ListNode* _prev;*/
	/*std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;*/

    //上面对ListNode的类型为shared_ptr,而不是原生指针ListNode*,是为了保持 _next _prev  n1  n2类型一致,下面才可以进行赋值
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

int main()
{
    // 循环引用 -- 内存泄露
    std::shared_ptr<ListNode> n1(new ListNode);
    std::shared_ptr<ListNode> n2(new ListNode);

    cout << n1.use_count() << endl;
    cout << n2.use_count() << endl;

    n1->_next = n2;
    n2->_prev = n1;

    cout << n1.use_count() << endl;
    cout << n2.use_count() << endl;

    return 0;
}

上面的代码与图,n1和n2析构后,管理两个节点的引⽤计数减到1

  1. 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。
  2. _next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。 3. 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释
    放了。
  3. _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。
    ⾄此逻辑上成功形成回旋镖似的循环引⽤,谁都不会释放就形成了循环引⽤,导致内存泄漏

解决方法:weak_ptr

weak_ptr不支持RAII,也不支持访问资源,但可以使用shared_ptr进行构造,绑定到shared_ptr时,不增加shared_ptr的引⽤计数,那么就可以解决上述的循环引⽤问题。

cpp 复制代码
/ 这里改成weak_ptr,当n1->_next = n2;绑定shared_ptr时
    // 不增加n2的引用计数,不参与资源释放的管理,就不会形成循环引用了
     std::weak_ptr<ListNode> _next;
     std::weak_ptr<ListNode> _prev;

weak_ptr

因为weak_ptr不参与资源管理,所以没有重载*和->,但如果绑定的shard_ptr已经过期了,weak_ptr再去访问就危险了
weak_ptr⽀持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引⽤计数,weak_ptr想访问资源时,可以调⽤lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。(调用lock,相当于自己又生成了一个shared_ptr)
expired 用于检测所管理的对象是否已经释放, 如果已经释放, 返回 true; 否则返回 false.

cpp 复制代码
int main()
{
    std::shared_ptr<string> sp1(new string("111111"));
    std::shared_ptr<string> sp2(sp1);

    std::weak_ptr<string> wp = sp1;
    cout << wp.expired() << endl;//0
    cout << wp.use_count() << endl;//2

    // sp1和sp2都指向了其他资源,则weak_ptr就过期了
    sp1 = make_shared<string>("222222");
    cout << wp.expired() << endl;//0
    cout << wp.use_count() << endl;//1

    sp2 = make_shared<string>("333333");
    cout << wp.expired() << endl;//1
    cout << wp.use_count() << endl;//0

    wp = sp1;
    //std::shared_ptr<string> sp3 = wp.lock();
    auto sp3 = wp.lock();
    cout << wp.expired() << endl;//0
    cout << wp.use_count() << endl;//2
    sp1 = make_shared<string>("4444444");

    cout << wp.expired() << endl;//0
    cout << wp.use_count() << endl;//1

    return 0;
}

内存泄漏

何为内存泄漏

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存,⼀般是忘记释
放或者发⽣异常释放程序未能执⾏导致的
危害
⻓期运⾏的程序出现内存泄漏,影响很⼤,如操作系统、后台服
务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越
慢,最终卡死。

如何检查内存泄漏

Linux下检查:Linux下几款C++程序中的内存泄露检查工具_c++内存泄露工具分析-CSDN博客

windows使用第三方检查:windows下的内存泄露检测工具VLD使用_windows内存泄漏检测工具-CSDN博客

如何避免

养成良好的编码规范,申请的内存空间记着匹配的去释放。

尽量使用智能指针,如果不可,也要使用RAII思想

定期使用内存泄漏工具检测

总结

智能指针

RAII思想是C++智能指针的核心思想

unique_ptr适用于独占所有权,不进行拷贝的场景

shared_ptr使用于共享所有权的场景

weak_ptr用于解决循环引用问题(不引用计数)

内存泄漏

内存泄漏很有可能造成事故,要避免

规范代码的编写

使用智能指针

使用RAII思想

使用第三方工具检测

相关推荐
刘某的Cloud2 小时前
列表、元组、字典、集合-组合数据类型
linux·开发语言·python
梁同学与Android2 小时前
Android ---【经验篇】ArrayList vs CopyOnWriteArrayList 核心区别,怎么选择?
android·java·开发语言
XFF不秃头3 小时前
力扣刷题笔记-全排列
c++·笔记·算法·leetcode
ss2733 小时前
从零实现线程池:自定义线程池的工作线程设计与实现
java·开发语言·jvm
石工记3 小时前
windows 10直接安装多个JDK
java·开发语言
郝学胜-神的一滴3 小时前
Python魔法函数一览:解锁面向对象编程的奥秘
开发语言·python·程序人生
San30.3 小时前
深入理解 JavaScript:手写 `instanceof` 及其背后的原型链原理
开发语言·javascript·ecmascript
北冥有一鲲3 小时前
LangChain.js:RAG 深度解析与全栈实践
开发语言·javascript·langchain