【C++】智能指针RALL实现shared_ptr
个人主页 : zxctscl
专栏 【C++】、 【C语言】、 【Linux】、 【数据结构】、 【算法】
如有转载请先通知
文章目录
- 1. 为什么需要智能指针?
- 2. 内存泄漏
- 2.1 什么是内存泄漏,内存泄漏的危害
- 2.2 内存泄漏分类(了解)
- 2.3 如何检测内存泄漏(了解)
- 2.4 如何避免内存泄漏
- 3. 智能指针的使用及原理
- 3.1 RAII
- 3.2 智能指针的原理
- 3.3 std::auto_ptr
- 3.4 std::unique_ptr
- 3.5 std::shared_ptr
- 3.5.1 ==实现shared_ptr==
- 3.5.1.1 构造+拷贝构造+析构
- 3.5.1.2 赋值
- 3.5.1.3 shared_ptr类代码
- 3.5.2 make_shared
- 3.5.3 线程安全
- 3.5.3.2 std::shared_ptr的线程安全问题
- 3.5.3.2 线程安全下shared_ptr类实现
- 3.5.4 循环引用
- 3.5.4.1 什么是循环引用
- 3.5.4.2 weak_ptr
- 3.5.5 定制删除器(了解)
- 3.5.6 定制删除器下实现shared_ptr的代码
- 4. C++11和boost中智能指针的关系
上次分享到 【C++】C++中异常及异常的使用,有了异常,就很难控制,那么该怎么办呢?这时候就来看看智能指针了。
1. 为什么需要智能指针?
在异常中提到的下面这段代码,可能会出现p1申请空间时候出现异常,p2申请空间也可能会出现异常,所以p1释放时候要捕获,Division时候也可能会抛异常,就得把p1、p2释放掉。
示例代码:
double Division(int a, int b)
{if (b == 0)throw invalid_argument("除0错误");return a / b;
}void Func()
{int* p1 = new int[10];int* p2 = nullptr;try{p2 = new int[20];try{int len, time;cin >> len >> time;cout << Division(len, time) << endl; // throw}catch (...){delete[] p1;cout << "delete:" << p1 << endl;delete[] p2;cout << "delete:" << p2 << endl;throw; // 捕获什么抛出什么}}catch (...){delete[] p1;cout << "delete:" << p1 << endl;throw;}delete[] p1;cout << "delete:" << p1 << endl;delete[] p2;cout << "delete:" << p2 << endl;
}
C++就借助构造函数和析构函数来帮助解决上面问题。
构造一个SmartPtr类把指针保存起来,再析构的时候就对这个指针进行释放。
class SmartPtr
{
public:SmartPtr(int * ptr):_ptr(ptr){}~SmartPtr(){delete[] _ptr;cout << "delete:" << _ptr << endl;}private:int* _ptr;
};
只要生命周期结束就可以调用析构函数释放。
int main()
{SmartPtr sp1(new int[10]);retuen 0;
}
此时它就能释放,构造函数保存资源,析构函数释放资源:
C++11中确定不抛异常就加上noexcept
2. 内存泄漏
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下几款内存泄漏检测工具
在windows下使用第三方工具: VLD工具说明
其他工具: 内存泄漏工具比较
2.4 如何避免内存泄漏
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
- 采用RAII思想或者智能指针来管理资源。
- 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结一下:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具
3. 智能指针的使用及原理
3.1 RAII
RAII
(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效**,最后在对象析构的时候释放资源**。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
(1)不需要显式地释放资源。
(2)采用这种方式,对象所需的资源在其生命期内始终保持有效。
上面示例代码p1申请空间时候出现异常,p2申请空间也可能会出现异常,有了SmartPtr,直接不定义p1,直接定义一个SmartPtr sp1
,同样再定义一个SmartPtr sp2
:
void Func()
{SmartPtr sp1(new int[10]);SmartPtr sp2(new int[20]);int len, time;cin >> len >> time;cout << Division(len, time) << endl;
}
对比之下,左边嵌套捕获、释放,而右边就没有:
测试一下代码:
double Division(int a, int b)
{if (b == 0)throw invalid_argument("除0错误");return a / b;
}class SmartPtr
{
public:SmartPtr(int* ptr):_ptr(ptr){}~SmartPtr(){delete[] _ptr;cout << "delete:" << _ptr << endl;}private:int* _ptr;
};void Func()
{SmartPtr sp1(new int[10]);SmartPtr sp2(new int[20]);int len, time;cin >> len >> time;cout << Division(len, time) << endl;
}int main()
{/*SmartPtr sp1(new int[10]);*/try{Func();}catch (const exception& e){cout << e.what() << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}
会正常释放,就不用担心忘记释放而导致抛异常了:
目前SmartPtr只能管理整型,那么将它改为模板,它有时候会用到它原生指针,还能重载operator*
、重载operator->
、operator[]
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){delete[] _ptr;cout << "delete:" << _ptr << endl;}T* get(){return _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}private:T* _ptr;
};void Func()
{SmartPtr<int> sp1(new int[10]);SmartPtr<int> sp2(new int[20]);int len, time;cin >> len >> time;cout << Division(len, time) << endl;
}
3.2 智能指针的原理
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。
template<class T>
class SmartPtr {
public:SmartPtr(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;
};
int main()
{SmartPtr<int> sp1(new int);*sp1 = 10cout << *sp1 << endl;SmartPtr<int> sparray(new Date);// 需要注意的是这里应该是sparray.operator->()->_year = 2018;// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->sparray->_year = 2018;sparray->_month = 1;sparray->_day = 1;
}
总结一下智能指针的原理:
- RAII特性
- 重载operator*和opertaor->,具有像指针一样的行为。
3.3 std::auto_ptr
在示例代码中:
double Division(int a, int b)
{if (b == 0)throw invalid_argument("除0错误");return a / b;
}template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){delete[] _ptr;cout << "delete:" << _ptr << endl;}T* get(){return _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}
private:T* _ptr;
};void Func()
{SmartPtr<int> sp1(new int[10]);SmartPtr<int> sp2(new int[20]);int len, time;cin >> len >> time;cout << Division(len, time) << endl;
}int main()
{SmartPtr<int> sp1(new int[10]);SmartPtr<int> sp2(sp1);int* p1 = new int[10];int* p2 = p1;return 0;
}
sp2去拷贝构造sp1,会导致析构两次,也就是浅拷贝的问题:
模拟指针的行为,是不能深拷贝的,期望的是浅拷贝,想要sp1和sp2一起管理资源。
拷贝问题怎么解决呢?
早期解决用了std::auto_ptr
std::auto_ptr文档
C++98版本的库中就提供了auto_ptr的智能指针。
auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份bit::auto_ptr来了解它的原理:
double Division(int a, int b)
{if (b == 0)throw invalid_argument("除0错误");return a / b;
}template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){delete[] _ptr;cout << "delete:" << _ptr << endl;}T* get(){return _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}
private:T* _ptr;
};void Func()
{SmartPtr<int> sp1(new int[10]);SmartPtr<int> sp2(new int[20]);int len, time;cin >> len >> time;cout << Division(len, time) << endl;
}int main()
{auto_ptr<int> sp1(new int(1));// C++98 转移管理权,sp1对象悬空auto_ptr<int> sp2(sp1);return 0;
}
拷贝构造的时候就转移资源,
C++98 转移管理权,sp1对象悬空,如果在不知道这个特性时候使用,就会崩溃:
这个内部最主要就是:
auto_ptr(auto_ptr<T>& sp):_ptr(sp._ptr)
{// 管理权转移sp._ptr = nullptr;
}
Boost库里面
C++11拿其中scoped_ptr和shared_ptr和weak_ptr,并将scoped_ptr改名为unique_ptr。
在memory中
管理权限转移,被拷贝对象悬空,有风险
3.4 std::unique_ptr
C++11中开始提供更靠谱的unique_ptr
unique_ptr文档
unique_ptr不支持拷贝构造:
class A
{
public:~A(){cout << "~A()" << endl;}
private:int _a1 = 1;int _a2 = 1;
};int main()
{unique_ptr<A> sp1(new A);unique_ptr<A> sp2(sp1);return 0;
}
它不支持拷贝构造:
可以获取到它原生指针:
还可以做条件判断,如果成员变量是公有的就能用operator->:
多个对象就能用operator[]:
unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原理。
// C++11库才更新智能指针实现
// C++11出来之前,boost搞除了更好用的scoped_ptr/shared_ptr/weak_ptr
// C++11将boost库中智能指针精华部分吸收了过来
// C++11->unique_ptr/shared_ptr/weak_ptr
// unique_ptr/scoped_ptr
// 原理:简单粗暴 -- 防拷贝
namespace bit
{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>& operator=(const unique_ptr<T>& sp) = delete;private:T* _ptr;};
}
//int main()
//{
// /*bit::unique_ptr<int> sp1(new int);
// bit::unique_ptr<int> sp2(sp1);*/
//
// std::unique_ptr<int> sp1(new int);
// //std::unique_ptr<int> sp2(sp1);
//
// return 0;
//}
// 引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源
3.5 std::shared_ptr
shared_ptr支持拷贝构造
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr
std::shared_ptr文档
不支持隐式类型转换:
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
class A
{
public:~A(){cout << "~A()" << endl;}
private:int _a1 = 1;int _a2 = 1;
};int main()
{shared_ptr<A> sp1(new A);shared_ptr<A> sp2(sp1);/*shared_ptr<A> sp3 = make_shared<A>(1, 1);*/cout << sp1.use_count() << endl;{shared_ptr<A> sp3(sp1);cout << sp1.use_count() << endl;}cout << sp1.use_count() << endl;return 0;
}
用了引用计数;
make_shared构造一个智能指针
make_shared构造一个智能指针来管理A,来返回一个对象:
3.5.1 实现shared_ptr
3.5.1.1 构造+拷贝构造+析构
要想实现shared_ptr最主要的就是实现引用计数。
如果用一个成员变量,是不行的,因为当p1和p2管理相同的资源,每个对象里面都有一个引用计数,就不合适。
如果用静态或者全局成员变量,也不可行,当sp1和sp2管理同一个资源时候没有什么问题;但是当再有一个sp3的时候,也需要一个计数。所以不能单只有一个计数,而静态成员是属于所有对象的,就不能再记录sp3的。
我们想要的是一个资源就伴随一个计数。可以开多个计数,管理同一个资源的技术就能用它们的指针指向这个计数,就能找到这个计数,就不用存到每一个对象里面。
所以这里计数成员变量用指针来实现:
template<class T>class Share_ptr{public:private:T* _ptr;int* _pcount;};
shared_ptr构造时就给计数开一个空间,用来计数。
当sp2和sp1共用一个空间时候,计数的指针指向的资源就加加
template<class T>class shared_ptr{public:shared_ptr(T* ptr):_ptr(ptr),_pcount(new int(1)){}//sp2(sp1)shared_ptr(const shared_ptr<T>& sp):_ptr(sp._pcount), _pcount(sp._pcount){(*_pcount)++;}private:T* _ptr;int* _pcount;};
#include"Share_ptr.h"int main()
{bit::shared_ptr<A> sp1(new A(1,1));bit::shared_ptr<A> sp2(sp1);bit::shared_ptr <A> sp3(new A(2,2));return 0;
}
一个资源有一个计数,需要共管这个资源的对象就加加这个计数就行:
析构:把计数置为0,再释放资源:
~shared_ptr(){if (--(*_pcount) == 0){//最后一个管理的对象释放资源delete _ptr;}}
use_count()来获取*_pcount:
int use_count(){return *_pcount;}
就能看到获得资源数量:
3.5.1.2 赋值
加入将sp3赋值给sp1,不能直接将sp1释放,是希望sp1和sp3共同管理资源,sp1的资源是和sp2共用的。
在赋值里面就相等于调析构,但是不能真的去调用析构,就可以重新写一个release()减减计数,如果减到0就释放:
void release(){if (--(*_pcount) == 0){//最后一个管理的对象释放资源delete _ptr;}}~shared_ptr(){release();}
如果只有sp1就释放;如果sp1的资源还有其他共用,就减减计数,再让sp3共管同一块资源,再加加计数:
//sp1=sp3shared_ptr<T>& operator=(const shared_ptr<T>& sp){this->release();_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);return *this;}
测试一下:
此时sp1计数就变成了4
就是这样的关系图:
如果自己给自己赋值,就会出现值丢失的情况,因为sp6只有一个对象管理,一赋值,就先调用release(),就直接释放了:
解决方法,先判断是不是自己给自己赋值
//sp1=sp3shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){this->release();_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}
3.5.1.3 shared_ptr类代码
namespace bit
{template<class T>class shared_ptr{public:shared_ptr(T* ptr):_ptr(ptr),_pcount(new int(1)){}//sp2(sp1)shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}void release(){if (--(*_pcount) == 0){//最后一个管理的对象释放资源delete _ptr;}}//sp1=sp3shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){this->release();_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}~shared_ptr(){release();}int use_count(){return *_pcount;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;int* _pcount;};
}
3.5.2 make_shared
make_shared可以减少内存碎片,它不仅仅会构造对象还会构造计数。
3.5.3 线程安全
3.5.3.2 std::shared_ptr的线程安全问题
std::shared_ptr的线程安全问题
通过下面的程序我们来测试shared_ptr的线程安全问题。需要注意的是shared_ptr的线程安全分为两方面:
- 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的。
- 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
创建一个链表,再创建两个线程,想要在链表里面插入东西。
先用func函数来实现链表插入,再调用来创建线程:
void func(bit::shared_ptr<list<int>> sp, int n)
{for (int i = 0; i < n; i++){sp->emplace_back(i);}
}int main()
{bit::shared_ptr<list<int>> sp1(new list<int>);thread t1(func, sp1, 1000000);thread t2(func, sp1, 2000000);t1.join();t2.join();cout << sp1->size() << endl;return 0;
}
有时候程序会崩溃:
此时需要给程序加锁:
定义一个全局锁mutex mtx;
mutex mtx;// 智能指针对象本身拷贝析构是线程安全的
// 底层引用计数加减是线程安全的
// 指向的资源访问不是线程安全的
void func(bit::shared_ptr<list<int>> sp, int n)
{for (int i = 0; i < n; i++){mtx.lock();sp->emplace_back(i);mtx.unlock();}
}int main()
{bit::shared_ptr<list<int>> sp1(new list<int>);thread t1(func, sp1, 1000000);thread t2(func, sp1, 2000000);t1.join();t2.join();cout << sp1->size() << endl;return 0;
}
但如果在func里面每次拷贝一下:
void func(bit::shared_ptr<list<int>> sp, int n)
{for (int i = 0; i < n; i++){bit::shared_ptr<list<int>> copy1(sp);bit::shared_ptr<list<int>> copy2(sp);bit::shared_ptr<list<int>> copy3(sp);mtx.lock();sp->emplace_back(i);mtx.unlock();}
}
时不时就会程序崩溃:
此时保护了emplace_back,两个线程不能同时往list里面去插入数据。但是func里面的拷贝不是线程安全,就说明拷贝构造不是线程安全。
为什么拷贝构造不是线程安全呢?
是因为引用计数不是线程安全。
当在list里面插入数据
测试结果显示,计数不同。
线程1有自己的copy1、copy2、copy3,线程2也有自己的copy1、copy2、copy3。
对比之下库里面的就没有问题:
3.5.3.2 线程安全下shared_ptr类实现
此时问题出现在:引用计数不是线程安全,t1和t2都去调用自己的copy1、copy2、copy3。两个线程都在不断的加加减减这个计数,这个计数就不是安全的
那么怎么修改呢?
把_pcount类型改为atomic<int>*
,其他地方就不用改:
#pragma once
#include<atomic>namespace bit
{template<class T>class shared_ptr{public:shared_ptr(T* ptr):_ptr(ptr), _pcount(new atomic<int>(1)){}// sp2(sp1)shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}// sp1 = sp3;shared_ptr<T>& operator=(const shared_ptr<T>& sp){//if (this != &sp)if (_ptr != sp._ptr){this->release();_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}void release(){if (--(*_pcount) == 0){// 最后一个管理的对象,释放资源delete _ptr;delete _pcount;}}~shared_ptr(){release();}int use_count(){return *_pcount;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;atomic<int>* _pcount;};
}
3.5.4 循环引用
3.5.4.1 什么是循环引用
实现链表,在两个节点p1和p2互相指向的时候会出现循环引用
struct Node
{std::shared_ptr<Node> _next;std::shared_ptr<Node> _prev;int _val;~Node(){cout << "~Node()" << endl;}
};int main()
{std::shared_ptr<Node> p1(new Node);std::shared_ptr<Node> p2(new Node);p1->_next = p2;p2->_prev = p1;return 0;
}
这时候就会出现内存泄漏,没有调用析构函数,那两个节点没有释放。
为什么没有释放呢?
在管理节点时候会有计数,如果没有p1和p2互相指向的时候,是正常的。
如果p1和p2互相指向的时候,此时计数是2:
释放时候,如果左边节点什么时候释放由右边节点_prev管理着;右边节点什么时候释放由左边节点_next管理着,释放逻辑就是一个死循环。
循环引用分析:
- node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
- node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
- node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
- 也就是说_next析构了,node2就释放了。
- 也就是说_prev析构了,node1就释放了。
- 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。
3.5.4.2 weak_ptr
官方解决循环引用的方法提供了weak_ptr
weak_ptr不支持RAII,不单独管理资源,它专门用来辅助解决shared_ptr的循环引用
它的构造支持无参构造,
将上面代码std::shared_ptr改成std::weak_ptr
struct Node
{/*std::shared_ptr<Node> _next;std::shared_ptr<Node> _prev;*/std::weak_ptr<Node> _next;std::weak_ptr<Node> _prev;int _val;~Node(){cout << "~Node()" << endl;}
};int main()
{std::shared_ptr<Node> p1(new Node);std::shared_ptr<Node> p2(new Node);cout << p1.use_count() << endl;cout << p2.use_count() << endl;p1->_next = p2;p2->_prev = p1; return 0;
}
此时测试的代码就没有问题,关键就是weak_ptr中引用计数没有增加,引用计数还是1,不直接参与资源的释放
weak_ptr本质赋值或者拷贝时,只指向资源,但是不增加shared_ptr的引用计数。
它也能访问到引用计数:
3.5.5 定制删除器(了解)
如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题(ps:删除器这个问题我们了解一下)
定制删除器
如果传了删除器,就用删除器去删,没有就用delete去删除
设计一个仿函数
template<class T>
struct FreeFunc {void operator()(T* ptr){cout << "free:" << ptr << endl;free(ptr);}
};
代码测试:
class A
{
public:A(int a1 = 0, int a2 = 0):_a1(a1), _a2(a2){}~A(){cout << "~A()" << endl;}private:int _a1 = 1;int _a2 = 1;
};template<class T>
struct FreeFunc {void operator()(T* ptr){cout << "free:" << ptr << endl;free(ptr);}
};//定制删除器
int main()
{//std::shared_ptr<A[]> sp1(new A[10]);bit::shared_ptr<A> sp1(new A[10], [](A* ptr) {delete[] ptr; });bit::shared_ptr<int> sp2((int*)malloc(4), FreeFunc<int>());bit::shared_ptr<FILE> sp3(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr); });bit::shared_ptr<A> sp4(new A);return 0;
}
传一个 FreeFunc 匿名对象,会回掉删除器,来进行对应的释放:
那么定制删除器如何在shared_ptr类中传导呢?
构造函数中传导之后,析构怎么办呢?此时构造函数中加了一个_del的成员变量,去接受传导过来的删除器对象,再用_del进行释放。
template<class D>shared_ptr(T* ptr, D del): _ptr(ptr), _pcount(new atomic<int>(1)), _del(del){}
加一个包装器functional,在模板里面指定可调用包装的可调用对象和返回值类型,同时为了解决单个对象不支持删除器,就在定义成员变量时候给个缺省值:
function<void(T*)> _del = [](T* ptr) {delete ptr;
3.5.6 定制删除器下实现shared_ptr的代码
shared_ptr.h文件中
#pragma once
#include<atomic>
#include<functional>namespace bit
{template<class T>class shared_ptr{public:shared_ptr(T* ptr):_ptr(ptr), _pcount(new atomic<int>(1)){}template<class D>shared_ptr(T* ptr, D del): _ptr(ptr), _pcount(new atomic<int>(1)), _del(del){}// sp2(sp1)shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}// sp1 = sp3;shared_ptr<T>& operator=(const shared_ptr<T>& sp){//if (this != &sp)if (_ptr != sp._ptr){this->release();_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}void release(){if (--(*_pcount) == 0){// 最后一个管理的对象,释放资源//delete _ptr;_del(_ptr);delete _pcount;}}~shared_ptr(){release();}int use_count(){return *_pcount;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;atomic<int>* _pcount;function<void(T*)> _del = [](T* ptr) {delete ptr; };};
}
4. C++11和boost中智能指针的关系
- C++ 98 中产生了第一个智能指针
auto_ptr
. - C++ boost给出了更实用的
scoped_ptr
和shared_ptr
和weak_ptr
. - C++ TR1,引入了
shared_ptr
等。不过注意的是TR1并不是标准版。 - C++ 11,引入了
unique_ptr
和shared_ptr
和weak_ptr
。需要注意的是unique_ptr
对应boost的scoped_ptr
。并且这些智能指针的实现原理是参考boost中的实现的。
有问题请指出,大家一起进步!!!
相关文章:
【C++】智能指针RALL实现shared_ptr
个人主页 : zxctscl 专栏 【C】、 【C语言】、 【Linux】、 【数据结构】、 【算法】 如有转载请先通知 文章目录 1. 为什么需要智能指针?2. 内存泄漏2.1 什么是内存泄漏,内存泄漏的危害2.2 内存泄漏分类(了解)2.3 如何…...
利用迁移学习实现食物分类:基于PyTorch与ResNet18的实战案例
利用迁移学习实现食物分类:基于PyTorch与ResNet18的实战案例 在深度学习领域,训练一个高性能的模型往往需要大量的数据和计算资源。然而,通过迁移学习,我们能够巧妙地利用在大规模数据集上预训练好的模型,将其知识迁移…...
列日-巴斯通-列日:与VELO Senso TT+见证精彩时刻
近日,第111届列日-巴斯通-列日自行车赛落下帷幕,波加查毫无悬念地再度单飞夺冠。这场赛事不仅是速度与耐力的较量,更是装备与技术的完美结合。 在2025年第111届列日-巴斯通-列日自行车赛中,波加查以绝对优势再度单飞夺冠&a…...
C++笔记之委托
C++笔记之委托 code review! 文章目录 C++笔记之委托一、什么是委托?二、委托的常见应用场景2.1 事件委托(Event Delegation)2.2 C# 的委托类型(Delegate)2.3 对象组合中的委托(Design Delegation Pattern)三、C++ 委托模式示例四、委托的优点五、委托与23种设计模式的…...
Windows11 VS code 安装 Cline 调用 Github MCP 配置过程坑点汇总
背景 为了调研 MCP 在 windows 上如何使用本地的命令执行一些操作而实现自动化的过程,在 B 站视频的指导下,进行相应填坑过程,最终运行起来,并实现 github 自动化编程并提交代码的过程。 B 站 Cline 视频演示 Cline Cline 是一…...
SpringCloud多环境配置的一些问题
一、配置优先级(高到低) 命令行参数bootstrap.yaml/propertiesnacos配置config/applicaion.properties > config/applicaion.yml > config/applicaion.yamlapplicaion.properties > applicaion.yml > applicaion.yaml 有环境配置的会覆盖基础配置5的重复项&#…...
多语言笔记系列:Polyglot Notebooks 中运行 BenchmarkDotnet 基准测试
运行 BenchmarkDotnet 基准测试 在多语言笔记中,可以很方便的使用 BenchmarkDotnet 进行基准测试。 使用步骤 1. 安装 BenchmarkDotNet 包 // 默认包源 #i "nuget:https://api.nuget.org/v3/index.json"#r "nuget: BenchmarkDotNet, 0.13.12&quo…...
Model Context Protocol (MCP)笔记
目录 摘要MCP理论MCP的作用MCP 传输机制 Stdio 与 SSESTDIOSSE 传输部署模式 模型是如何确定工具的选用的?Fc x MCP x LangChain MCP快速开始编写客户端基于golang的mcp 摘要 Model Context Protocol(MCP,模型上下文协议)是由 An…...
【codeforces 2070c】二分答案详解
【codeforces 2070c】二分答案详解 二分答案转化成判定 对于任何问题,如果我们有了一个判定算法,那把解空间枚举并判定一遍,当然就可以得到解了。而当解空间具有单调性时,我们就可以使用二分法代替枚举。 考虑如下问题…...
启发式算法-禁忌搜索算法
禁忌搜索是一种可以用于解决组合优化问题的启发式算法,通过引入记忆机制跳出局部最优,避免重复搜索。该算法从一个初始解开始,通过邻域搜索策略来寻找当前解的邻域解,并在邻域解中选择一个最优解作为下一次迭代的当前解࿰…...
simulink 外循环与内循环执行流程
目录 前言 一、外循环 模型 执行流程 二、内循环 模型 执行流程 仓库 前言 某些需求需要使用到simulink外循环和内循环,本篇通过对其执行顺序进行记录,以便后续查阅。 一、外循环 模型 下面是我搭建的简单模型 执行流程 0-step:执行en step…...
Gradio全解20——Streaming:流式传输的多媒体应用(6)——构建视频流目标检测系统
Gradio全解20——Streaming:流式传输的多媒体应用(6)——构建视频流目标检测系统 本篇摘要20. Streaming:流式传输的多媒体应用20.6 RT-DETR模型构建视频流目标检测系统20.6.1 RT-DETR模型1. 模型介绍2. 使用示例 20.6.2 系统配置…...
比较两种判断相同二叉树的方法:递归与遍历序列对比
在二叉树操作中,判断两棵树是否相同是一个常见的问题。本文将对比两种不同的解决方案:递归法和遍历序列对比法,分析它们的优缺点,并探讨为何递归法是更优的选择。 问题描述 给定两棵二叉树的根节点 p 和 q,判断它们是…...
Java IO流核心处理方式详解
一、IO流概述 Java IO(Input/Output)流是处理输入输出操作的核心机制,通过流(Stream)的形式实现设备间的数据传输。所有操作都基于以下两个核心抽象: InputStream/OutputStream:字节流基类 Re…...
C++竞赛指南
关注支持,好运连连 目录 关注支持,好运连连 一、竞赛C核心优势 二、必备语法与STL组件 1. 输入输出优化 2. 常用STL容器 3. 算法函数 三、竞赛常用算法 1. 时间复杂度分析 2. 高频算法模板 二分查找 快速幂(模运算) …...
Python字符串全面指南:从基础到高级操作
字符串是Python编程中最基础也是最重要的数据类型之一。本文将全面介绍Python字符串的相关知识,从基础概念到高级操作,帮助您彻底掌握字符串的使用。 1. 字符串基础 1.1 字符串的概念 字符串是由一系列字符组成的不可变序列容器,存储的是字…...
【推荐】智慧矿山矿业信息化智能化资料汇总-共25份
智慧矿山矿业信息化智能化资料汇总 25 份: 有色金属矿山智能化采选生产线智能矿山建设与示范智能矿山建设实践与思考智慧矿山建设解决方案与实现途径以信息化、智能化为手段打造生态型、效益型国际一流示范矿山新型智能 X 荧光多通道高精度在线品位分析仪的研制与应…...
Oracle OCP认证考试考点详解083系列08
题记: 本系列主要讲解Oracle OCP认证考试考点(题目),适用于19C/21C,跟着学OCP考试必过。 36. 第36题: 题目 解析及答案: 关于数据库闪回(FLASHBACK DATABASE)功能,以下…...
备战蓝桥杯国赛第一天-atcoder-beginner-contest404
B. 因为只有四种情况,旋转90/180/270度后替换,直接替换,暴力即可 C. 循环图的定义是每个点出度为2,而且只有一个环的,所以先判断出度,再判断是否成环 #include <bits/stdc.h> using namespace st…...
Python异步编程进阶:深入探索asyncio高级特性
异步上下文管理器 (async with) 异步上下文管理器允许你在异步环境中管理资源,比如数据库连接或文件操作。 基本实现 class AsyncDatabaseConnection:async def __aenter__(self):print("建立数据库连接")await asyncio.sleep(0.5) # 模拟连接建立return selfas…...
【Java ee初阶】多线程(7)
一、线程池 线程池的一些参数: corePoolSize:核心线程数量 maximumPoolSize:核心线程数量临时线程数量 上述是“java 的线程池策略”(其他语言,其他库的线程池可能不同) keepAliveTime :临时线程的存活时间.临时线程…...
【PostgreSQL数据分析实战:从数据清洗到可视化全流程】6.2 预测分析基础(线性回归/逻辑回归实现)
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 PostgreSQL数据分析实战:预测分析基础(线性回归/逻辑回归实现)6.2 预测分析基础——线性回归与逻辑回归实现6.2.1 预测分析核心理论框架1…...
【NLP】29. 高效训练与替代模型:让语言模型更轻、更快、更强
高效训练与替代模型:让语言模型更轻、更快、更强 本文介绍语言模型如何通过结构优化与新模型探索,提升训练和推理的效率,适应资源受限环境,同时概述了一些 Transformer 替代模型的最新进展。 一、如何让语言模型更高效?…...
【LaTeX+VSCode本地Win11编译教程】
LaTeXVSCode本地编译教程参考视频: LaTeXVSCode本地编译教程 下面提供一种Win11的Latex环境配置和设置方案,首先vscode安装参考博客:【VscodeGit教程】,然后准备安装Latex相关组件 在 https://miktex.org/download 下载 miktex 并…...
组合两个表 --- MySQL [Leetcode 题目详解]
目录 题目链接 往期相关基础内容讲解博客 题目详解 1. 题目内容 2. 解题思路 3. 代码编写 题目链接 // 175. 组合两个表 往期相关基础内容讲解博客 // 聚合查询和联合查询博客 题目详解 1. 题目内容 // 编写解决方案,报告 Person 表中每个人的姓、名、城市…...
STM32 PulseSensor心跳传感器驱动代码
STM32CubeMX中准备工作: 1、设置AD 通道 2、设置一个定时器中断,间隔时间2ms,我这里采用的是定时器7 3、代码优化01 PulseSensor.c文件 #include "main.h" #include "PulseSensor/PulseSensor.h"/******************…...
macOS 上是否有类似 WinRAR 的压缩软件?
对于习惯使用 Windows 的用户来说,WinRAR 是经典的压缩/解压工具,但 macOS 系统原生并不支持 RAR 格式的解压,更无法直接使用 WinRAR。不过,macOS 平台上有许多功能相似甚至更强大的替代工具,以下是一些推荐࿱…...
Java求职面试:Spring Boot与微服务的幽默探讨
Java求职者面试:技术与幽默的碰撞 场景概述 在某互联网大厂的面试现场,面试官严肃认真,程序员则是一个搞笑的水货角色。面试者名叫张伟,年龄28岁,硕士学历,拥有5年的Java开发经验。以下是面试的详细过程。…...
《数字图像处理(面向新工科的电工电子信息基础课程系列教材)》封面颜色空间一图的选图历程
禹晶、肖创柏、廖庆敏《数字图像处理(面向新工科的电工电子信息基础课程系列教材)》 学图像处理的都知道,彩色图像的颜色空间很多,而且又是三维,不同的角度有不同的视觉效果,MATLAB的图又有有box和没有box。…...
Docker 使用下 (二)
Docker 使用下 (二) 文章目录 Docker 使用下 (二)前言一、初识Docker1.1 、Docker概述1.2 、Docker的历史1.3 、Docker解决了什么问题1.4 、Docker 的优点1.5 、Docker的架构图 二、镜像三、容器四、数据卷4.1、数据卷的概念4.2 、…...
【群晖NAS】Docker + WebStation + DDNS 部署无端口号HTTPs WordPress
前言 群晖提供官方的DDNS服务,可以直接配置一个类似于xxxx.synology.me的DDNS解析IPv4/IPv6到自己的NAS;群晖还有Web Station应用可以配置Docker的端口号映射,但是他自己占用了80端口,如果给自己的应用手动指定其他端口号&#x…...
手机SIM卡打电话时识别对方按下的DTMF按键(二)
手机SIM卡打电话时识别对方按下的DTMF按键(二) --本地AI电话机器人 前言 书接上篇,在上一篇章《手机打电话时如何识别对方按下的DTMF按键的字符》中,我们从理论的角度来论述了DTMF的频率组成。并尝试使用400Kb左右的【TarsosDS…...
N-Gram 模型
N-Gram 模型 什么是N-Gram?为什么叫 N-Gram?N-Gram怎么知道下一个词可能是什么?N-Gram 能做什么?N-Gram的问题 本文回答了四个问题: 一、N-Gram是什么?二、N-Gram为什么叫N-Gram?三、N-Gram具体…...
【漫话机器学习系列】240.真正类率(True Positive Rate,TPR)
理解真正类率(True Positive Rate,TPR):公式、意义与应用 在机器学习与深度学习模型评估中,"真正类率"(True Positive Rate,简称TPR)是一个非常重要的指标。TPR反映了分类…...
ThreadLocal源码深度剖析:内存管理与哈希机制
ThreadLocal是Java并发编程中的重要工具,它为每个线程提供独立的变量存储空间,实现了线程之间的数据隔离。本文将从源码实现角度,深入分析ThreadLocal的内部机制,特别是强弱引用关系、内存泄漏问题、ThreadLocalMap的扩容机制以及…...
Softmax回归与单层感知机对比
(1) 输出形式 Softmax回归 输出是一个概率分布,通过Softmax函数将线性得分转换为概率: 其中 KK 是类别数,模型同时计算所有类别的概率。 单层感知机 输出是二分类的硬决策(如0/1或1): 无概率解释&#x…...
数字社会学家唐兴通谈数字行动主义网络行动主义与标签行动主义,理解它才算抓住AI社会学与网络社会学关键所在
让我们继续探讨一个在数字时代至关重要的概念——数字行动主义(Digital Activism)、网络行动主义(Cyberactivism)以及标签行动主义(Hashtag Activism)。我将尽力从一个数字社会学家的角度,抽丝剥…...
PandasAI:对话式数据分析新时代
PandasAI:对话式数据分析新时代 引言项目概述分析基本信息 核心功能详解1. 自然语言查询处理2. 数据可视化生成3. 多数据源集成分析4. 安全沙箱执行5. 云平台协作功能 安装和使用教程1.环境要求2.安装步骤3.基本使用方法4.切换其他LLM 应用场景和实际价值1.适用业务…...
全球化电商平台AWS云架构设计
业务需求: 支撑全球三大区域(北美/欧洲/亚洲)用户访问,延迟<100ms处理每秒50,000订单的峰值流量混合云架构整合本地ERP系统全年可用性99.99%满足GDPR和PCI DSS合规要求 以下是一个体现AWS专家能力的全球化电商平台架构设计方…...
Linux 怎么使用局域网内电脑的网络访问外部
一次性 export http_proxy"http://192.168.0.188:7890" export https_proxy"http://192.168.0.188:7890"一直生效 写入 ~/.bashrc(或 ~/.bash_profile) nano ~/.bashrc加入这一行: export http_proxy"http://19…...
Python-numpy中ndarray对象创建,数据类型,基本属性
numpy库 numpy中的数据结构ndarrayndarray中的dtypendarray中的dtype的指定方式创建ndarray及指定dtype从列表创建ndarray使用 np.empty(), np.zeros(), np.ones() 和 np.full() 创建特定值的数组使用 np.arange() 创建等差数列数组使用 np.linspace() 创建等差数组使用np.logs…...
Python从入门到高手8.2节-元组的常用操作符
目录 8.2.1 元组的常用操作符 8.2.2 []操作符: 索引访问元组 8.2.3 [:]操作符:元组的切片 8.2.4 操作符:元组的加法 8.2.5 *操作符:元组的乘法 8.2.6 元组的关系运算 8.2.7 in操作符:查找元素 8.2.8 五一她玩了个狗吃…...
Python内置函数
Python作为一门简洁强大的编程语言,提供了丰富的内置函数(Built-in Functions),这些函数无需导入任何模块即可直接使用。本文将介绍Python中最常用、最重要的内置函数,帮助初学者快速掌握这些强大的工具。 官方地址&a…...
一款基于 .NET 开源的多功能的 B 站视频下载工具
前言 哔哩哔哩(B站)是一个知名的视频学习平台,作为程序员而言这是一个非常值得推荐的网站。今天大姚给大家推荐一款基于 .NET 开源的多功能的 B 站视频下载工具:downkyi。 项目介绍 downkyi(哔哩下载姬)…...
【PostgreSQL数据分析实战:从数据清洗到可视化全流程】5.2 数据分组与透视(CUBE/ROLLUP/GROUPING SETS)
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 5.2 数据分组与透视:CUBE/ROLLUP/GROUPING SETS深度解析5.2.1 数据准备与分析目标数据集与表结构分析目标 5.2.2 ROLLUP:层级化分组汇总功能与语法示…...
20、数据可视化:魔镜报表——React 19 图表集成
一、魔镜的预言本质 "数据可视化是霍格沃茨的预言水晶球,将混沌的数据星尘转化为可解读的命运轨迹!" 魔法部占卜司官员挥舞魔杖,Echarts与Three.js的图表矩阵在空中交织成动态星图。 ——基于《国际魔法联合会》第9号可视化协议&a…...
笔记本电脑升级计划(2017———2025)
ThinkPad T470 (2017) vs ThinkBook 16 (2025) 完整性能对比报告 一、核心硬件性能对比 1. CPU性能对比(i5-7200U vs Ultra9-285H) 参数i5-7200U (2017)Ultra9-285H (2025)提升百分比核心架构2核4线程 (Skylake)16核16线程 (6P8E2LPE)700%核心数制程工…...
Flutter——数据库Drift开发详细教程(四)
目录 参考正文表达式1.比较2.布尔代数3.算术BigIn 4.空值检查6.日期和时间7.IN和NOT IN8.聚合函数(例如 count 和 sum)8.1比较8.2算术8.4计数8.5group_concat8.9窗口函数 9.数学函数和正则表达式10.子查询10.1 标量子查询10.2 isInQuery10.3 存在10.4完整…...
android-ndk开发(6): 查看反汇编
android-ndk开发(6): 查看反汇编 2025/05/05 1. 概要 android-ndk 是基于 clang 的工具链, clang 则保持了和 gcc 的高度兼容。 在 Linux 开发机上, GCC 套件里的 objdump 提供了反汇编的功能。 实际上 android-ndk 也提供了一份 objdump,…...
浅析AI大模型为何需要向量数据库?【入门基础】
文章目录 引言:大模型时代的存储挑战一、向量数据库:大模型的"海马体"1.1 什么是向量数据库?1.2 为什么大模型离不开向量数据库?(1) 嵌入(Embedding)的本质(2) 突破上下文窗口限制 二、相似性度量:欧氏距离与…...