当前位置: 首页 > news >正文

智能指针

目录

1. 智能指针的使用及其原理

2. 内存泄露

2.1 什么是内存泄露,内存泄露的危害

2.2 内存泄露分类

2.3 如何检测内存泄露

2.4 如何避免内存泄露

3. 智能指针的使用

3.1 RAII

3.2 Boost库

3.3 std::auto_ptr

3.4 std::unique_ptr

3.5 std::shared_ptr

4. 智能指针的原理

4.1 std::auto_ptr

4.2 std::unique_ptr

4.3 std::shared_ptr

4.3.1 shared_ptr代码

4.3.2 shared_ptr移动构造和移动赋值

4.3.3 shared_ptr线程安全问题

4.3.4 shared_ptr循环引用问题

4.3.4.1 weak_ptr

5. C++11和boost中智能指针的关系

6. 模拟内存泄漏


1. 智能指针的使用及其原理

下面的程序中我们可以看到,new了以后,我们也要delete了,但是因为抛异常,后面的delete没有得到执行,所以就内存泄露了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本身也可以抛异常,连续的两个new和下面的Divide都可能会抛异常,让我们处理起来很麻烦。智能指针放到这样的场景里面就让问题简单多了。

#include <iostream>using namespace std;double Divide(int a, int b)
{//当 b == 0时抛出异常if (b == 0){throw "Divide by zero condition";}else{return (double)a / (double)b;}
}void Func()
{// 这里可以看到如果发生除0错误抛出异常,另外下面的array1和arrayw没有得到释放// 所以这里铺货异常后并不处理异常,异常还是交给外面处理,这里捕获了再重新抛出// 但是如果array2 new的时候抛出异常呢? 就还需要一层捕获释放逻辑// 这里更好的解决方案是智能指针,否则代码太挫了int* array1 = new int[10];int* array2 = new int[10];try{int len, time;cin >> len >> time;cout << Divide(len, time) << endl;}catch (...){cout << "delete []" << array1 << endl;cout << "delete []" << array2 << endl;delete[] array1;delete[] array2;throw; // 异常重新抛出,捕获到什么抛出什么}// ...cout << "delete []" << array1 << endl;delete[] array1;cout << "delete []" << array2 << endl;delete[] array2;
}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}catch (const exception& e){cout << e.what() << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}

2. 内存泄露

void MemoryLeaks()
{// 1.内存申请了忘记释放int* p1 = (int*)malloc(sizeof(int));int* p2 = new int;// 2.异常安全问题int* p3 = new int[10];Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放delete[] p3;
}

2.1 什么是内存泄露,内存泄露的危害

什么是内存泄露:内存泄露是指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄露并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该内存段的控制,因而造成了内存的浪费。

内存泄露的危害:长期运行的程序出现内存泄露,影响很大,如操作系统,后台服务等等,出现内存泄露会导致响应越来越慢,最终卡死。

void MemoryLeaks()
{// 1.内存申请了忘记释放int* p1 = (int*)malloc(sizeof(int));int* p2 = new int;// 2.异常安全问题int* p3 = new int[10];Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放delete[] p3;
}

2.2 内存泄露分类

C/C++程序中一般我们关心两种方面的内存泄露:

  • 堆内存泄露(Heap leak) 堆内存指的是程序执行中依据需要分配通过malloc/calloc/realloc/new等从堆中分配的一块内存,用完后必须通过调用相应的free或者delete删除。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生HEap Leak。

  • 系统资源泄露 指程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

