从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr
目录
1. 智能指针的引入_内存泄漏
1.1 内存泄漏
1.2 如何避免内存泄漏
2. RAII思想
2.1 RAII解决异常安全问题
2.2 智能指针原理
3. auto_ptr
3.1 auto_ptr模拟代码
4. unique_ptr
4.1 unique_ptr模拟代码
5.2 循环引用
6. weak_ptr
6.1 weak_ptr模拟代码
7. 定制删除器(了解)
8. 完整代码
9. 笔试面试题
9.1 智能指针的发展历史
9.2 笔试选择题:
9.3 选择题答案及解析
本篇完。
1. 智能指针的引入_内存泄漏
为什么需要智能指针?上一篇:
1.1 内存泄漏
上面是异常安全导致的内存泄漏问题,开空间没有释放也可能导致内存泄漏。
什么是内存泄漏?:
内存泄漏指因为疏忽或错误(逻辑错误)造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制(指针丢了),因而造成了内存的浪费。内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
void MemoryLeaks()
{int* p1 = (int*)malloc(sizeof(int)); // 1.内存申请了忘记释放int* p2 = new int;int* p3 = new int[10]; // 2.异常安全问题Func(); // 这里如果Func函数抛异常n,会导致 delete[] p3未执行,p3没被释放.delete[] p3;
}
内存泄漏分类(了解):
C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap leak):
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一
块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分
内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏:
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放
掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
1.2 如何避免内存泄漏
1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
2. 采用RAII思想或者智能指针来管理资源。
3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结 :内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
2. RAII思想
- RAII:是英文Resource Acquisition Is Initialization(资源请求即初始化)的首字母,是一种利用对象生命周期来控制程序资源的简单技术。
- 这些资源可以是内存,文件句柄,网络连接,互斥量等等。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
① 不需要显式地释放资源。
② 采用这种方式,对象所需的资源在其生命期内始终保持有效
2.1 RAII解决异常安全问题
利用RAII思想设计delete资源的类:
#include <iostream>
using namespace std;
double Division(int a, int b)
{if (b == 0){throw "Divide by Zero Error";}else{return ((double)a / (double)b);}
}
// 利用RAII思想设计delete资源的类
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout << "delete: " << _ptr << endl;delete _ptr;}
protected:T* _ptr;
};void Func()
{//1、如果p1这里new 抛异常会如何?//2、如果p2这里new 抛异常会如何?//3、如果div调用这里又会抛异常会如何?//int* p1 = new int;//int* p2 = new int;//cout << Division() << endl;//delete p1;//delete p2;//cout << "释放资源" << endl; SmartPtr<int> sp1(new int);SmartPtr<int> sp2(new int);cout << Division(3, 0) << endl;
}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}catch (...){cout << "unknown exception" << endl;}cout << "return 0;" << endl;return 0;
}
运行:
把 Division(3, 0) 改为 Division(3, 1):
2.2 智能指针原理
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。
// 1、利用RAII思想设计delete资源的类
// 2、重载operator*和opertaor->,具有像指针一样的行为。
// 3、浅拷贝问题(析构两次,下面讲
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout << "delete: " << _ptr << endl;delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
protected:T* _ptr;
};
所谓RAII,就是将资源的生命周期和对象的生命周期绑定。从构造函数开始,到析构函数结束。智能指针就是使用了RAII技术,并且利用对象生命周期结束时,编译器会自动调用对象的析构函数来释放资源。智能指针的智能就在于资源会被自动释放,不需要显式地释放资源。采用智能指针,对象所需的资源在其生命周期内始终保持有效。
总结智能指针的原理:
1、利用RAII思想设计delete资源的类
2、重载operator*和opertaor->,具有像指针一样的行为。
3、拷贝问题(不同的智能指针的解决方式不一样)
3. auto_ptr
C++98就已经提供了这样的一个智能指针:(注意到上面写着deprecated不推荐使用了)
让上面写的SmartPtr使用编译器自动生成的拷贝构造函数:
#include <iostream>
using namespace std;
// 1、利用RAII思想设计delete资源的类
// 2、重载operator*和opertaor->,具有像指针一样的行为。
// 3、浅拷贝问题
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout << "delete: " << _ptr << endl;delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
protected:T* _ptr;
};int main()
{SmartPtr<int> sp1(new int);SmartPtr<int> sp2(sp1);return 0;
}
上面代码在运行时报错。
智能指针ap2拷贝复制了ap1,此时ap1和ap2都指向同一块动态内存空间。
当程序执行结束以后,ap1对象和ap2对象都会销毁,并且会执行各自的析构函数,所以那份动态空间就会被释放两次,所以报错了。怎么解决?:
显式定义一个拷贝构造函数,不能让两个智能指针指向同一份动态内存空间。(但是这样没有很好的解决问题,auto_ptr就是这样设计的)
//auto_ptr
#include <iostream>
using namespace std;
// 1、利用RAII思想设计delete资源的类
// 2、重载operator*和opertaor->,具有像指针一样的行为。
// 3、浅拷贝问题
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout << "delete: " << _ptr << endl;delete _ptr;}SmartPtr(SmartPtr<T>& ptr):_ptr(ptr._ptr){ptr._ptr = nullptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
protected:T* _ptr;
};int main()
{SmartPtr<int> sp1(new int);SmartPtr<int> sp2(sp1);return 0;
}
增加一个名字叫A的类重复上面操作:
class A
{
public:~A(){cout << "~A()" << endl;}
protected:int _a1 = 0;int _a2 = 0;
};int main()
{SmartPtr<A> sp1(new A);SmartPtr<A> sp2(sp1);return 0;
}
使用一下库里的auto_ptr试一下:
class A
{
public:~A(){cout << "~A()" << endl;}
protected:int _a1 = 0;int _a2 = 0;
};int main()
{//SmartPtr<A> sp1(new A);//SmartPtr<A> sp2(sp1);auto_ptr<A> sp1(new A);auto_ptr<A> sp2(sp1);return 0;
}
和显式定义一个拷贝构造函数的效果一样。
auto_ptr到这种情况就崩了:
class A
{
public:~A(){cout << "~A()" << endl;}
//protected:int _a1 = 0;int _a2 = 0;
};int main()
{//SmartPtr<A> sp1(new A);//SmartPtr<A> sp2(sp1);auto_ptr<A> sp1(new A);auto_ptr<A> sp2(sp1);sp1->_a1++;sp1->_a2++;return 0;
}
3.1 auto_ptr模拟代码
(上面SmartPtr再加一个赋值重载改下名字就差不多是auto_ptr的模拟了,再用命名空间封一下)
赋值重载细节还挺多的,前面学的赋值重载都类似拷贝构造,可以不看先写写,这里直接放代码:
#include <iostream>
#include <memory>
using namespace std;
//1、RAII
//2、像指针一样
//3、解决拷贝问题(不同的智能指针的解决方式不一样)namespace rtx
{template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}~auto_ptr(){cout << "~auto_ptr -> delete: " << _ptr << endl;delete _ptr;}auto_ptr(auto_ptr<T>& ptr):_ptr(ptr._ptr){ptr._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& ap){if (this != &ap) // 防止自己赋值给自己{if (_ptr) // 防止释放空,delete空也行{cout << "operator= -> Delete:" << _ptr << endl;delete _ptr;}_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}protected:T* _ptr;};
}class A
{
public:~A(){cout << "~A()" << endl;}
//protected:int _a1 = 0;int _a2 = 0;
};int main()
{//SmartPtr<A> sp1(new A);//SmartPtr<A> sp2(sp1);rtx::auto_ptr<A> sp1(new A);rtx::auto_ptr<A> sp2(sp1);rtx::auto_ptr<A> sp3 = sp2;return 0;
}
可以把命名空间切换到std比较一下,auto_ptr使用的是管理权转移的办法,会导致被拷贝对象悬空,是不负责的拷贝,对于不清楚auto_ptr这个特点的人来说,拷贝后再次使用ap1就会出问题。
auto_ptr是C++98一个失败的设计,被挂在了耻辱柱上,很多公司明确要求不能使用auto_ptr。
C++98到C++11期间人们被迫用C++更新探索的库:boost库里的一些智能指针,到了C++11,
终于更新了三个智能指针:unique_prt,shared_ptr,wead_ptr,相当于抄boost库的作业了。
下面我们介绍以及模拟实现这几个智能指针,当然,还有很多接口在模拟代码里没有实现。
4. unique_ptr
在C++11中更加靠谱的unique_ptr智能指针:
- unique_ptr直接禁止使用拷贝构造函数,即使编译器也不能生成默认的拷贝构造函数,因为使用了delete关键字。
unique_ptr采用的策略就是,既然拷贝有问题,那么就直接禁止拷贝,这确实解决了悬空等问题,使得unique_ptr是一个独一无二的智能指针。
(写到这发现忘记创建新项目了,这里创建一个Test.cpp和SmartPtr.hpp(.h+.cpp,直接.h也行,都可以把函数的实现在里面实现。声明和定义分离只是为了保护源码)
4.1 unique_ptr模拟代码
直接复制一份auto_ptr代码过来,用delete关键字禁言拷贝构造和赋值重载就行了:
template<class T>class unique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){cout << "~unique_ptr -> delete: " << _ptr << endl;delete _ptr;}unique_ptr(unique_ptr<T>& ptr) = delete;unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;T& operator*(){return *_ptr;}T* operator->(){return _ptr;}protected:T* _ptr;};
关于delete关键字(在5.2):从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值_GR_C的博客-CSDN博客
5. shared_ptr
unique_ptr禁掉了拷贝,但是如果就想拷贝智能指针呢?这就要用到shared_ptr了:
shared_ptr采用了引用计数的方法来解决拷贝问题:
(引用计数直接在成员变量加一个int Count可以吗?每一个对象都有一个自己的Count显然是不对的,我们应该让拷贝和被拷贝对象管理同一个Count。那么使用静态成员变量可以吗?这也不可以,因为这样所有的对象都管理的是同一个Count了,包括没有拷贝的对象)
shared_ptr原理:
通过引用计数的方式来实现多个shared_ptr对象之间共享资源。例如:
老师晚上在下班之前都会通知,让最后走的学生记得把门锁下。
① shared_ptr内部,给每个资源都维护了一份计数,用来记录该份资源被几个对象共享。
② 在对象被销毁时(也就是析构函数调用),说明自己不使用该资源了,对象引用计数减一。
③ 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源。
④ 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
shared_ptr增加了一个成员类似int* _pCount解决这个问题:
这样构造,拷贝构造和析构函数就是这样的:
构造先给 _pCount指向1,析构无论什么时候都减减,如果减减0就释放资源,拷贝构造就是把指针也给它,然后指针指向的内容加加。
OK,请你到这写一个赋值重载出来,手写或者敲都行(坏笑.jpg),这里直接放代码了:
5.1 shared_ptr模拟代码
template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr): _ptr(ptr), _pCount(new int(1)){}void Release(){if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数{delete _ptr;delete _pCount;}}~shared_ptr(){Release();}shared_ptr(const shared_ptr<T>& sp): _ptr(sp._ptr), _pCount(sp._pCount){(*_pCount)++;}shared_ptr<T>& operator=(const shared_ptr<T>& sp){//if (this != &sp)if (_ptr != sp._ptr) // 防止自己给自己赋值,注意不能比较this,类似s1 = s2; 再来一次s1 = s2;{ // 比较_pCount也行//if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数//{// delete _ptr;// delete _pCount;//}Release();_ptr = sp._ptr;_pCount = sp._pCount;(*_pCount)++;}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}protected:T* _ptr;int* _pCount;// 引用计数,有多线程安全问题,学了linux再讲,不能用静态成员};
赋值重载需要注意细节的都在注释写了,可以自己画一个图看看。
5.2 循环引用
shared_ptr 完美了吗?并不是,它有一个死穴:循环引用。
创建一个链表节点,在该节点的析构函数中打印提示信息:
struct Node
{~Node(){cout << "~Node" << endl;}int _val;std::shared_ptr<Node> _next;std::shared_ptr<Node> _prev;
};
将n1和n2互相指向,形成循环引用:
(因为要给_next和_prev赋值,所以Node里也要用智能指针)
int main()
{std::shared_ptr<Node> n1(new Node);std::shared_ptr<Node> n2(new Node);n1->_next = n2;n2->_prev = n1;return 0;
}
执行该程序后,节点析构函数中的打印信息并没有打印,说明析构出了问题。
如果不形成循环引用就会打印提示信息:
可以调用shared_ptr里的use_count接口打印引用计数值:
n1和n2刚创建的时候,它两的引用计数值都是1。当两个节点循环引用后,它们的引用计数值都变成了2。
n2先析构,右边的引用计数变为1,n1再析构,左边的引用计数变为1,然后就没了。
左边结点的_next什么时候释放?-> 取决于左边的结点什么时候delete。
左边的结点什么时候delete?-> 取决于右边结点的_prev。
右边结点的_prev什么时候释放?-> 取决于右边的结点什么时候delete。
右边的结点什么时候delete?-> 取决于左边结点的_next。
左边结点的_next什么时候释放? -> 回到一开始的问题,进入死循环。
在循环引用中,节点得不到真正的释放,就会造成内存泄漏。
循环引用的根本原因在于,next和prev也参与了资源的管理。
这个漏洞shared_ptr本身也解决不了,所以就增加了weak_ptr来解决这个问题。
解决办法就是让节点中的_next和_prev仅指向对方,
而不参与资源管理,也就是计数值不增加。
这里为了配合上面和给下面模拟weak_ptr演示给我们的shared_ptr加两个接口函数:
6. weak_ptr
weak_ptr是为解决循环引用问题而产生的,可以把weak_ptr当作shared_ptr的小跟班,weak_ptr主要用shared_ptr来构造,所以weak_ptr的拷贝构造以及赋值都不会让引用计数值加1,仅仅是指向资源。
把链表类里的指针换成weak_ptr解决循环引用问题:
6.1 weak_ptr模拟代码
weak_ptr中只有一个成员变量_ptr,用来指向动态内存空间,在默认构造函数中,仅仅指向动态内存空间。拷贝构造函数和赋值运算符重载函数中,拷贝和赋值的对象都是shared_ptr指针。
weak_ptr就是用来解决循环引用问题的,所以拷贝和赋值的智能指针必须是shared_ptr。
weak_ptr和shared_ptr并不是同一个类,所以获取shared_ptr中的_ptr时,不能直接访问,需要通过shared_ptr的接口get()来获取。
template<class T> // 辅助型智能指针,配合解决shared_ptr循环引用问题class weak_ptr // 没有RAII,不管理资源{public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr(const weak_ptr<T>& wp):_ptr(wp._ptr){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}protected:T* _ptr;};
换下命名空间:
效果一样。
7. 定制删除器(了解)
前面我们自己实现的所有智能指针中,在释放动态内存资源的时候,都只用了delete,也就是所有new出来的资源都是单个的:
如果是new int[10],或者malloc(20)呢?当需要释放的资源是其他类型的呢?delete肯定就不能满足了,
对于不同类型的资源,需要定制删除器。
先来看库中是如何实现的,这里仅拿shared_ptr为例,unique_ptr也是一样的。
在构造智能指针的时候,可以传入定制的删除器。
可以采用仿函数的方式,lambda的方式,以及函数指针的方式,只要是可调用对象都可以。
此时的智能指针指向的是动态数组,我们传入的定制删除器也是释放数组的
#include "SmartPtr.hpp"
#include <memory>class A
{
public:~A(){cout << "~A()" << endl;}//protected:int _a1 = 0;int _a2 = 0;
};struct Node
{~Node(){cout << "~Node" << endl;}int _val;rtx::weak_ptr<Node> _next;rtx::weak_ptr<Node> _prev;
};template<class T>
struct DeleteArray
{void operator()(T* ptr){cout << "delete[]" << ptr << endl;delete[] ptr;}
};template<class T>
struct Free
{void operator()(T* ptr){cout << "free" << ptr << endl;free(ptr);}
};int main()
{// 仿函数对象std::shared_ptr<Node> n1(new Node);std::shared_ptr<Node> n2(new Node[5], DeleteArray<Node>());std::shared_ptr<int> n3(new int[7], DeleteArray<int>());std::shared_ptr<int> n4((int*)malloc(sizeof(12)), Free<int>());// lambdastd::shared_ptr<Node> n5(new Node);std::shared_ptr<Node> n6(new Node[5], [](Node* ptr) {delete[] ptr; });std::shared_ptr<int> n7(new int[7], [](int* ptr) {delete[] ptr; });std::shared_ptr<int> n8((int*)malloc(sizeof(12)), [](int* ptr) {free(ptr); });//std::shared_ptr<FILE> n9(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr); }); //不安全了,但这样也行std::unique_ptr<Node, DeleteArray<Node>> up(new Node[5]); // unique_ptr只能这样传return 0;
}
下面我们类似库里unique_ptr的方法给我们的shared_ptr弄个类似的定制删除器:
给我们的shared_ptr配一个默认删除方式的仿函数,执行的是delete ptr。
在shared_ptr类模板的模板参数中增加一个定制删除器的模板参数,缺省值默认删除方式。
在释放资源的时候,在Release()中调用定制的删除器仿函数对象。
template<class T>struct Delete{void operator()(T* ptr){cout << "delete:" << ptr << endl;delete ptr;}};template<class T, class D = Delete<T>>class shared_ptr{public:shared_ptr(T* ptr = nullptr): _ptr(ptr), _pCount(new int(1)){}void Release(){if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数{//delete _ptr;delete _pCount;//D del;//del(_ptr);D()(_ptr); // 对_ptr直接用匿名对象删掉}}~shared_ptr(){Release();}
...........................略
(下面放了这个程序运行的完整代码)
标准库中的shared_ptr,在使用定制删除器的时候,是在构造对象时传入函数对象来实现的。我们自己实现的shared_ptr,是在实例化时,传入仿函数类型实现的。
这是因为,C++11标准库实现的方式和我们不一样,它的更加复杂,专门封装了几个类管理引用计数以及定制删除器等内容。
8. 完整代码
SmartPtr.hpp:
#include <iostream>
using namespace std;
//1、RAII
//2、像指针一样
//3、解决拷贝问题(不同的智能指针的解决方式不一样)namespace rtx
{template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}~auto_ptr(){cout << "~auto_ptr -> delete: " << _ptr << endl;delete _ptr;}auto_ptr(auto_ptr<T>& ptr):_ptr(ptr._ptr){ptr._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& ap){if (this != &ap) // 防止自己赋值给自己{if (_ptr) // 防止释放空,delete空也行{cout << "operator= -> Delete:" << _ptr << endl;delete _ptr;}_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}protected:T* _ptr;};template<class T>class unique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){cout << "~unique_ptr -> delete: " << _ptr << endl;delete _ptr;}unique_ptr(unique_ptr<T>& ptr) = delete;unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;T& operator*(){return *_ptr;}T* operator->(){return _ptr;}protected:T* _ptr;};template<class T>struct Delete{void operator()(T* ptr){cout << "delete:" << ptr << endl;delete ptr;}};template<class T, class D = Delete<T>>class shared_ptr{public:shared_ptr(T* ptr = nullptr): _ptr(ptr), _pCount(new int(1)){}void Release(){if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数{//delete _ptr;delete _pCount;//D del;//del(_ptr);D()(_ptr); // 对_ptr直接用匿名对象删掉}}~shared_ptr(){Release();}shared_ptr(const shared_ptr<T>& sp): _ptr(sp._ptr), _pCount(sp._pCount){(*_pCount)++;}shared_ptr<T>& operator=(const shared_ptr<T>& sp){//if (this != &sp)if (_ptr != sp._ptr) // 防止自己给自己赋值,注意不能比较this,类似s1 = s2; 再来一次s1 = s2;{ // 比较_pCount也行//if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数//{// delete _ptr;// delete _pCount;//}Release();_ptr = sp._ptr;_pCount = sp._pCount;(*_pCount)++;}return *this;}int use_count(){return *_pCount;}T* get() const{return _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}protected:T* _ptr;int* _pCount;// 引用计数,有多线程安全问题,学了linux再讲,不能用静态成员};template<class T> // 辅助型智能指针,配合解决shared_ptr循环引用问题class weak_ptr // 没有RAII,不管理资源{public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr(const weak_ptr<T>& wp):_ptr(wp._ptr){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}protected:T* _ptr;};
}
Test.cpp:
#include "SmartPtr.hpp"
#include <memory>class A
{
public:~A(){cout << "~A()" << endl;}//protected:int _a1 = 0;int _a2 = 0;
};struct Node
{~Node(){cout << "~Node" << endl;}int _val;rtx::weak_ptr<Node> _next;rtx::weak_ptr<Node> _prev;
};template<class T>
struct DeleteArray
{void operator()(T* ptr){cout << "delete[]" << ptr << endl;delete[] ptr;}
};template<class T>
struct Free
{void operator()(T* ptr){cout << "free" << ptr << endl;free(ptr);}
};int main()
{仿函数对象//std::shared_ptr<Node> n1(new Node);//std::shared_ptr<Node> n2(new Node[5], DeleteArray<Node>());//std::shared_ptr<int> n3(new int[7], DeleteArray<int>());//std::shared_ptr<int> n4((int*)malloc(sizeof(12)), Free<int>());lambda//std::shared_ptr<Node> n5(new Node);//std::shared_ptr<Node> n6(new Node[5], [](Node* ptr) {delete[] ptr; });//std::shared_ptr<int> n7(new int[7], [](int* ptr) {delete[] ptr; });//std::shared_ptr<int> n8((int*)malloc(sizeof(12)), [](int* ptr) {free(ptr); });std::shared_ptr<FILE> n9(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr); }); //不安全了,但这样也行//std::unique_ptr<Node, DeleteArray<Node>> up(new Node[5]); // unique_ptr只能类似这样传rtx::shared_ptr<Node> n1(new Node);rtx::shared_ptr<Node, DeleteArray<Node>> n2(new Node[5]);rtx::shared_ptr<int, DeleteArray<int>> n3(new int[5]);rtx::shared_ptr<int, Free<int>> n4((int*)malloc(sizeof(12)));return 0;
}
9. 笔试面试题
面试很大几率会让手撕一个指针指针,如果没有要求的话可以写一个uniqeu_ptr,别写auto_ptr就行,有要求的话就应该就是shared_ptr了,所以智能指针的模拟实现应该闭着眼都能手撕出来。
笔试面试常问问题:
上面的问题博客上面都讲了,总结下智能指针的发展历史:
9.1 智能指针的发展历史
C++98中的auto_ptr,存在非常大的缺陷,在拷贝构造或者赋值的时候,原本的auto_ptr会被置空,所以这个智能指针存在非常大的缺陷,很多地方都禁止使用。
C++11中的unique_ptr,禁止了拷贝和赋值,直接避免了auto_ptr可能存在的缺陷,是一个独一无二的智能指针,但是它不能拷贝和赋值。
C++11又提供了shared_ptr,通过引用计数的方式解决了不能拷贝和赋值的缺陷,并且通过互斥锁保证了shared_ptr本身的线程安全,但是它的死穴是循环引用。
C++11为了解决shared_ptr的循环引用问题,又提供了weak_ptr智能指针,通过仅指向不管理的方式解决了这个问题。
在使用的时候要根据具体情况选择合适的智能指针,切记最好不要使用auto_ptr。
C++委员会还发起了一个库,叫boost库,这个库可以理解为C++标准库的先行版,boost库中好用的东西会被C++标准库收录,标准库中的智能指针就是参照boost库中的智能指针再加以修改定义出来的。
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中的实现的。
9.2 笔试选择题:
1. 以下那个误操作不属于资源泄漏()
A.打开的文件忘记关闭
B.malloc申请的空间未通过free释放
C.栈上的对象没有通过delete销毁
D.内存泄漏属于资源泄漏的一种,但资源泄漏不仅仅是内存泄漏
2. 下面那个说法可以表示资源泄漏()
A.从商店买东西
B.借钱不还
C.买房子交首付
D.办信用卡
3. 下面关于内存泄漏的说法正确的是()
A.如果对程序影响不是很大的情况下,泄漏一两个字节不是很重要
B.内存没有释放时,进程在销毁的时候会统一回收,不用担心
C.内存泄漏不一定会对系统马上造成影响,可以不着急进行处理
D.写代码时要有良好的编码规范,万一发生内存泄漏要及时处理
4. 关于RAII下面说法错误的是()
A.RAII的实现方式就是在构造函数中将资源初始化,在析构函数中将资源清理掉
B.RAII方式管理资源,可以有效避免资源泄漏问题
C.所有智能指针都借助RAII的思想管理资源
D.RAII方式管理锁,有些场景下可以有效避免死锁问题
5. 下面关于auto_ptr的说法错误的是()
A.auto_ptr智能指针是在C++98版本中已经存在的
B.auto_ptr的多个对象之间,不能共享资源
C.auto_ptr的实现原理是资源的转移
D.auto_ptr完全可以正常使用
6. 下面关于unique_ptr说法错误的是()
A.unique_ptr是C++11才正式提出的
B.unique_ptr可以管理一段连续空间
C.unique_ptr不能使用其拷贝构造函数
D.unique_ptr的对象之间不能相互赋值
7. 下面关于shared_ptr说法错误的是 ( )
A.shared_ptr是C++11才正式提出来的
B.shared_ptr对象之间可以共享资源
C.shared_ptr可以应用于任何场景
D.shared_ptr是借助引用计数的方式实现的
8. 下面关于weak_ptr的说法错误的是()
A.weak_ptr与shread_ptr的实现方式类似,都是通过引用计数的方式实现的
B.weak_ptr的对象可以独立管理资源
C.weak_ptr的唯一作用就是解决shared_ptr中存在的循环引用问题
D.weak_ptr一般情况下都用不到
9.3 选择题答案及解析
1. C
A:属于,打开的文件用完时一定要关闭
B:属于,堆上申请的空间,需要用户显式的释放
C:不属于,栈上的对象不需要释放,函数结束时编译器会自动释放
D:正确,资源泄漏包含的比较广泛,比如文件未关闭、套接字为关闭等
2. B
从系统中动态申请的资源,一定要记着及时归还,否则别人可能就使用不了,或者申请失败。就像借钱一样
3. D
A:错误,一两个字节不处理时可能会因小失大,很多个两字节,就是很大的一块内存空间
B:错误,虽然进程退出时会回收,但是进程为退出时可能会影响程序性能甚至会导致崩溃
C:错误,只要发现了就要及时处理,否则,说不定什么时候程序就会崩溃
D:正确,内存泄漏最好不要发生,万一发生了一定要及时处理
4. C
C:错误,weak_ptr不能单独管理资源,必须配合shared_ptr一块使用,解决shared_ptr中存在的 循环引用问题
5. D
A:正确
B:正确,因为auto_ptr采用资源管理权转移的方式实现的,比如:用ap1拷贝构造ap2时,ap1中 的资源会转移给 ap2,而ap1与资源断开联系
C:正确
D:错误,可以使用,但是不建议用,因为有缺陷,标准委员会建议:什么情况下都不要使用auto_ptr
6. B
A:正确
B:错误,C++11中提供的智能指针都只能管理单个对象的资源,没有提供管理一段空间资源的智能指针
C:正确,因为unique_ptr中已经将拷贝构造函数和赋值运算符重载delete了
D:正确,原因同C
7. C
C:错误,有些场景下shared_ptr可能会造成循环引用,必须与weak_ptr配合使用
8. B
A:正确,weak_ptr和shared_ptr都是通过引用计数实现,但是在底层还是有区别的
B:错误,weak_ptr不能单独管理资源,因为其给出的最主要的原因是配合shared_ptr解决其循环引用问题
C:正确,处理解决shared_ptr的循环引用问题外,别无它用
D:正确
本篇完。
shared_ptr还有线程安全问题没办法讲,需要学完Linux多线程后再讲。C++一些关于Linux的内容更新到Linux多线程的内容后后再放出来,应该一两篇就够了。
继承和多态是C++第一重要的话,智能指针应该算是C++第二重要的部分了,后几篇算是C++收尾了。下一篇:特殊类设计和C++的类型转化。
相关文章:

从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr
目录 1. 智能指针的引入_内存泄漏 1.1 内存泄漏 1.2 如何避免内存泄漏 2. RAII思想 2.1 RAII解决异常安全问题 2.2 智能指针原理 3. auto_ptr 3.1 auto_ptr模拟代码 4. unique_ptr 4.1 unique_ptr模拟代码 5. shared_ptr 5.1 shared_ptr模拟代码 5.2 循环引用 6.…...

C++信息学奥赛1187:统计字符数
#include <bits/stdc.h> using namespace std; int main() {string arr;cin >> arr; // 输入一个字符串int n, a, max; // 定义变量n, a, maxchar ArrMax; // 定义字符变量ArrMaxn arr.length(); // 获取字符串长度max a 0; // 初始化max和a为0// 外层循环&…...

计算机毕设 大数据商城人流数据分析与可视化 - python 大数据分析
文章目录 0 前言课题背景分析方法与过程初步分析:总体流程:1.数据探索分析2.数据预处理3.构建模型 总结 最后 0 前言 🔥 这两年开始毕业设计和毕业答辩的要求和难度不断提升,传统的毕设题目缺少创新和亮点,往往达不到…...

vscode上搭建go开发环境
前言 Go语言介绍: Go语言适合用于开发各种类型的应用程序,包括网络应用、分布式系统、云计算、大数据处理等。由于Go语言具有高效的并发处理能力和内置的网络库,它特别适合构建高并发、高性能的服务器端应用。以下是一些常见的Go语言应用开发…...

10.(Python数模)(预测模型二)LSTM回归网络(1→1)
LSTM回归网络(1→1) 长短期记忆网络 - 通常只称为“LSTM” - 是一种特殊的RNN,能够学习长期的规律。 它们是由Hochreiter&Schmidhuber(1997)首先提出的,并且在后来的工作中被许多人精炼和推广。…...

mac常见问题(五) Mac 无法开机
在mac的使用过程中难免会碰到这样或者那样的问题,本期为您带来Mac 无法开机怎么进行操作。 1、按下 Mac 上的电源按钮。每台 Mac 电脑都有一个电源按钮,通常标有电源符号 。然后检查有没有通电迹象,例如: 发声,例如由风…...

WebSocket与SSE区别
一,websocket WebSocket是HTML5下一种新的协议(websocket协议本质上是一个基于tcp的协议) 它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的 Websocket是一个持久化的协议 websocket的原理 …...

Qt鼠标点击事件处理:显示鼠标点击位置(完整示例)
Qt 入门实战教程(目录) 前驱文章: Qt Creator 创建 Qt 默认窗口程序(推荐) 什么是事件 事件是对各种应用程序需要知道的由应用程序内部或者外部产生的事情或者动作的通称。 事件(event)驱动…...

OpenCV:实现图像的负片
负片 负片是摄影中会经常接触到的一个词语,在最早的胶卷照片冲印中是指经曝光和显影加工后得到的影像。负片操作在很多图像处理软件中也叫反色,其明暗与原图像相反,其色彩则为原图像的补色。例如,颜色值A与颜色值B互为补色&#…...
HZOJ#237. 递归实现排列型枚举
题目描述 从 1−n这 n个整数排成一排并打乱次序,按字典序输出所有可能的选择方案。 输入 输入一个整数 n。(1≤n≤8) 输出 每行一组方案,每组方案中两个数之间用空格分隔。 注意每行最后一个数后没有空格。 样例…...
C++ PIMPL 编程技巧
C PIMPL 编程技巧 文章目录 C PIMPL 编程技巧什么是pimpl?pimpl优点举例实现 什么是pimpl? Pimpl (Pointer to Implementation) 是一种常见的 C 设计模式,用于隐藏类的实现细节,从而减少编译依赖和提高编译速度。它的基本思想是将…...
一个通用的EXCEL生成下载方法
Excel是一个Java开发中必须会用到的东西,之前博主也发过一篇关于使用Excel的文章,但是最近工作中,发现了一个更好的使用方法,所以,就对之前的博客进行总结,然后就有了这篇新的,万能通用的方法说…...
介绍 TensorFlow 的基本概念和使用场景。
TensorFlow(简称TF)是由Google开发的开源机器学习框架,它具有强大的数值计算和深度学习功能,广泛用于构建、训练和部署机器学习模型。以下是TensorFlow的基本概念和使用场景: 基本概念: 张量(T…...

【力扣】304. 二维区域和检索 - 矩阵不可变 <二维前缀和>
目录 【力扣】304. 二维区域和检索 - 矩阵不可变二维前缀和理论初始化计算面积 题解 【力扣】304. 二维区域和检索 - 矩阵不可变 给定一个二维矩阵 matrix,以下类型的多个请求: 计算其子矩形范围内元素的总和,该子矩阵的 左上角 为 (row1, …...

线上问诊:数仓开发(三)
系列文章目录 线上问诊:业务数据采集 线上问诊:数仓数据同步 线上问诊:数仓开发(一) 线上问诊:数仓开发(二) 线上问诊:数仓开发(三) 文章目录 系列文章目录前言一、ADS1.交易主题1.交易综合统计2.各医院交易统计3.各性…...

微信小程序 通过响应式数据控制元素class属性
我想大家照这个和我最初的目的一样 希望有和vue中v-bind:class一样方便的指令 但答案不太尽人意 这里 我们只能采用 三元运算符的形式 参考代码如下 <view class"item {{ userId item.userId ? isThisUser : }}"> </view>这里 我们判断 如果当前ite…...

linux并发服务器 —— linux网络编程(七)
网络结构模式 C/S结构 - 客户机/服务器;采用两层结构,服务器负责数据的管理,客户机负责完成与用户的交互;C/S结构中,服务器 - 后台服务,客户机 - 前台功能; 优点 1. 充分发挥客户端PC处理能力…...

Java后端开发面试题——企业场景篇
单点登录这块怎么实现的 单点登录的英文名叫做:Single Sign On(简称SSO),只需要登录一次,就可以访问所有信任的应用系统 JWT解决单点登录 用户访问其他系统,会在网关判断token是否有效 如果token无效则会返回401&am…...

TiDB x 安能物流丨打造一栈式物流数据平台
作者:李家林 安能物流数据库团队负责人 本文以安能物流作为案例,探讨了在数字化转型中,企业如何利用 TiDB 分布式数据库来应对复杂的业务需求和挑战。 安能物流作为中国领先的综合型物流集团,需要应对大规模的业务流程ÿ…...
负载均衡算法实现
负载均衡算法实现 负载均衡介绍 负责均衡主要有以下五种方法实现: 1、轮询法 将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载; 2、随机法 通过系统的随机算法&#…...
Python|GIF 解析与构建(5):手搓截屏和帧率控制
目录 Python|GIF 解析与构建(5):手搓截屏和帧率控制 一、引言 二、技术实现:手搓截屏模块 2.1 核心原理 2.2 代码解析:ScreenshotData类 2.2.1 截图函数:capture_screen 三、技术实现&…...

【第二十一章 SDIO接口(SDIO)】
第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...
1688商品列表API与其他数据源的对接思路
将1688商品列表API与其他数据源对接时,需结合业务场景设计数据流转链路,重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点: 一、核心对接场景与目标 商品数据同步 场景:将1688商品信息…...

让AI看见世界:MCP协议与服务器的工作原理
让AI看见世界:MCP协议与服务器的工作原理 MCP(Model Context Protocol)是一种创新的通信协议,旨在让大型语言模型能够安全、高效地与外部资源进行交互。在AI技术快速发展的今天,MCP正成为连接AI与现实世界的重要桥梁。…...

Map相关知识
数据结构 二叉树 二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子 节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只 有左子节点,有的节点只有…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
在Ubuntu24上采用Wine打开SourceInsight
1. 安装wine sudo apt install wine 2. 安装32位库支持,SourceInsight是32位程序 sudo dpkg --add-architecture i386 sudo apt update sudo apt install wine32:i386 3. 验证安装 wine --version 4. 安装必要的字体和库(解决显示问题) sudo apt install fonts-wqy…...

推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材)
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材) 这个项目能干嘛? 使用 gemini 2.0 的 api 和 google 其他的 api 来做衍生处理 简化和优化了文生图和图生图的行为(我的最主要) 并且有一些目标检测和切割(我用不到) 视频和 imagefx 因为没 a…...

GruntJS-前端自动化任务运行器从入门到实战
Grunt 完全指南:从入门到实战 一、Grunt 是什么? Grunt是一个基于 Node.js 的前端自动化任务运行器,主要用于自动化执行项目开发中重复性高的任务,例如文件压缩、代码编译、语法检查、单元测试、文件合并等。通过配置简洁的任务…...

基于SpringBoot在线拍卖系统的设计和实现
摘 要 随着社会的发展,社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 在线拍卖系统,主要的模块包括管理员;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单…...