从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr

news/2024/10/25 11:29:09/

目录

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. 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++的类型转化。


http://www.ppmy.cn/news/1076016.html

相关文章

Visual Studio 2017安装和项目配置

目录 前言1. What、Why and How1.1 What1.2 Why1.3 How 2. 安装3. 创建新项目4. 配置OpenCV库4.1 下载opencv安装包4.2 配置系统环境变量4.3 VS项目环境配置4.4 总结 5. 已有项目添加6. Tips6.1 常用快捷键6.2 字体和颜色选择6.3 配置编译路径 结语下载链接参考 前言 最近因为项…

gif怎么转换成mp4格式视频

gif怎么转换成mp4格式视频&#xff1f;GIF格式是一种广泛应用的公用图像文件格式标准&#xff0c;具有许多优势。它占用的内存较小&#xff0c;可以实现自动循环播放&#xff0c;并且兼容多个平台。然而&#xff0c;GIF格式也存在一些缺点。例如&#xff0c;它无法处理复杂的图…

【每日一题】力扣1768. 交替合并字符串

题目以及链接&#xff1a; 1768. 交替合并字符串 给你两个字符串 word1 和 word2 。请你从 word1 开始&#xff0c;通过交替添加字母来合并字符串。如果一个字符串比另一个字符串长&#xff0c;就将多出来的字母追加到合并后字符串的末尾。 返回 合并后的字符串 。 示例 1&…

echarts图表静态数据 象形柱形图、折线图、日历饼图、饼状图四种实现

标题 页面全部代码 <template><div class"data-serve"><div class"side"><div class"side-inner"><router-link class"side-btn" to"/camer/pushInfo"><i class"el-icon-picture&q…

idea 常用插件和常用快捷键 - 记录

idea 常用插件 记得下载插件完成后&#xff0c;点击 Apply 和 OK Alibaba Java Coding Guidelines 作用&#xff1a;使用该插件可以&#xff0c;自动提示相关的语法格式问题&#xff0c;格式参考 阿里巴巴代码规范 详情链接&#xff1a; 代码规范之Alibaba Java Coding G…

C++11 智能指针详解

C 程序设计中使用堆内存是非常频繁的操作&#xff0c;堆内存的申请和释放都由程序员自己管理。程序员自己 管理堆内存可以提高了程序的效率&#xff0c;但是整体来说堆内存的管理是麻烦的&#xff0c;C11 中引入了智能指针的 概念&#xff0c;方便管理堆内存。使用普通指针&…

算法通关村第9关【黄金】| 两道有挑战的问题

1. 将有序数组转换为二叉搜索树 思路&#xff1a;二分法&#xff0c;这个算法保证了每次选择的中间元素都能保持左右子树的高度差不超过 1&#xff0c;从而构建一个高度平衡的二叉搜索树。这个过程类似于分治法&#xff0c;通过递归不断将大问题分解成小问题并解决。 找到数组…

小米手机便签怎么导出到华为mate60Pro手机上?

华为mate60Pro手机于2023年8月29日发布了先锋计划&#xff0c;有不少网友都抢到了这款新机。而有一些网友表示自己在换手机之前遇到了问题&#xff0c;这就是之前使用的手机是小米&#xff0c;所以需要把重要的图片、短信、通讯录、便签等数据导出到新的手机上&#xff0c;但是…