哈尔滨企业网站模板建站,网站开发实验总结,平台设计图片,一分钟建站目录 1.为什么要使用智能指针#xff1f;
2.RAII和智能指针的设计思路
3.删除器
4.auto_ptr
4.1 auto_ptr的使用
4.2 auto_ptr的原理
5.unique_ptr
5.1 unique_ptr的使用#xff1a; 5.2 unique_ptr的原理#xff1a;
6.shared_ptr
6.1 shared_ptr的使用#xff1…目录 1.为什么要使用智能指针
2.RAII和智能指针的设计思路
3.删除器
4.auto_ptr
4.1 auto_ptr的使用
4.2 auto_ptr的原理
5.unique_ptr
5.1 unique_ptr的使用 5.2 unique_ptr的原理
6.shared_ptr
6.1 shared_ptr的使用
6.2 make_shared
6.3 shared_ptr的使用原理
7.shared_ptr的线程安全问题
8.shared_ptr循环引用问题 8.1 为什么使用shared_ptr会导致闭环呢
9.weak_ptr
9.1 weak_ptr的使用
9.2 weak_ptr的原理代码底层 10.C11和boost中智能指针的关系
11.内存泄漏
11.1 什么是内存泄漏内存泄漏的危害
11.2 如何避免内存泄漏 1.为什么要使用智能指针
智能指针就是帮我们C程序员管理动态分配的内存的它会帮助我们自动释放new出来的内存从而避免内存泄漏
那么咱们看下面的一个例子
#include iostream
#include string
#include memoryusing namespace std;// 动态分配内存没有释放就return
void memoryLeak1() {string *str new string(动态分配内存);return;
}// 动态分配内存虽然有些释放内存的代码但是被半路截胡return了
int memoryLeak2() {string *str new string(内存泄露);// ...此处省略一万行代码// 发生某些异常需要结束函数if (1) {return -1;}/// 另外使用try、catch结束函数也会造成内存泄漏/delete str; // 虽然写了释放内存的代码但是遭到函数中段返回使得指针没有得到释放return 1;
}int main(void) {memoryLeak1();memoryLeak2();return 0;
} memoryLeak1函数中new了一个字符串指针但是没有delete就已经return结束函数了导致内存没有被释放内存泄露 memoryLeak2函数中new了一个字符串指针虽然在函数末尾有些释放内存的代码delete str但是在delete之前就已经return了所以内存也没有被释放内存泄露
使用指针我们没有释放就会造成内存泄露。但是我们使用普通对象却不会
而智能指针恰好就是一个指针对象可以帮助我们自动释放内存的指针对象。
2.RAII和智能指针的设计思路
1.RAII是ResourceAcquisition Is Initialization的缩写他是⼀种管理资源的类的设计思想本质是 ⼀种利用对象生命周期来管理获取到的动态资源避免资源泄漏这里的资源可以是内存、文件指 针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象接着控制对资源的访问 资源在对象的生命周期内始终保持有效最后在对象析构的时候释放资源这样保障了资源的正常 释放避免资源泄漏问题。
2. 智能指针类除了满足RAII的设计思路还要方便资源的访问所以智能指针类还会想迭代器类⼀ 样重载 operator*/operator-/operator[] 等运算符方便访问资源。因为你一个智能指针下面有好多个接口所以你的智能指针得准备完全这个功能才可以。
那么在将解智能指针之前还得先来讲一下删除器。
3.删除器
struct Date
{int _year;int _month;int _day;Date(int year 1, int month 1, int day 1):_year(year), _month(month), _day(day){}~Date(){cout ~Date() endl;}
};
templateclass T
void DeleteArrayFunc(T* ptr)
{delete[] ptr;
}
templateclass T
class DeleteArray
{
public:void operator()(T* ptr){delete[] ptr;}
};
class Fclose
{
public:void operator()(FILE* ptr){cout fclose: ptr endl;fclose(ptr);}
};
int main()
{// 这样实现程序会崩溃// unique_ptrDate up1(new Date[10]);// shared_ptrDate sp1(new Date[10]);// 解决⽅案1// 因为new[]经常使⽤所以unique_ptr和shared_ptr// 实现了⼀个特化版本这个特化版本析构时⽤的delete[]//对于unique_ptr其特化版本大致如下简化//template typename T//class unique_ptrT[] {//public:// // ... 其他成员函数// ~unique_ptr() {// delete[] ptr;// }//};//注意以上只是概念上的简化实际实现更为复杂。unique_ptrDate[] up1(new Date[5]);shared_ptrDate[] sp1(new Date[5]);// 解决⽅案2// 仿函数对象做删除器//unique_ptrDate, DeleteArrayDate up2(new Date[5], DeleteArrayDate());// unique_ptr和shared_ptr⽀持删除器的⽅式有所不同// unique_ptr是在类模板参数⽀持的shared_ptr是构造函数参数⽀持的// 这⾥没有使⽤相同的⽅式还是挺坑的// 使⽤仿函数unique_ptr可以不在构造函数传递因为仿函数类型构造的对象直接就可以调⽤// 但是下⾯的函数指针和lambda的类型不可以unique_ptrDate, DeleteArrayDate up2(new Date[5]);shared_ptrDate sp2(new Date[5], DeleteArrayDate());// 函数指针做删除器unique_ptrDate, void(*)(Date*) up3(new Date[5], DeleteArrayFuncDate);shared_ptrDate sp3(new Date[5], DeleteArrayFuncDate);// lambda表达式做删除器auto delArrOBJ [](Date* ptr) {delete[] ptr; };unique_ptrDate, decltype(delArrOBJ) up4(new Date[5], delArrOBJ);shared_ptrDate sp4(new Date[5], delArrOBJ);// 实现其他资源管理的删除器shared_ptrFILE sp5(fopen(Test.cpp, r), Fclose());shared_ptrFILE sp6(fopen(Test.cpp, r), [](FILE* ptr) {cout fclose: ptr endl;fclose(ptr);});return 0;
}
咱们接下来看几个问题
1.为什么unique_ptrDate up1(new Date[10]);shared_ptrDate sp1(new Date[10]);程序会崩溃unique_ptrDate[] up1(new Date[5]);shared_ptrDate[] sp1(new Date[5]);而这个程序就不会崩溃呢 原因在于删除方式不匹配。 1. 当我们使用new[]分配数组时必须使用delete[]来释放内存。如果使用delete而不是delete[]来释放数组行为是未定义的Undefined Behavior。对于非平凡析构的类型即类有析构函数这通常会导致程序崩溃因为delete只会调用第一个元素的析构函数而不会调用整个数组的析构函数并且释放内存的方式也不正确。 2. 在提供的代码中unique_ptr和shared_ptr的默认删除器是delete。因此 - unique_ptrDate up1(new Date[10]);unique_ptr的默认删除器是delete但这里分配的是一个数组应该用delete[]。所以当up1析构时它会调用delete来释放内存而不是delete[]导致未定义行为崩溃。 - 同样shared_ptrDate sp1(new Date[10]);shared_ptr的默认删除器也是delete同样会导致错误。 3. 而unique_ptrDate[] up1(new Date[5]);和shared_ptrDate[] sp1(new Date[5]);使用了特化版本 - unique_ptr有一个针对数组的特化版本unique_ptrT[]其默认删除器是delete[]因此会正确释放数组。 - C17开始shared_ptr也支持数组特化shared_ptrT[]并且使用delete[]作为删除器。 因此使用特化版本unique_ptrT[]和shared_ptrT[]不会崩溃因为它们使用了正确的删除方式。 2.unique_ptr 是在类模板参数支持的 shared_ptr 是构造函数参数支持的 。
3.定制删除方式最好用shared_ptr其次就是最好用lambda做删除器。
4.注意lambda表达式每个都是唯一的类型所以用decltype(delArrOBJ)来获取类型。
其余的方式都在上面的代码中基本上用仿函数、函数指针、lambda表达式等自定义删除器。 C标准库中的智能指针都在 memory这个头文件下面我们包含memory就可以是使用了 智能指针有好几种除了weak_ptr他们都符合RAII和像指针⼀样访问的行为原理上而言主要是解 决智能指针拷贝时的思路不同。
那么C的智能指针的发展史挺曲折的为什么这么说呢第一代的auto_ptr智能指针可被骂惨了。之后的shared_ptr,unique_ptr才好起来至于weak_ptr只是为了解决shared_ptr这个智能指针中出现的某种问题而产生的。那么下面就听作者来讲解
4.auto_ptr
4.1 auto_ptr的使用
auto_ptr是C98时设计出来的智能指针他的特点是拷贝时把被拷贝对象的资源的管理权转移给 拷贝对象这是⼀个非常糟糕的设计因为他会导致被拷贝对象悬空那这个一旦指针悬空了那可就很危险了所以知道为什么当时这个智能指针出来之后很多公司不愿意用它。 其实这个地方咱们还是想用浅拷贝不想用深拷贝为什么呢因为智能指针不是拥有资源是代管资源。那么既然是代管资源这个智能指针拷贝的时候难道不应该两个都指向同一个对象吗所以咱们希望是浅拷贝。
例如上面这个图auto_ptr就是将假如将这个对象的资源管理权交给sp3,好然后呢好家伙sp1这个指针悬空了你说可怎么办吧。
struct Date
{int _year;int _month;int _day;Date(int year 1, int month 1, int day 1):_year(year), _month(month), _day(day){}~Date(){cout ~Date() endl;}
};int main()
{auto_ptrDate ap1(new Date(2025, 6, 6));// 拷贝时管理权转移就导致ap1悬空auto_ptrDate ap2(ap1);// 悬空访问ap1-_year;//此时你再去运行就会显示报错return 0;
}
看如上代码即可看出你对一个已经悬空的指针进行操作是会报错的。
4.2 auto_ptr的原理
这个auto_ptr的原理还是挺简单的
templateclass T
class auto_ptr
{
public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptrT sp):_ptr(sp._ptr){// 管理权转移sp._ptr nullptr;}auto_ptrT operator(auto_ptrT ap){// 检测是否为⾃⼰给⾃⼰赋值if (this ! ap){// 释放当前对象中资源if (_ptr)delete _ptr;// 转移ap中资源到当前对象中_ptr ap._ptr;ap._ptr NULL;}return *this;}~auto_ptr(){if (_ptr){cout delete: _ptr endl;delete _ptr;}}// 像指针⼀样使⽤T operator*(){return *_ptr;}T* operator-(){return _ptr;}
private:T* _ptr;
};
5.unique_ptr
5.1 unique_ptr的使用 unique_ptr是C11设计出来的智能指针他的名字翻译出来是唯⼀指针他的特点的不支持拷 贝只支持移动。如果不需要拷贝的场景就非常建议使用他。
struct Date
{int _year;int _month;int _day;Date(int year 1, int month 1, int day 1):_year(year), _month(month), _day(day){}~Date(){cout ~Date() endl;}
};
int main()
{
unique_ptrDate up1(new Date);// 不⽀持拷⻉
//unique_ptrDate up2(up1);// ⽀持移动但是移动后up1也悬空所以使⽤移动要谨慎
unique_ptrDate up3(move(up1));return 0;
} 5.2 unique_ptr的原理
templateclass T
class unique_ptr
{
public:explicit 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_ptrTsp) delete;unique_ptrT operator(const unique_ptrTsp) delete;unique_ptr(unique_ptrT sp):_ptr(sp._ptr){sp._ptr nullptr;}unique_ptrT operator(unique_ptrT sp){delete _ptr;_ptr sp._ptr;sp._ptr nullptr;}
private:T* _ptr;
};
这个unique_ptr就是不让用拷贝只可以移动大家记住这个就可以了。
6.shared_ptr
6.1 shared_ptr的使用
不知道大家还记不记得博主之前有一篇文章讲过写时拷贝那么这个写时拷贝就是运用了一个叫做引用计数的东西。
这个地方你不是害怕析构两次嘛。OK这里引入了“引用计数”。就是你有几个shared_ptr指针指向这个对象那么引用计数就是几。之后你要是想释放掉这个对象里的资源你得等到引用计数减为0的时候才可以释放。 shared_ptr是一种智能指针它通过引用计数来管理对象的生命周期。多个shared_ptr可以指向同一个对象当最后一个指向该对象的shared_ptr被销毁时对象才会被析构。 ### 引用计数的原理 1. * *引用计数的存储 * *引用计数通常是一个动态分配的整数或者更精确地说是一个控制块的一部分该控制块还包含其他信息如删除器等。每个由shared_ptr管理的对象都有一个关联的引用计数。 2. * *计数的增减 * * - 当一个新的shared_ptr被创建并指向同一个对象时例如通过拷贝构造函数、拷贝赋值运算符等引用计数会增加。 - 当一个shared_ptr被销毁例如离开作用域或者不再指向该对象例如被重置时引用计数会减少。 3. * *计数为0 * *当引用计数变为0时表示没有任何shared_ptr指向该对象此时对象会被删除通过调用其析构函数然后释放内存。 ### 为什么引用计数变为0时才析构 - **目的 * *确保对象在所有使用者都不再需要它时才被销毁避免悬空指针和内存泄漏。 - **原理 * *只要还有至少一个shared_ptr指向该对象引用计数就大于0对象就应该保持有效。只有当所有拥有该对象所有权的shared_ptr都释放了即引用计数归零才表示没有任何代码再需要访问该对象此时安全地销毁对象。 ### 注意 - **循环引用 * *如果两个对象使用shared_ptr相互引用则引用计数永远不会变为0导致内存泄漏。这时需要使用weak_ptr来打破循环。这个咱们后面会讲到的 ### 总结 shared_ptr的引用计数机制提供了一种自动管理对象生命周期的便捷方式确保对象在不再被需要时被销毁。这种机制的核心是当引用计数减到0时意味着没有任何智能指针指向该对象因此可以安全释放。 为何引用计数为 0 才析构 唯一性原则引用计数为 0 表明没有任何 shared_ptr 再需要该对象。 资源安全若提前析构如计数 0 时会导致其他持有该对象的 shared_ptr 成为悬空指针。 内存泄漏预防若永不析构如循环引用会导致内存泄漏。 案例 { shared_ptrMyObj p1(new MyObj); // 计数1 { shared_ptrMyObj p2 p1; // 计数2 } // p2 析构 → 计数1 } // p1 析构 → 计数0 → 调用 MyObj 析构函数 现在大家应该清楚shared_ptr是个怎么一回事了那么接下来咱们来看一下代码
struct Date
{int _year;int _month;int _day;Date(int year 1, int month 1, int day 1):_year(year), _month(month), _day(day){}~Date(){cout ~Date() endl;}
};
int main()
{shared_ptrDate sp1(new Date);// ⽀持拷⻉shared_ptrDate sp2(sp1);shared_ptrDate sp3(sp2);cout sp1.use_count() endl;sp1-_year;cout sp1-_year endl;cout sp2-_year endl;cout sp3-_year endl;// ⽀持移动但是移动后sp1也悬空所以使⽤移动要谨慎shared_ptrDate sp4(move(sp1));return 0;
}
1.cout sp1.use_count() endl 输出 3。这个use_count()是shared_ptr提供的接口函数作用是可以查出来这个sp1的引用计数是多少。
2.sp1-_year 将对象的 _year 从默认值 1 增至 2。 由于 sp1、sp2、sp3 共享同一对象cout 输出的 _year 均为 2。
3.shared_ptrDate sp4(move(sp1)) 将 sp1 的所有权转移至 sp4。 移动后sp1 变为空指针悬空sp4 的引用计数为 3继承自原 sp1。
OK那么这个shared_ptr的使用相信大家已经很清楚了那么接下来来看原理代码
6.2 make_shared
在此之前先将一个东西
shared_ptrDate sp1(new Date(2024, 9, 11));shared_ptrDate sp2 make_sharedDate(2024, 9, 11);
第一行的代码等价于第二行的代码等价于第三行的代码auto sp3 make_sharedDate(2024, 9, 11);
shared_ptr除了支持用指向资源的指针构造还支持make_shared用初始化资源对象的值直接构造。
咱们来看一个 工厂函数为什么将make_shared称为工厂函数呢咱们知道工厂是干嘛的是不是不管你的原材料是什么样的最后出来的成品是不是都是一样的虽然样貌不同但是用法一定是相同的。
所以std::make_shared提供了一种统一的方式来创建std::shared_ptr管理的对象。用户只需传递构造参数函数内部负责分配内存和构造对象。用户不需要显式地使用new操作符也不需要直接处理原始指针从而避免了内存泄漏的风险。是不是很好呢确实是的。 make_shared的本质呢其实是一个可变函数参数模板。它通过构造T对象的参数包底层呢其实底层也是使用了new,但运用了完美转发使参数保持原有的属性最后再返回即可。
6.3 shared_ptr的使用原理
这个的使用原来还是比较重要的。
templateclass T
class shared_ptr
{
public:explicit shared_ptr(T* ptr nullptr): _ptr(ptr), _pcount(new int(1))// 创建新的引用计数初始化为1{}// 带自定义删除器的构造函数templateclass Dshared_ptr(T* ptr, D del): _ptr(ptr), _pcount(new int(1))// 引用计数初始化为1, _del(del)// 存储自定义删除器{}shared_ptr(const shared_ptrT sp):_ptr(sp._ptr)// 共享同一指针, _pcount(sp._pcount)// 共享引用计数, _del(sp._del)// 共享删除器{(*_pcount);// 引用计数1}void release(){if (--(*_pcount) 0)// 引用计数-1{// 最后⼀个管理的对象释放资源_del(_ptr);// 使用删除器释放资源delete _pcount;// 释放引用计数内存_ptr nullptr;// 置空指针_pcount nullptr;// 置空计数指针}}shared_ptrT operator(const shared_ptrT sp){if (_ptr ! sp._ptr)// 避免自赋值{release();// 释放当前资源//这个地方的拷贝赋值涉及到几个问题。1.拷贝构造是把一个已经存在的//对象拷贝到不存在的对象上面所以这个不需要释放拷贝目标对象的资源。//但是赋值是等号两边的对象都存在资源所以你如果说不释放等号左边的对象的资源//直接赋值的话可能会导致左边资源找不到了。//2.所以咱们得先释放左边的资源//那么释放资源要注意如果这个资源是多个对象共同管理的就像这个一样//这个sp1资源目前是sp1,sp2共同管理的你如果说直接释放资源的话然后再赋值会导致sp2的资源找不到了// 并且这个资源是sp1sp2共同管理的凭什么资源的释放由你一个人决定// 所以就有了计数如果计数等于0说明目前没有对象在管理这个资源那么此时直接释放了没有任何问题// 要是计数不等于0说明还是有对象在管理这个资源那么此时只需要我放手不管理这个资源了我去管理sp3的资源即可// 这个原先的资源交给sp2去管理。所以计数的作用就体现在这里//(这里所说的对象就是shared_ptr指针对象管理的资源就是shared_ptr所指向的对象// 复制新资源_ptr sp._ptr;_pcount sp._pcount;(*_pcount);// 新资源的引用计数1_del sp._del;}return *this;}~shared_ptr(){release();// 释放资源引用计数-1为0时删除对象}T* get() const{return _ptr;// 获取原始指针}int use_count() const{return *_pcount;// 获取引用计数}T operator*(){return *_ptr;// 解引用}T* operator-(){return _ptr;// 成员访问}
private:T* _ptr;int* _pcount;//atomicint* _pcount;functionvoid(T*) _del [](T* ptr) {delete ptr; };//这个地方注意要加上//缺省值为什么呢是因为你如果说我传的是调用的是普通的delete的//那么这个参数列表是必须要走的但你要是不给_del缺省值这个地方是不是空//那么这个地方是空的话上面release的_del(_ptr)空的调用_ptr会抛异常bad_function_call异常
}; 看清楚上面是如何设计的这个版本涉及到自定义删除器。注意这个地方使用的是*_pcount用来计一下引用计数而不是使用static int _pcount来引用计数。原因是因为 比如上面的这俩你使用静态的对于sp1sp2来说可能正好但是对于sp3来说呢静态变量不光sp1sp2可以使用sp3也可以使用。但是sp3跟12管理的不是同一个对象呀所以不可以使用静态变量。要使用堆上动态开辟的方式构造智 能指针对象时来⼀份资源就要new⼀个引用计数出来。多个shared_ptr指向资源时就引用计 数shared_ptr对象析构时就--引用计数引用计数减到0时代表当前析构的shared_ptr是最后⼀ 个管理资源的对象则析构资源。
另外的一些其他的挺重要的我都放到代码中了大家自行阅读。
还有一点为什么上面的每一个构造函数前面都加上了explicit呢这是因为智能指针不允许进行隐式类型转换。就是不允许从原始指针转换为智能指针。
void func(shared_ptrDate sp);//这个函数里面是智能指针 Date* raw_ptr new Date;//原始指针 func(raw_ptr); // 危险隐式转换为 shared_ptr
比如上面的这个例子。
7.shared_ptr的线程安全问题
1.shared_ptr的引用计数对象在堆上如果多个shared_ptr对象在多个线程中进行shared_ptr的拷 贝析构时会访问修改引用计数就会存在线程安全问题所以shared_ptr引用计数是需要加锁或者 原子操作保证线程安全的。
2. shared_ptr指向的对象也是有线程安全的问题的但是这个对象的线程安全问题不归shared_ptr 管它也管不了应该有外层使用shared_ptr的人进行线程安全的控制。
3. 下⾯的程序会崩溃或者A资源没释放shared_ptr引用计数从int*改成atomic*就可以保证 引用计数的线程安全问题或者使用互斥锁加锁也可以。这个可能得等到Linux章节里才会讲。
struct AA
{int _a1 0;int _a2 0;~AA(){cout ~AA() endl;}
};
int main()
{shared_ptrAA p(new AA);const size_t n 100000;mutex mtx;auto func [](){for (size_t i 0; i n; i){// 这⾥智能指针拷⻉会计数shared_ptrAA copy(p);{unique_lockmutex lk(mtx);copy-_a1;copy-_a2;}}};thread t1(func);thread t2(func);t1.join();t2.join();cout p-_a1 endl;cout p-_a2 endl;cout p.use_count() endl;return 0;
}
输出结果为 200000
200000
1
~AA() OK那么咱们来看一下经过这个版本改后的shared_ptr的底层代码
#includefunctional
#includeatomic
templateclass T
class shared_ptr
{
public:templateclass Dshared_ptr(T* ptr, D del):_ptr(ptr), _pcount(new atomicint(1)), _del(del){}shared_ptr(T* ptr nullptr):_ptr(ptr), _pcount(new atomicint(1)){}// sp2(sp1)shared_ptr(const shared_ptrT sp):_ptr(sp._ptr), _pcount(sp._pcount){(*_pcount);}// sp1 sp3shared_ptrT operator(const shared_ptrT sp){if (_ptr ! sp._ptr){release();//这个地方的拷贝赋值涉及到几个问题。1.拷贝构造是把一个已经存在的//对象拷贝到不存在的对象上面所以这个不需要释放拷贝目标对象的资源。//但是赋值是等号两边的对象都存在资源所以你如果说不释放等号左边的对象的资源//直接赋值的话可能会导致左边资源找不到了。//2.所以咱们得先释放左边的资源//那么释放资源要注意如果这个资源是多个对象共同管理的就像这个一样//这个sp1资源目前是sp1,sp2共同管理的你如果说直接释放资源的话然后再赋值会导致sp2的资源找不到了// 并且这个资源是sp1sp2共同管理的凭什么资源的释放由你一个人决定// 所以就有了计数如果计数等于0说明目前没有对象在管理这个资源那么此时直接释放了没有任何问题// 要是计数不等于0说明还是有对象在管理这个资源那么此时只需要我放手不管理这个资源了我去管理sp3的资源即可// 这个原先的资源交给sp2去管理。所以计数的作用就体现在这里//(这里所说的对象就是shared_ptr指针对象管理的资源就是shared_ptr所指向的对象_ptr sp._ptr;_pcount sp._pcount;(*_pcount);//引用计数加1是因为又有一个对象来管理这个资源.}return *this;}void release(){if (--(*_pcount) 0){//delete _ptr;_del(_ptr);delete _pcount;}}~shared_ptr(){release();}// ָһʹT operator*(){return *_ptr;}T* operator-(){return _ptr;}T* get(){return _ptr;}int use_count(){return *_pcount;}operator bool(){return _ptr ! nullptr;}
private:T* _ptr;// 管理的原始指针//int* _pcount;atomicint* _pcount;// 引用计数指针functionvoid(T*) _del [](T* ptr) {delete ptr; };//这个地方注意要加上//缺省值为什么呢是因为你如果说我传的是调用的是普通的delete的//那么这个参数列表是必须要走的但你要是不给_del缺省值这个地方是不是空//那么这个地方是空的话上面release的_del(_ptr)空的调用_ptr会抛异常bad_function_call异常
}; 8.shared_ptr循环引用问题
先来看一段代码
struct ListNode
{int _data;/*ListNode* _next;ListNode* _prev;*//*std::shared_ptrListNode _next;std::shared_ptrListNode _prev;*/std::weak_ptrListNode _next;std::weak_ptrListNode _prev;~ListNode(){cout ~ListNode() endl;}
};int main()
{// 循环引用 -- 内存泄露std::shared_ptrListNode n1(new ListNode);std::shared_ptrListNode 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;
} 代码中定义了一个链表节点结构体ListNode它包含 int _data; shared_ptrListNode _next; shared_ptrListNode _prev; 在main函数中创建了两个共享指针n1和n2分别指向两个ListNode对象。 初始时n1和n2的引用计数都是1。 然后执行 n1-_next n2; // 这会让n2的引用计数增加1因为_next是shared_ptr赋值会拷贝所以n2的引用计数变为2 n2-_prev n1; // 这会让n1的引用计数增加1同理n1的引用计数变为2 当main函数结束时n1和n2的生命周期结束它们的析构函数被调用会分别将引用计数减1。 此时n1指向的节点的引用计数从2减为1因为n2的_prev还指向它所以不会释放n1节点。(因为shared_ptr只有在引用计数为0的时候才会去释放这个对象所以这个问题的本质还是因为引用计数不为0就是shared_ptr会增加引用计数 同样n2指向的节点的引用计数从2减为1因为n1的_next还指向它所以也不会释放n2节点。 这样就形成了循环引用导致内存泄漏。 将_next和_prev改为weak_ptr。因为weak_ptr不会增加引用计数。 修改后的ListNode struct ListNode { int _data; std::weak_ptrListNode _next; std::weak_ptrListNode _prev; ~ListNode() { cout ~ListNode() endl; } }; 这样当执行 n1-_next n2; // 因为是weak_ptr所以n2的引用计数还是1weak_ptr的赋值不会增加shared_ptr的引用计数 n2-_prev n1; // 同理n1的引用计数还是1 在main函数结束时 n2析构引用计数减1变为0于是释放n2指向的对象在释放n2对象时其成员_prevweak_ptr也会被销毁但这不会影响n1的引用计数。 n1析构引用计数减1变为0于是释放n1指向的对象。 因此使用weak_ptr打破了循环引用。 注意weak_ptr只能指向由shared_ptr管理的对象并且不会影响其生命周期。当需要访问对象时可以通过lock()函数获得一个shared_ptr 8.1 为什么使用shared_ptr会导致闭环呢
还是上面的那个代码如果按照上面的那个例子 大家看我上面的这个图很好这样就形成了一个闭环。谁也降服不了谁。主要问题还是引用计数的问题
所以解决办法就是上面我写的把ListNode结构体中的_next和_prev改成weak_ptrweak_ptr绑定到shared_ptr时不会增加它的 引用计数_next和_prev不参与资源释放管理逻辑就成功打破了循环引用解决了这里的问题
9.weak_ptr
9.1 weak_ptr的使用
老说这个weak_ptr可以解决上面的这个问题那这个到底是何方神圣weak_ptr:弱指针。
咱们上面说的RAII它并不遵守。weak_ptr不支持RAII也不支持访问资源所以我们看文档发现weak_ptr构造时不支持绑定到资 源只支持绑定到shared_ptr绑定到shared_ptr时不增加shared_ptr的引用计数那么就可以 解决上述的循环引用问题。
所以他的构造不像上面那两个一样采用指针接收的方法。 都是要么是空要么是引用接收。
weak_ptr也没有重载operator*和operator-等因为他不参与资源管理那么如果他绑定的 shared_ptr已经释放了资源那么他去访问资源就是很危险的。weak_ptr支持expired检查指向的 资源是否过期use_count也可获取shared_ptr的引用计数weak_ptr想访问资源时可以调用 lock返回⼀个管理资源的shared_ptr如果资源已经被释放返回的shared_ptr是⼀个空对象如 果资源没有释放则通过返回的shared_ptr访问资源是安全的 。
大家来看一个例子就差不多明白了
int main()
{std::shared_ptrstring sp1(new string(111111));//创建一个shared_ptr管理一个string对象内容为111111。引用计数为1。std::shared_ptrstring sp2(sp1);//通过拷贝构造sp2和sp1共享同一个对象引用计数变为2。std::weak_ptrstring wp sp1;//创建一个weak_ptr wp它观察sp1所管理的对象。注意weak_ptr的创建不会增加引用计数所以引用计数还是2。cout wp.expired() endl;//expired()返回weak_ptr是否过期即它观察的对象是否已经被释放。此时对象还在所以返回false0。cout wp.use_count() endl;//返回与weak_ptr共享对象的shared_ptr的数量即引用计数这里应该是2。// sp1和sp2都指向了其他资源则weak_ptr就过期了sp1 make_sharedstring(222222);//sp1被重新赋值指向一个新的string对象内容为222222。这样原来的对象引用计数减1变为1因为sp2还指向它。wp仍然观察原来的对象。cout wp.expired() endl;//因为原来的对象还没有被释放sp2还指向它所以返回false0。cout wp.use_count() endl;//现在原来的对象的引用计数是1只有sp2指向它所以这里输出1。sp2 make_sharedstring(333333);//sp2也被重新赋值指向一个新的string对象内容为333333。这样原来的对象引用计数再减1变为0于是被释放。cout wp.expired() endl;//因为原来的对象已经被释放所以wp过期返回true1。cout wp.use_count() endl;//此时wp观察的对象已经释放引用计数为0所以输出0。wp sp1;//将wp重新指向sp1当前管理的对象即内容为222222的对象。//std::shared_ptrstring sp3 wp.lock();/*lock()获取共享指针shared_ptrT lock() const noexcept;返回一个管理被观察对象的 shared_ptr对象有效时增加引用计数对象无效时返回空的 shared_ptr*/auto sp3 wp.lock();//通过lock()函数wp尝试获取一个shared_ptr。因为sp1管理的对象还存在引用计数至少为1所以lock()返回一个有效的shared_ptrsp3此时引用计数增加1sp1和sp3所以引用计数为2。cout wp.expired() endl;//现在wp观察的对象是sp1管理的对象它还存在所以返回false0。cout wp.use_count() endl;//返回当前共享对象的shared_ptr个数即引用计数应该是2sp1和sp3。*sp3 ###;//通过sp3修改字符串在字符串末尾追加###。cout *sp1 endl;//输出sp1指向的字符串由于sp1和sp3指向同一个对象所以输出的是修改后的字符串222222###。return 0;
}
大家要细细的观摩上面代码。你不仅会巩固之前的只是还能够对weak_ptr理解更深。
9.2 weak_ptr的原理代码底层
// 需要注意的是我们这⾥实现的shared_ptr和weak_ptr都是以最简洁的⽅式实现的
// 只能满⾜基本的功能这⾥的weak_ptr lock等功能是⽆法实现的想要实现就要
// 把shared_ptr和weak_ptr⼀起改了把引⽤计数拿出来放到⼀个单独类型shared_ptr
// 和weak_ptr都要存储指向这个类的对象才能实现有兴趣可以去翻翻源代码
templateclass T
class weak_ptr
{
public:weak_ptr()// 默认构造空指针{}// 从 shared_ptr 构造weak_ptr(const shared_ptrT sp):_ptr(sp.get())// 获取但不增加引用计数{}// 从 shared_ptr 赋值weak_ptrT operator(const shared_ptrT sp){_ptr sp.get();// 获取指针但不增加计数return *this;}
private:T* _ptr nullptr;// 观察的指针不管理生命周期
}; 10.C11和boost中智能指针的关系
Boost库是为C语言标准库提供扩展的⼀些C程序库的总称Boost社区建立的初衷之⼀就是为 C的标准化工作提供可供参考的实现Boost社区的发起⼈Dawes本⼈就是C标准委员会的成员 之⼀。在Boost库的开发中Boost社区也在这个方向上取得了丰硕的成果C11及之后的新语法 和库有很多都是从Boost中来的。
1. C98中产生了第⼀个智能指针auto_ptr。
2. Cboost给出了更实用的scoped_ptr/scoped_array和shared_ptr/shared_array和weak_ptr等.
3. CTR1引入了shared_ptr等不过注意的是TR1并不是标准版。
4. C11引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的 scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
11.内存泄漏
11.1 什么是内存泄漏内存泄漏的危害
什么是内存泄漏内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存⼀般是忘记释 放或者发生异常释放程序未能执行导致的。内存泄漏并不是指内存在物理上的消失而是应用程序分 配某段内存后因为设计错误失去了对该段内存的控制因而造成了内存的浪费。
内存泄漏的危害普通程序运行⼀会就结束了出现内存泄漏问题也不大进程正常结束页表的映射 关系解除物理内存也可以释放。长期运行的程序出现内存泄漏影响很大如操作系统、后台服 务、长时间运行的客户端等等不断出现内存泄漏会导致可用内存不断变少各种功能响应越来越 慢最终卡死。 int main() { // 申请一个1G未释放这个程序多次运行也没啥危害 // 因为程序马上就结束进程结束各种资源也就回收了 char* ptr new char[1024 * 1024 * 1024]; cout (void*)ptr endl; return 0; } 11.2 如何避免内存泄漏
⼯程前期良好的设计规范养成良好的编码规范申请的内存空间记着匹配的去释放。ps这个理 想状态。但是如果碰上异常时就算注意释放了还是可能会出问题。需要下⼀条智能指针来管理 才有保证。
1. 尽量使用智能指针来管理资源如果自己场景比较特殊采用RAII思想自己造个轮子管理。
2. 定期使用内存泄漏工具检测尤其是每次项目快上线前不过有些工具不够靠谱或者是收费。 3. 总结⼀下内存泄漏非常常见解决方案分为两种1、事前预防型。如智能指针等。2、事后查错 型。如泄漏检测工具。
OK本篇完........................................