2.3 如何检测内存泄露

  • 在linux下内存泄漏检测:Linux下几款C++程序中的内存泄露检查工具_c++内存泄露工具分析-CSDN博客文章浏览阅读5.8w次,点赞46次,收藏294次。本文介绍了几种常用的Linux内存泄露检测工具,包括valgrind、mtrace、dmalloc和Kmemleak。文章详细阐述了每种工具的特点、安装方法及使用步骤,并提供了具体的示例程序。 https://blog.csdn.net/gatieme/article/details/51959654

  • 在windows下使用第三方工具:(不太靠谱)VS编程内存泄漏:VLD(Visual LeakDetector)内存泄露库_visual leak detector vs2020-CSDN博客文章浏览阅读1.2w次,点赞10次,收藏51次。背景: 这几日在学习一位朋友的LoggerTest日志编程时,碰到内存泄漏问题,由于VS自带的内存邪路检查不好用,因此出现内存问题时比较头疼,很难找到根源。经过一番搜索,得到一个内存泄漏工具:VLD(Visual LeakDetector)内存泄露库。_visual leak detector vs2020 https://blog.csdn.net/GZrhaunt/article/details/56839765

  • 其他工具:内存泄露检测工具比较 - 默默淡然 - 博客园1.ccmalloc-Linux和Solaris下对C和C++程序的简单的使用内存泄漏和malloc调试库。2.Dmalloc-Debug Malloc Library.3.Electric Fence-Linux分发版中由Bruce Perens编写的malloc()调试库。4.Leaky-Linhttps://www.cnblogs.com/liangxiaofeng/p/4318499.html

2.4 如何避免内存泄露

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。但是如果碰上异常时,就算注意释放了,还是可能会出现问题,需要智能指针来管理才有保证。

  2. 采用RAII思想或者智能指针来管理资源。

  3. 有些公司内部规范使用内部实现的私有的内存管理库。这些库自带内存泄漏检测的功能选项。

  4. 出问题了使用内存泄漏工具检测。不过很多工具都不靠谱,或者收费昂贵。

总结一下:

内存泄漏非常常见,解决方案分为两种:1、事前预防型号。如智能指针等。2、事后查错型。如泄漏检测工具。

3. 智能指针的使用

3.1 RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单计数。

  • RAII是Resource Acquisition Is Initialization的缩写,他是一种管理资源的类的设计思想,本质是一种利用对象声明周期来管理获取到的动态资源,避免资源泄露,这里的资源可以是内存,文件指针、网络互连、互斥锁等等。RAII再获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄露问题。
  • 智能指针除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会像迭代器类一样,重载operator*/operator->/operatpr[]等运算符,方便资源访问。

在对象构造时获取资源,记者控制堆资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显示的释放资源。

  • 采用这种方式,对象所需的资源在其生命周期内始终保持有效。、

智能指针除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会像迭代器类一样,重载operator*/operator->/operatpr[]等运算符,方便资源访问。

