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

news2024/10/7 8:21:59

目录

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>());

	// 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只能这样传

	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.coloradmin.cn/o/953915.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大数据学习:Hive安装部署

Hive的安装部署 注意hive就是一个构建数据仓库的工具&#xff0c;只需要在一台服务器上安装就可以了&#xff0c;不需要在多台服务器上安装。 此处以安装到node03为例&#xff1b;请大家保持统一 使用hadoop普通用户操作 1.1 先决条件 搭建好三节点Hadoop集群&#xff1b;node…

十六、策略模式

一、什么是策略模式 策略&#xff08;Strategy&#xff09;模式的定义&#xff1a;该模式定义了一系列算法&#xff0c;并将每个算法封装起来&#xff0c;使它们可以相互替换&#xff0c;且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式&#xff0c;它通过对算法…

Python+turtle实现一个乌龟逃跑小游戏(可以和孩子一起完成)

直接上演示视频 这个代码也是之前当老师的时候&#xff0c;给孩子们写的一个小游戏&#xff0c;那么我们一起看一下这个小游戏是如何让完成的 1、首先完成代码的前期准备 1、这里我们t turtle.Pen() # 海龟—表示我们操作的小海龟 2、enemy turtle.Pen() # 敌龟—表示追击我…

计算机视觉的应用12-卷积神经网络中图像特征提取的可视化研究,让大家理解特征提取的全过程

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下计算机视觉的应用12-卷积神经网络中图像特征提取的可视化研究&#xff0c;让大家理解特征提取的全过程。 要理解卷积神经网络中图像特征提取的全过程&#xff0c;我们可以将其比喻为人脑对视觉信息的处理过程。就像…

(数学) 剑指 Offer 39. 数组中出现次数超过一半的数字 ——【Leetcode每日一题】

❓ 剑指 Offer 39. 数组中出现次数超过一半的数字 难度&#xff1a;简单 数组中有一个数字出现的次数超过数组长度的一半&#xff0c;请找出这个数字。 你可以假设数组是非空的&#xff0c;并且给定的数组总是存在多数元素。 示例 1: 输入: [1, 2, 3, 2, 2, 2, 5, 4, 2] 输…

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…

学习ros机器人导航从精读nav2导航launch文件开始

nav2导航launch文件经过了多层套娃&#xff0c;真的是让初学者哭晕在厕所&#xff0c;今天我们就拆解一下他的launch文件&#xff0c;还原他最简单的状态&#xff0c;看看他到底启动了什么节点。 一 tb3仿真机器人启动文件&#xff1a;tb3_simulation_launch.py 1 文件目录结…

一文了解什么是同源策略

同源策略是一种重要的安全机制&#xff0c;它限制一个源加载的文档或脚本如何与另一个源的资源进行交互。下文小文智能就为您详细解答什么是同源策略。 一、目的 它有助于隔离潜在的恶意文档&#xff0c;减少可能的攻击媒介。例如&#xff0c;它可以防止互联网上的恶意网站在…

C++ 手写实现类似lower_bound和upper_bound的二分功能

目录 lower_bound和upper_bound介绍手动实现类似的二分效果lower_boundupper_bound另一种常见的二分形式 对lower_bound函数使用lamda函数 lower_bound和upper_bound介绍 lower_bound函数的作用是查找范围内第一个大于等于目标元素的元素迭代器/指针 数组的简单使用&#xff…

11、监测数据采集物联网应用开发步骤(8.2)

监测数据采集物联网应用开发步骤(8.1) 新建TCP/IP Client线程类com.zxy.tcp.ClientThread.py #! python3 # -*- coding: utf-8 -Created on 2017年05月10日 author: zxyong 13738196011 import datetime import socket import threading import timefrom com.zxy.adminlog.Us…

ATA-1222A宽带放大器的电子实验案例(案例合集)