template<class T>
class SmartPtr
{
public:// RAIISmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout << "delete[]" << _ptr << endl;delete[] + ptr;}// 重载运算符,模拟指针的行为,方便资源的访问T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size i){return _ptr[i];}
private:T* _ptr;
};double Divide(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "Divide by zero condition";}else{return (double)a / (double)b;}
}void Func()
{// 这里使用RAII的智能指针类管理new出来的数据以后,程序就简单多了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;}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}catch (const exception& e){cout << e.what() << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}

智能指针最重要的问题是拷贝的问题。

template<class T>
class SmartPtr
{
public:// RAIISmartPtr(T* ptr = nullptr):_ptr(ptr){}~SmartPtr(){if(_ptr)delete _ptr;}// 重载运算符,模拟指针的行为,方便资源的访问T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};struct Date
{int _year;int _month;int _day;~Date(){cout << "~Date" << endl;}
};int main()
{SmartPtr<Date> sp1(new Date);SmartPtr<Date> sp2(sp1);return 0;
}

智能指针的拷贝问题会非常的难受,C++默认的拷贝构造实现的是浅拷贝,对内置类型完成的是浅拷贝,值拷贝,也就是说sp1和sp2里面的指针是一样的,浅拷贝就会导致析构两次,我们这里也不能实现深拷贝,一块资源给sp1管,我拷贝一下就是给sp2管,针对这个拷贝的问题是怎么解决的呢?

智能指针产生的历史非常早,在最早的时候C++98的时候就开始引入智能指针了,叫做auto_ptr,auto_ptr听名字就是自动指针。

库里面的智能指针都在memory这个头文件中。

- C++ Reference https://legacy.cplusplus.com/reference/memory/

总结一下智能指针的原理:

  1. RAII特性。

  2. 重载operator*和operator->,具有像指针一样的行为。

3.2 Boost库

98年到11年产生的,为C++语言标准库提供扩展C++程序库的总称。

Boost库是为C++语言标准库提供扩展的一些C++程序库的总称,由Boost社区组织开发、维护。Boost库可以与C++标准库完美共同工作,并且为其提供扩展功能。也可以说是准标准库

标准库和扩展库不一样,标准库编译器必须支持,包头文件就能用。

auto_ptr还不取消的原因就是C++标准库和标准语法不能取消,因为还有人在用,像这种第三方库就更自由了。

C++11里面很多核心的东西都是从Boost出来的,包括C++14,C++17,C++网络库在之前版本中主要也是参考boost库里面的ASIO的库。

所以Boost库就提供了scoped_ptr/scoped_array以及shared_ptr/shared_array,这两个智能指针,像RAII都支持,主要是解决拷贝问题,还有一个叫weak_ptr。

scoped_ptr/scoped_array主要是防拷贝,其实也是unique_ptr的前身,不让拷贝也不行,于是就搞出了shared_ptr。

shared_ptr/shared_array是支持拷贝的,用引用计数的方式支持拷贝,shared_ptr也被引入到了C++11,名字没有变化,共享指针,共享的本质其实就是引用计数。

weak_ptr,不同于上面智能指针,不支持直接管理资源,配合解决shared_ptr的一个缺陷,循环引用的问题,循环引用会导致内存泄漏。C++11也引入进来了,就叫weak_ptr,弱指针。

有了C++11我们也不用boost库了,C++11跟他可以说是平替。

3.3 std::auto_ptr

auto_ptr - C++ Referencehttps://legacy.cplusplus.com/reference/memory/auto_ptr/这个类被声明在C++11中,unique_ptr是新的家族系列类的的功能,以后推荐使用unique_ptr,不推荐使用auto_ptr,unique_ptr不提供拷贝功能。但是auto_ptr提供拷贝功能,为什么提供拷贝功能还推荐去使用unique_ptr呢?那么下面我们就来看一看。

我们可以看到,auto_ptr也可以完成自动释放。

我们可以看到,拷贝的话也不会崩。

但是,我要是对日期++的话就会崩。

他这个拷贝叫做管理权转移,比如ap1管理着一块资源,拷贝给sp2,但是如果都析构的话会析构两次,就会出问题,所以转移之后ap1就为空了。空了就空了,但是别人还能使用,一使用就有问题了。所以这个方式很坑,这个方式叫做拷贝时管理权转移,会导致被拷贝对象悬空。

3.4 std::unique_ptr

unique_ptr - C++ Referencehttps://legacy.cplusplus.com/reference/memory/unique_ptr/

scoped_ptr/scoped_array主要是防拷贝,其实也是unique_ptr的前身,不让拷贝也不行,于是就搞出了shared_ptr,这里我们先看unique_ptr。

unique_ptr如果管理多个对象呢?

底层默认是delete,就报错了,new和delete一定要匹配,new[]一定要匹配delete[]。

那这个时候第一种解决方案就是定制删除器。

给一个调用对象,这个可调用对象可以是lambda,仿函数,函数指针,包装器,调用D会对指针释放。

template<class T>
class SmartPtr
{
public:// RAIISmartPtr(T* ptr = nullptr):_ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;}// 重载运算符,模拟指针的行为,方便资源的访问T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};struct Date
{int _year;int _month;int _day;~Date(){cout << "~Date" << endl;}
};template<class T>
class DeleteArray
{
public:void operator()(T* ptr){delete[] ptr;}
private:
};class Fclose
{
public:void operator()(FILE* ptr){cout << "fclose:" << ptr << endl;fclose(ptr);}
private:
};int main()
{// C++11unique_ptr<Date> up1(new Date);// 不支持拷贝 - 拷贝构造用delete给禁掉了// unique_ptr<Date> up2(up1);// 定制删除器unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);unique_ptr<FILE,Fclose> up3(fopen("main.cpp","r"));// []的话也不用写定制删除器,有特化版本的话就可以这样写// 其他的话就不行了,其他还得依靠定制删除器unique_ptr<Date[]> up4(new Date[5]); // 走的第二个特化版本//vector<shared_ptr<Date>> v; // 一定会涉及到拷贝shared_ptr<Date> sp1(new Date);shared_ptr<Date> sp2(sp1);shared_ptr<Date> sp3(sp2);// shared_ptr是可以尝试看引用计数的cout << sp1.use_count() << endl;return 0;
}

unique_ptr这个部分使用上面不会有特别大的问题,正真的问题就是当我不是new的,而是new[]的,是fopen的,是malloc的,删除方式,释放方式得变化,那么这个地方也提供了对应的渠道。

get_deleter其实就是获取对应的删除器对象,平时也用不上。

3.5 std::shared_ptr

shared_ptr - C++ Referencehttps://legacy.cplusplus.com/reference/memory/shared_ptr/

shared_ptr中的shared是共享的意思,叫共享指针,支持多个对象管理同一块资源,本质就是支持拷贝。

shared_ptr跟这个有所不同。

shared_ptr<Date[]> sp4(new Date[5]); // 支持
//shared_ptr<FILE, Fclose> sp5(fopen("main.cpp", "r")); // 不支持,需要在构造函数的参数传.unique_ptr支持这样写。
shared_ptr<FILE> sp5(fopen("main.cpp", "r"), Fclose()); // 感觉像是两个人写的

shared_ptr的引用计数要用一个小块的内存去存,如果大量使用shared_ptr是有内存碎片的风险。

make_shared是一个模板的可变参数,make_shared会返回对应的shared_ptr。

shared_ptr<Date> sp6 = make_shared<Date>(2025, 05, 21);

可以传Date,也可以传构造Date的参数,我想构造一个日期类对象,这个日期类对象是new出来的,new出来以后我期望交给智能指针去管理,我可以直接new,也可以调make_shared。make_shared会在里面把日期类对象new出来,然后交给智能指针管理。

引用计数和对象是分离的,有一个智能指针就得有一个管理计数的小块内存,大量的使用shared_ptr的话就会有大量的小块内存,就有效率和内存碎片的问题,make_shared把日期给进去后他会把引用计数和Date放在一起,比如说Date占12字节,那么就会在前面把计数开前面,多开4个或者8个字节存计数,这样的话就不会出现要管理小块内存的问题了。

4. 智能指针的原理

4.1 std::auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针。

auto_ptr的实现原理:管理权转移的思想,下面简化模板实现了一份bit::auto_ptr来了解它的原理。

// C++98 管理权转移 auto_ptr
namespace Aurora
{template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& sp):_ptr(sp._ptr){// 管路权转移sp._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
//int main()
//{
//        std::auto_ptr<int> sp1(new int);
//        std::auto_ptr<int> sp2(sp1); // 管理权转移
//
//        // sp1悬空
//        *sp2 = 10;
//        cout << *sp2 << endl;
//        cout << *sp1 << endl;
//
//        return 0;
//}

4.2 std::unique_ptr

C++11中开始提供更靠谱的unique_ptr。

unique_ptr的实现原理:简单粗暴的防拷贝,下面简单模拟实现一份unique_ptr来了解它的原理。

// C++11库才更新智能指针实现
// C++11出来之前。boots搞出了更好用的scoped_ptr/shared_ptr/weak_ptr
// C++11将boots库中智能指针精华部分吸收了过来
// C++11->unique_ptr/shared_ptr/weak_ptr// unique_ptr/scoped_ptr
// 原理:简单粗暴 --防拷贝
namespace Aurora
{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>& sp) = delete;unique_ptr<T>& s operator=(const unique_ptr<T>& sp) = delete;private:T* _ptr;};
}//int main()
//{
//        Aurora::unique_ptr<int> sp1(new int);
//        Aurora::unique_ptr<int> sp2(sp1); // 防止拷贝
//
//        std::unique_ptr<int> up1(new int);
//        std::unique_ptr<int> up2(up1); // 防止拷贝
//
//        return 0;
//}

4.3 std::shared_ptr

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

  1. shared_ptr在其内部,给每个资源都维护了一份引用计数,用来记录该份资源被几个对象共享。

  2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。

  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源。

  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

make_shared的意义就是如果你给的资源是new,开好的,那么就得单独开计数,再new一次,new一个计数出来,比如给个日期类对象,里面就会开一块整空间,后面是日期类,前面四个或者八个字节是计数,计数有可能就和资源开到一起了,减少内存碎片。

4.3.1 shared_ptr代码

C Plus Plus: C++program code - Gitee.comhttps://gitee.com/Axurea/c-plus-plus/tree/master/2025_05_23_shared_ptr没有解决加锁的问题。

4.3.2 shared_ptr移动构造和移动赋值

这个shared_ptr有没有移动构造和移动赋值?

  • 我们不写编译器会不会默认生成?

不会,移动构造和移动赋值我们不写编译器默认生成的条件是:没有写拷贝构造,也没有写赋值,也没有写析构,这三个一个都没有写才会自动生成,而我们的代码是全给写了,全写了就不会生成了。

不会生成,我们也没有写,是左值会走shared_ptr(const shared_ptr<T>& sp),是右值会走shared_ptr<T>& operator=(const shared_ptr<T>& sp),const左值引用会引用左值也会引用右值。

  • 如果是右值,需不需要转移资源?可不可以转移资源?

可以转移资源,但是没必要,我写一个移动构造和移动赋值有必要吗?是左值,拷贝,加加计数,是右值,把你的资源都转移给我,有必要吗?没有必要,你是将亡值,是右值的时候就走拷贝构造,无非就是把你的资源的指针给我,把你的计数给我,我再++计数,你是右值,相当于我仅仅对你进行了拷贝,拷贝的时候仅仅++了计数而已,紧接着你就析构,析构就再减减一下计数,也就把你的资源相当于转移给我了。

严格来说,不是深拷贝的类都不需要移动构造shared_ptr本质还是浅拷贝,我给你一起指向资源,通过计数去释放的问题。所以这里我们不需要考虑拷贝构造,移动构造的问题。传值返回也是一样的,先拷贝,再加加计数,里面的生命周期到了再减减计数。

浅拷贝的类,移动构造和移动赋值都没有特别大的意义,所以我们不需要实现,编译器也不会默认生成,就算它生成了,如果是浅拷贝的类,它跟拷贝构造和赋值是一样的。

4.3.3 shared_ptr线程安全问题

namespace Aurora
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _pRefCount(new int(1)),_pmtx(new mutex){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pRefCount(sp._pRefCount),_pmtx(sp._pmtx){AddRef();}void AddRef(){_pmtx->lock();++(*_pRefCount);_pmtx->unlock();}void Release(){_pmtx->lock();bool flag = false;if (--(*_pRefCount) == 0 && _ptr){cout << "delete:" << _ptr << endl;delete _ptr;delete _pRefCount;flag = true;}_pmtx->unlock();if (flag == true){delete _pmtx;}}shared_ptr<T>& operator=(const shared_ptr<T>& sp){//if(this != &sp)if (_ptr != sp._ptr){Release();_ptr = sp._ptr;_pRefCount = sp._pRefCount;_pmtx = sp._pmtx;AddRef();}return *this;}int use_count(){return *_pRefCount;}~shared_ptr(){Release();}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get() const{return _ptr;}private:T* _ptr;int* _pRefCount;mutex* _pmtx;};// 简化版本的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<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}// shared_ptr智能指针是线程安全的吗?
// 是的,引用计数的加减是加锁保护的。但是指向资源不是线程安全的// 指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了
// 引用计数的线程安全问题,是智能指针要处理的
//int main()
//{
//	Aurora::shared_ptr<int> sp1(new int);
//	Aurora::shared_ptr<int> sp2(sp1);
//	Aurora::shared_ptr<int> sp3(sp1);
//
//	Aurora::shared_ptr<int> sp4(new int);
//	Aurora::shared_ptr<int> sp5(sp4);
//
//	//sp1 = sp1;
//	//sp1 = sp2;
//
//	//sp1 = sp4;
//	//sp2 = sp4;
//	//sp3 = sp4;
//
//	*sp1 = 2;
//	*sp2 = 3;
//
//	return 0;
//}

std::shared_ptr的线程安全问题

通过下面的程序我们来测试shared_ptr的线程安全问题。需要注意的是shared_ptr的线程安全分为两方面:

  1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2,这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、--是需要加锁的,也就是说引用计数的操作是线程安全的。

  2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。

// 1.演示引用计数线程安全问题:就把AddRefCount和SubRefCount中的锁去掉
// 2.演示可能不出现线程安全问题,因为线程安全问题是偶尔性出现的,main
// 函数的n改大一些概率就变大了,就容易出现了。
// 3.下面代码我们使用SharedPtr演示,是为了方便演示引用计数的线程
// 安全问题,将代码中的SharedPtr换成shared_ptr进行测试,可以验证
// 库的shared_ptr,发现结论是一样的。struct Date
{int _year = 0;int _month = 0;int _day = 0;
};void SharePtrFuunc(Aurora::shared_ptr<Date>& sp, size_t n, mutex& mtx)
{cout << sp.get() << endl;for (size_t i = 0; i < n; i++){// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。Aurora::shared_ptr<Date> copy(sp);// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个// 线程++了2n次,但是最终看到的结果,并不一定是加了2n{unique_lock<mutex> lk(mtx);copy->_year++;copy->_month++;copy->_day++;}}
}int main()
{Aurora::shared_ptr<Date> p(new Date);cout << p.get() << endl;const size_t n = 100000;mutex mtx;thread t1(SharePtrFuunc, std::ref(p), n, std::ref(mtx));thread t2(SharePtrFuunc, std::ref(p), n, std::ref(mtx));t1.join();t2.join();cout << p->_year << endl;cout << p->_month << endl;cout << p->_day << endl;cout << p.use_count() << endl;return 0;
}

4.3.4 shared_ptr循环引用问题

shared_ptr本身还有一个缺陷,这个缺陷叫循环引用,循环引用会导致内存泄漏

struct ListNode
{int _data;ListNode* _next;ListNode* _prev;~ListNode(){cout << "~ListNode()" << endl;}
};int main()
{std::shared_ptr<ListNode> n1(new ListNode);std::shared_ptr<ListNode> n2(new ListNode);// 一个是智能指针,一个是原生指针,智能指针不能给原生指针// 所以我们会考虑把两个原生指针变成智能指针n1->_next = n2; // errreturn 0;
}

修改代码,将结构体中的原生指针修改成智能指针。

struct ListNode
{int _data;std::shared_ptr<ListNode> _next;std::shared_ptr<ListNode> _prev;~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析构,右边的节点就释放了。

如果让n1的next指向n2,这个时候也不会有问题,n1是个智能指针,调operator->,operator->返回里面的LinstNode*,ListNode*再->就访问到next成员,next指向n2就是next跟着一起管n2,那我们的计数就++,这个时候也不会有问题,出了作用域n2先析构,后定义的先析构,n2析构,引用计数--,减到1,也就意味着右边的节点交给_next管理,_next析构,引用计数减到0,右边的空间也就释放了,_next是左边节点的成员,左边节点delete的时候就会调析构,析构的时候_next也就析构,接着出了作用域n1的生命周期就到了,--就减到0,然后释放左边的节点,调delete,就会调它的成员的析构,调到析构,会把右边节点的计数减到0,然后释放右边的节点,最后把n1释放。

出了作用域,n2先析构,n2析构之后,把计数减到1,再紧接着,n1析构,把计数减到1,就结束了。

n1和n2析构后,两个节点引用计数减到1

  1. 右边的节点什么时候释放呢?next管着呢,next析构,右边节点就释放了。

  2. next什么时候析构呢?next左边节点的成员,左边节点释放,next就析构。

  3. 左边的节点什么时候释放呢?prev管着呢,prev析构,左边节点就释放了。

  4. prev什么时候析构呢?prev右边节点的成员,右边节点释放,_prev就析构。

这就是一个死循环,这个计数永远减不到0,因为这两个对象永远不会释放,所以说它叫做循环引用,类似三角债,一旦形成循环引用,计数就到不了0,计数到不了0,就不会释放,不会释放就有内存泄漏。

我们把资源交给shared_ptr管理是没有问题的,但是是正确的管理,有了智能指针是正确使用智能指针就没有内存泄漏,不正确使用还是会导致内存的泄漏,就类似这样的场景。

库里面专门搞了weak_ptr来解决这个问题。

循环引用不太出现,但是一旦出现问题就很严重。

4.3.4.1 weak_ptr

weak_ptr - C++ Referencehttps://legacy.cplusplus.com/reference/memory/weak_ptr/weak_ptr其实也是boost库中引进来的,weak_ptr叫弱指针,这个和前面的智能指针都不一样,前面的智能指针都是可以交给资源让它进行管理的,而weak_ptr不支持RAII,不持支直接交给他资源。

// weak_ptr不支持管理资源,不支持RAII
std::weak_ptr<ListNode> wp(new ListNode); // err

弱指针不支持管理资源,设计出来就不是为了管理资源,所以它不是常规的智能指针。

它支持拷贝构造,它还支持用shared_ptr去构造或者赋值,当然支持自己拷贝。它用shared_ptr构造的时候不增加引用计数。

struct ListNode
{int _data;/*ListNode* _next;ListNode* _prev;*/std::weak_ptr<ListNode> _next;std::weak_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}};int main()
{std::shared_ptr<ListNode> n1(new ListNode);std::shared_ptr<ListNode> n2(new ListNode);n1->_next = n2;n2->_prev = n1;return 0;
}

如果你识别出了这是循环引用,解决这个的方案就是原生指针不行,就用weak_ptr就可以。weak_ptr的特点就是当你用shared_ptr赋值或者拷贝构造的时候,不增加引用计数。意味着我指向了它,但不增加计数。

用weak_ptr就没办法next = new node了,但是可以用make_shared,应该make_shared一个node,然后给next。

weak不增加引用计数,但是并不代表没有引用计数,也可以获取use_count,我跟你一起指向一个资源,但是这个资源,你的生命周期到了,被你释放了呢?我的生命周期还在呢,比如说我的生命周期比你长,我再外面,你在里面的一个代码块中,也有可能有这样的问题,所以weak_ptr也要有计数,weak_ptr可以根据expired判断是不是过期,过期就是判断我指向的是不是空,或者说我指向的那个shared_ptr管理的资源shared_prt有没有把它给释放,相当于引用计数有没有到0。

lock就相当于比如我现在正在用,shared_ptr释放了,我就悬空了,lock一下就相当于是增加一个shared_ptr管理,让这个shared_ptr跟我的生命周期一样,那这个时候,在我的生命周期之内就不至于说它被释放了。

struct ListNode
{int _data;/*ListNode* _next;ListNode* _prev;*/std::weak_ptr<ListNode> _next;std::weak_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}};int main()
{std::weak_ptr<ListNode> wp;{std::shared_ptr<ListNode> n1(new ListNode);wp = n1;cout << wp.expired() << endl; // 0 - 没有过期n1->_data++;   }cout << wp.expired() << endl; // 1 - 过期了return 0;
}

过期了就不能访问了,那么我怎么保持这个东西不过期呢?

struct ListNode
{int _data;/*ListNode* _next;ListNode* _prev;*/std::weak_ptr<ListNode> _next;std::weak_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}};int main()
{std::weak_ptr<ListNode> wp;// 创建一个跟我生命周期一样的对象std::shared_ptr<ListNode> sp;{std::shared_ptr<ListNode> n1(new ListNode);wp = n1;cout << wp.expired() << endl; // 0 - 没有过期n1->_data++;sp = wp.lock();}cout << wp.expired() << endl; // 1 - 过期了return 0;
}

首先创建一个跟weak_ptr一样的shared_ptr对象,然后再lock一下,这个时候就不过期了,因为这两个生命周期是一样的。

weak_ptr严格来说是要拿到计数的,才能判断是否过期,可是要实现的话很麻烦,这个计数如果有weak_ptr的话 就交给weak_ptr管理,如果没有的话就是shared_ptr管理,要实现的话还是很麻烦的。weak_ptr虽然不增加计数,但是也要能获取到计数,这样才能够判断是否过期,严格来说还得存一个_pcount,把计数给weak_ptr,shared_ptr析构的时候就不能释放计数,如果释放了计数,拿到的计数就是一个野指针,万一shared_ptr没有给weak_ptr的话shared_ptr就得释放,不然就没人释放了,所以这个计数还得带一个计数来管理,所以库里把这些计数单独又放了一个类去管理。

5. C++11和boost中智能指针的关系

  1. C++ 98中产生了第一个智能指针auto_ptr。

  2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr。

  3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。

  4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中实现的。

boost库里面实现了shared_array和scoped_array,C++11是没有提供unique_array的,是因为它用特化这个特性来支持的。

shared_ptr<Date[]> sp4(new Date[5]);

[]的时候就调delete[]释放,相当于特化就不需要单独实现shared_array这样的类了。

TR1(Technical Report 1)是C++标准委员会于2005年发布的一份技术报告,全称为 *ISO/IEC TR 19768:2005*。它的主要目的是为C++标准库提出扩展建议,作为对C++03标准的补充,并为未来的C++11标准(原称C++0x)铺路。TR1中的许多特性后来被正式纳入C++11及后续标准中。就相当于先交一些作业吧。

6. 模拟内存泄漏

int main()
{char* ptr = new char[1024 * 1024 * 1024]; // 申请1G// cout << ptr << endl; // char*类型,cout自动判断类型,找\0cout << (void*)ptr << endl; // 需要强转为void*return 0;
}
// 不管运行多少次电脑也不会卡死

我们的程序运行起来是以进程的方式运行的,内存是以进程为单位给分配空间的,叫虚拟进程地址空间,当你需要内存的时候,它会按页跟物理内存进行映射,访问虚拟内存的时候要根据映射转换,找到实际的物理内存,进程正常结束的话会把页表进行解掉,进程的这些资源都会释放掉,所以内存泄漏了,进行要是正常结束,也是没问题的。日常的内存泄露没有什么危害。但是有两种情况下会有问题:

  1. 进程没有正常结束,这个进程变成僵尸进程了呢?

  2. 我们平时写的程序,严格来说都是长期运行的,比如服务器程序。

快速的内存泄露还不怕,怕的就是每次泄露一点点。

相关文章:

  • 科研经验贴:AI领域的研究方向总结
  • DAO模式
  • Java转Go日记(五十六):gin 渲染
  • 提高 Maven 项目的编译效率
  • 大厂技术大神远程 3 年,凌晨 1 点到 6 点竟开会 77 次。同事一脸震惊,网友:身体还扛得住吗?
  • matlab时间反转镜算法
  • Appium+python自动化(四)- 如何查看程序所占端口号和IP
  • 动态防御体系实战:AI如何重构DDoS攻防逻辑
  • 交安安全员:交通工程安全领域的关键角色
  • DB-GPT扩展自定义Agent配置说明
  • 同为科技领军智能电源分配单元技术,助力物联网与计量高质量发展
  • Linux安装Nginx并配置转发
  • WPF性能优化之延迟加载(解决页面卡顿问题)
  • 园区/小区执法仪部署指南:ZeroNews低成本+高带宽方案”
  • 实时操作系统革命:实时Linux驱动的智能时代底层重构
  • EasyExcel使用
  • Git全流程操作指南
  • OS面试篇
  • SFP与Unsloth:大模型微调技术全解析
  • Lock锁
  • 网站建设与推广的步骤/关键词优化排名详细步骤
  • 京东网上商城购物平台/seo网站排名优化案例
  • 做外贸有哪些好的免费b2b网站/网络营销logo
  • 动易cms下载/重庆seo关键词优化服务
  • 网站主题推荐/seo包括哪些方面
  • 购物网站用那个软件做/百度指数分析大数据