ATA-1222A宽带放大器是安泰电子打造的高带宽功放产品&#xff0c;其采用ClassAB的工作模式&#xff0c;带宽高达22MHz&#xff0c;饱和输出功率40W&#xff0c;能兼容全球不同地区的电源标准要求。凭借其优异的指标参数受到不少电子工程师的喜欢&#xff0c;其在电子实验中的应…

远程访问Linux的DataEase数据可视化分析,有哪些推荐的工具?

DataEase 是开源的数据可视化分析工具&#xff0c;帮助用户快速分析数据并洞察业务趋势&#xff0c;从而实现业务的改进与优化。是开源的数据可视化分析工具&#xff0c;帮助用户快速分析数据并洞察业务趋势&#xff0c;从而实现业务的改进与优化。 在本地搭建后,借助cpolar 内…

Densenet模型详解

模型介绍 DenseNet的主要思想是密集连接&#xff0c;它在卷积神经网络&#xff08;CNN&#xff09;中引入了密集块&#xff08;Dense Block&#xff09;&#xff0c;在这些块中&#xff0c;每个层都与前面所有层直接连接。这种设计可以让信息更快速地传播&#xff0c;有助于解…

数据可视化工具中的显眼包:奥威BI自带方案上阵

根据经验来看&#xff0c;BI数据可视化分析项目是由BI数据可视化工具和数据分析方案两大部分共同组成&#xff0c;且大多数时候方案都需从零开始&#xff0c;反复调整&#xff0c;会耗费大量时间精力成本。而奥威BI数据可视化工具别具匠心&#xff0c;将17年经验凝聚成标准化、…

(AcWing) spfa求最短路

给定一个 n 个点 m 条边的有向图&#xff0c;图中可能存在重边和自环&#xff0c; 边权可能为负数。 请你求出 1 号点到 n 号点的最短距离&#xff0c;如果无法从 1 号点走到 n 号点&#xff0c;则输出 impossible。 数据保证不存在负权回路。 输入格式 第一行包含整数 n 和…

indexDb使用

indexDb是什么&#xff1f; indexDb是除了cookie&#xff0c;localstorage&#xff0c;sessionstroage外的另一种前端存贮方式。 现有前端存贮比较 indexDb特点 无大小限制&#xff0c;适用于前端存贮数据较多场景存贮结构以对象仓库形式&#xff0c;可以存入任何类型数据&a…

企业智能知识管理在线工具语雀、helplook、石墨文档、Baklib怎么样?

语雀、helplook、石墨文档和Baklib都是企业智能知识管理的在线工具&#xff0c;它们都提供了一系列功能来帮助企业管理和共享知识。下面我将对这些工具进行详细的介绍和评价。 语雀&#xff1a; 语雀是一款功能强大的在线知识管理工具&#xff0c;它提供了丰富的功能和优秀的…

Java对接海康威视(二次开发)组织信息、人员信息等

一.获取合作方数据 1.在【综合安防平台】的【关于】中前往【运行管理中心】 2.输入账户和密码进入【运行管理中心】 3.点击【状态监控】,搜索【能力开放网关】&#xff0c;点击【API管理】&#xff0c;查询可以对接的接口&#xff0c;点击对应的接口名称可以查看请求参数和返回…

抽象又有点垃圾的JavaScript

常数的排序 let x 10;let y 20;let z;if (x < y) {z x;x y;y z;}console.log(x, y);//x 20 ,y 10 通过一个媒介来继承x的初始值&#xff0c;然后将y的值赋值给x&#xff0c;再把媒介z的值赋值给y&#xff0c;达到排序 一个可重复使用的排序程序 第一种 function s…

微力同步私人网盘部署教程:利用端口映射实现远程访问的解决方案

文章目录 1.前言2. 微力同步网站搭建2.1 微力同步下载和安装2.2 微力同步网页测试2.3 cpolar的安装和注册 3.本地网页发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 1.前言 私有云盘作为云存储概念的延伸&#xff0c;虽然谈不上多么新颖&#xff0c;但是其…