【C++从0到王者】第四十站:智能指针

文章目录

  • 一、为什么需要智能指针
  • 二、智能指针
    • 1.基本使用
    • 2. RaII
    • 3.解引用
    • 4.赋值运算符重载的问题
  • 三、auto_ptr
    • 1.库里面的auto_ptr
    • 2.模拟实现auto_ptr
  • 四、unique_ptr
    • 1.库里面的unique_ptr
    • 2.模拟实现unique_ptr
  • 五、shared_ptr
    • 1.库里面的shared_ptr
    • 2.模拟实现shared_ptr
    • 3.缺陷:循环引用
    • 4.weak_ptr
  • 六、weak_ptr
    • 1.完善我们的shared_ptr
    • 2.模拟实现weak_ptr
  • 七、析构函数的一些其他问题
    • 1.定制删除器
    • 2.模拟实现一个定制删除器
  • 八、总结四种指针
  • 九、内存泄漏
    • 1.什么是内存泄漏,内存泄漏的危害
    • 2.内存泄漏分类
    • 3.如何检测内存泄漏
    • 4.如何避免内存泄漏

一、为什么需要智能指针

我们之前在提到异常的时候,有这样的一种应用场景

在下面的代码中,我们必须要捕获两次异常,不然的话,会导致p1的内存泄漏。但同时这样也使得代码很丑陋

#include 
#include 
#include 
#include 


using namespace std;
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
	{
		throw invalid_argument("除零错误");
	}
	return a / b;
}
void f()
{
	pair<string, string>* p1 = new pair<string, string>;
	
	try 
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete p1;
		cout << "delete: " << p1 << endl;
		throw;
	}
}
int main()
{
	try
	{
		f();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

【C++从0到王者】第四十站:智能指针_第1张图片

如果我们这里有两个或者更多的指针

#define _CRT_SECURE_NO_WARNINGS 1
#include 
#include 
#include 
#include 


using namespace std;
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
	{
		throw invalid_argument("除零错误");
	}
	return a / b;
}
void f()
{
	pair<string, string>* p1 = new pair<string, string>;
	pair<string, string>* p2 = new pair<string, string>;
	pair<string, string>* p3 = new pair<string, string>;
	
	try 
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete p1;
		cout << "delete: " << p1 << endl;
		throw;
	}
}
int main()
{
	try
	{
		f();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

如上代码中,我们多添加了几个指针,我们会发现,这里存在更多的问题了。如果new抛异常呢?对他们每一个都抛异常吗?那这样的话代码会非常麻烦,非常丑陋。

为了解决上面的问题,我们就有了一种智能指针的东西

二、智能指针

1.基本使用

为了避免前面的资源不好释放的问题。我们可以使用智能指针去管理,所谓智能指针其实就是封装了一层类,然后将这个指针交给这个类来管理,通过它的析构函数的特性,来避免内存泄漏的问题

class SmartPtr
{
public:
	SmartPtr(pair<string, string>* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		cout << "delete: " << _ptr << endl;
		delete _ptr;
	}
private:
	pair<string, string>* _ptr;
};
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
	{
		throw invalid_argument("除零错误");
	}
	return a / b;
}
void f()
{
	pair<string, string>* p1 = new pair<string, string>;
	SmartPtr sp(p1);
	cout << div() << endl;
}
int main()
{
	try
	{
		f();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

运行结果如下:

【C++从0到王者】第四十站:智能指针_第2张图片

class SmartPtr
{
public:
	SmartPtr(pair<string, string>* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		cout << "delete: " << _ptr << endl;
		delete _ptr;
	}
private:
	pair<string, string>* _ptr;
};
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
	{
		throw invalid_argument("除零错误");
	}
	return a / b;
}
void f()
{
	SmartPtr sp1(new pair<string, string>);
	cout << div() << endl;
	SmartPtr sp2(new pair<string, string>);
	SmartPtr sp3(new pair<string, string>);
	SmartPtr sp4(new pair<string, string>);
	cout << div() << endl;
}
int main()
{
	try
	{
		f();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

像一些div比较多的复杂场景也是可以的

如下是第一次就抛异常,刚好只有一个指针被释放

【C++从0到王者】第四十站:智能指针_第3张图片

如下是第二次的div抛出异常,那么就是两个指针被释放

【C++从0到王者】第四十站:智能指针_第4张图片

不过上面的例子太单一了,我们最好加上模板。这样就可以管理各种类型了

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		cout << "delete: " << _ptr << endl;
		delete _ptr;
	}
private:
	T* _ptr;
};
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
	{
		throw invalid_argument("除零错误");
	}
	return a / b;
}
void f()
{
	SmartPtr<pair<string, string>> sp1(new pair<string, string>);
	cout << div() << endl;
	SmartPtr<pair<string, string>> sp2(new pair<string, string>);
	SmartPtr<pair<string, string>> sp3(new pair<string, string>);
	SmartPtr<string> sp4(new string);
	cout << div() << endl;
}
int main()
{
	try
	{
		f();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

运行结果如下

【C++从0到王者】第四十站:智能指针_第5张图片

2. RaII

上面这种机制我们称作RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效

即资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源

3.解引用

不过我们上面还存在一些问题,那就是它还不能像指针使用,即不能解引用。

  1. RAII它必须要保证管控资源释放
  2. 其次一定要像指针一样可以去使用

如下代码所示,我们为他添加了解引用功能

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;
	}


private:
	T* _ptr;
};
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
	{
		throw invalid_argument("除零错误");
	}
	return a / b;
}
void f()
{
	SmartPtr<pair<string, string>> sp1(new pair<string, string>);
	//cout << div() << endl;
	SmartPtr<pair<string, string>> sp2(new pair<string, string>);
	SmartPtr<pair<string, string>> sp3(new pair<string, string>("张三", "94"));
	SmartPtr<string> sp4(new string("hello world"));
	cout << *sp4 << endl;
	cout << sp3->first << ", " << sp3->second << endl;
	//cout << div() << endl;
}
int main()
{
	try
	{
		f();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

运行结果如下所示

【C++从0到王者】第四十站:智能指针_第6张图片

4.赋值运算符重载的问题

如果我们直接使用赋值运算符重载的话,那么这里由于是一个而且也必须是一个浅拷贝,那么就会导致同一个地址被释放两次的问题,即程序崩溃了。而且这里也出现了内存泄漏

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;
	}
private:
	T* _ptr;
};
int main()
{
	SmartPtr<string> sp1(new string("hello sp1"));
	SmartPtr<string> sp2(new string("hello sp2"));
	sp1 = sp2;
	return 0;
}

运行结果如下:

【C++从0到王者】第四十站:智能指针_第7张图片

为了解决这个问题:C++库里面有以下四种智能指针,其中第一种不推荐使用。

C++98 搞出了一个auto_ptr

C++11 搞出了一个unique_ptr

C++11 shared_ptr

C++11 weak_ptr

三、auto_ptr

1.库里面的auto_ptr

auto_ptr会做一件事情,即管理权转移

如下代码是简单的使用auto_ptr

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << " ~A()" << endl;
	}
private:
	int _a;
};

int main()
{
	auto_ptr<A> ap1(new A(1));
	auto_ptr<A> ap2(new A(2));
	auto_ptr<A> ap3(ap1);


	return 0;
}

运行结果为

【C++从0到王者】第四十站:智能指针_第8张图片

我们在看一下监视窗口

【C++从0到王者】第四十站:智能指针_第9张图片

我们会注意到,这里我们调用了拷贝构造函数以后,ap1的内容被置空了。让ap3去管理资源了。这就是管理权转移。

拷贝时,会把被拷贝对象的资源管理权转移给拷贝对象,导致被拷贝对象悬空

我们可以对ap1进行一下操作,发现代码崩溃。

【C++从0到王者】第四十站:智能指针_第10张图片

对于这个智能指针,我们一般严禁使用这个

2.模拟实现auto_ptr

	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}

		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete: " << _ptr << endl;
				delete _ptr;
			}
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			if (this->_ptr == ap._ptr)
			{
				return *this;
			}
			if (_ptr)
			{
				delete _ptr;
			}
			_ptr = ap._ptr;
			ap._ptr = nullptr;
			return *this;
		}

	private:
		T* _ptr;
	};

四、unique_ptr

1.库里面的unique_ptr

unique_ptr的实现原理:简单粗暴的防拷贝

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << " ~A()" << endl;
	}
private:
	int _a;
};

int main()
{
	unique_ptr<A> ap1(new A(1));
	unique_ptr<A> ap2(new A(2));
	//unique_ptr ap3(ap1);

	return 0;
}

上面代码运行可以正常的释放内存。

如果我们把这个拷贝构造给加上,那么则直接报错

【C++从0到王者】第四十站:智能指针_第11张图片

2.模拟实现unique_ptr

如下所示,是我们模拟实现一个unique_ptr智能指针

值得注意的是这里我们要防拷贝的话,我们可以直接将拷贝构造与赋值运算符重载给放入私有中,然后只声明不实现即可,这是C++98的方法

也可以直接使用delete标识符直接删除这个函数,这是C++11的方法

	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			if (_ptr)
			{
				cout << "delete: " << _ptr << endl;
				delete _ptr;
			}
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
	//private:
		//C++98
		//unique_ptr(unique_ptr& ap);
		//unique_ptr& operator=(const unique_ptr& up);

		//C++11 delete也可以代表删除函数 
		unique_ptr(unique_ptr<T>& ap) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
		
	private:
		T* _ptr;
	};

五、shared_ptr

1.库里面的shared_ptr

有时候我们还是需要使用拷贝的,这时候就要用到shared_ptr了

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享

  2. 对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。

  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源

  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << " ~A()" << endl;
	}
	int _a;
};

int main()
{
	shared_ptr<A> sp1(new A(1));
	shared_ptr<A> sp2(new A(2));
	shared_ptr<A> sp3(sp1);
	
	sp1->_a++;
	sp2->_a++;
	sp3->_a++;

	cout << sp1->_a << endl;
	return 0;
}

运行结果为

【C++从0到王者】第四十站:智能指针_第12张图片

2.模拟实现shared_ptr

这里我们需要注意的是这个引用计数如何设置。我们期望的是每一份资源有一个count

  • 首先肯定不可以直接是一个普通的成员变量,因为我们需要的是指向同一个地址的指针要可以共享一个空间

    【C++从0到王者】第四十站:智能指针_第13张图片

  • 其次也不可以是静态的,静态的变量是属于该类的所有对象,那么就会导致即便是指向不同地址的指针,也会共享同一个空间

    【C++从0到王者】第四十站:智能指针_第14张图片

  • 我们需要的是一份资源有一个引用计数。那么这个引用计数,我们可以设置为动态在堆区的的变量。

    【C++从0到王者】第四十站:智能指针_第15张图片

最终代码如下所示:

这里最麻烦的就是赋值运算符重载,我们需要考虑两方面,一方面是如果是自己给自己赋值,那么直接返回即可。如果赋值了,那么原来的指针的引用计数就要减减,如果减为0,还要释放资源。最后才是对获得到的指针的操作

	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{}
		~shared_ptr()
		{
			if (_ptr)
			{
				//先引用计数--,一旦减到0就释放
				if (--(*_pcount) == 0)
				{
					cout << "delete: " << _ptr << endl;
					delete _ptr;
					delete _pcount;
				}
			}
		}
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
		shared_ptr(shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
		{
			++(*_pcount);
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//自己给自己赋值
			//这里一定要是指针一样的
			if (_ptr == sp._ptr)
			{ 
				return *this;
			}
			//清理原来的资源
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
			//更换新的资源
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);
			return *this;
		}
	private:
		T* _ptr;
		int* _pcount;
	};

3.缺陷:循环引用

我们看一下下面的代码

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << " ~A()" << endl;
	}
	int _a;
};

struct Node
{
	A _a;
	shared_ptr<Node> _prev;
	shared_ptr<Node> _next;
};

int main()
{
	shared_ptr<Node> sp1(new Node);
	shared_ptr<Node> sp2(new Node);

	sp1->_next = sp2;
	sp2->_prev = sp1;

	return 0;
}

运行以后,我们发现没有析构,发生了内存泄漏了

【C++从0到王者】第四十站:智能指针_第16张图片

上面这种现象,就是循环引用

如下是一开始我们定义好了两个指针

【C++从0到王者】第四十站:智能指针_第17张图片

紧接着我们让这两个相互指向。

这里就是两次赋值运算符重载。最终使得两个的引用计数都变为了2

【C++从0到王者】第四十站:智能指针_第18张图片

后来程序将要结束了,sp1和sp2这两个对象都要调用自己的析构函数了。但是这里发现引用计数是2,所以引用计数改为1即可。

【C++从0到王者】第四十站:智能指针_第19张图片

所以最终释放的仅仅只是sp1和sp2这两个栈区上的资源。sp1和sp2所指向堆区的资源中,由于这两块空间都有一个指针互相指向,所以就不会被释放掉。这两块内存就被泄漏掉了!

4.weak_ptr

要解决上面的循环引用问题。我们只需要将结点中的shared_ptr换为weak_ptr即可

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << " ~A()" << endl;
	}
	int _a;
};

struct Node
{
	A _a;
	weak_ptr<Node> _prev;
	weak_ptr<Node> _next;
};

int main()
{
	shared_ptr<Node> sp1(new Node);
	shared_ptr<Node> sp2(new Node);

	sp1->_next = sp2;
	sp2->_prev = sp1;

	return 0;
}

【C++从0到王者】第四十站:智能指针_第20张图片

weak_ptr其实并不是RAII智能指针。而是专门用来解决shared_ptr循环引用问题的

weak_ptr不增加引用计数,可以访问资源,不参与资源释放的管理

我们可以使用use_count接口来看到shared_ptr的引用计数个数

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << " ~A()" << endl;
	}
	int _a;
};

struct Node
{
	A _a;
	weak_ptr<Node> _prev;
	weak_ptr<Node> _next;
};

int main()
{
	shared_ptr<Node> sp1(new Node);
	shared_ptr<Node> sp2(new Node);
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;
	sp1->_next = sp2;
	sp2->_prev = sp1;
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;
	return 0;
}

运行结果为

【C++从0到王者】第四十站:智能指针_第21张图片

如果是shared_ptr

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << " ~A()" << endl;
	}
	int _a;
};

struct Node
{
	A _a;
	//weak_ptr _prev;
	//weak_ptr _next;
	shared_ptr<Node> _prev;
	shared_ptr<Node> _next;
};

int main()
{
	shared_ptr<Node> sp1(new Node);
	shared_ptr<Node> sp2(new Node);
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;
	sp1->_next = sp2;
	sp2->_prev = sp1;
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;
	return 0;
}

运行结果为

.【C++从0到王者】第四十站:智能指针_第22张图片

六、weak_ptr

1.完善我们的shared_ptr

我们先将我们的use_count接口给加上去

	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{}
		~shared_ptr()
		{
			if (_ptr)
			{
				//先引用计数--,一旦减到0就释放
				if (--(*_pcount) == 0)
				{
					cout << "delete: " << _ptr << endl;
					delete _ptr;
					delete _pcount;
				}
			}
		}
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
		{
			++(*_pcount);
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//自己给自己赋值
			//这里一定要是指针一样的
			if (_ptr == sp._ptr)
			{ 
				return *this;
			}
			//清理原来的资源
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
			//更换新的资源
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);
			return *this;
		}
		size_t use_count() const
		{
			return *_pcount;
		}

		T* get() const
		{
			return _ptr;
		}

	private:
		T* _ptr;
		int* _pcount;
	};

2.模拟实现weak_ptr

	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}
		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}

	private:
		T* _ptr;
	};

然后用我们自己的智能指针来测试一下我们的循环引用场景

【C++从0到王者】第四十站:智能指针_第23张图片

【C++从0到王者】第四十站:智能指针_第24张图片

七、析构函数的一些其他问题

1.定制删除器

在我们前面的模拟实现四种智能指针的过程中,我们的析构函数中释放资源的时候,仅仅只是delete,这样智能释放掉一个普通的资源,那么如果我们管理的是一个数组呢?如果管理的是一个malloc出来的呢?

这里就会出现不配套的问题了,就会代码直接崩溃

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << " ~A()" << endl;
	}
	int _a;
};

int main()
{
	std::shared_ptr<A> sp1(new A[10]);
	std::shared_ptr<A> sp2((A*)malloc(10 * sizeof(A)));
	return 0;
}

【C++从0到王者】第四十站:智能指针_第25张图片

库里面为了解决这个问题,引入了定制删除器

template <class U> explicit shared_ptr (U* p);
//用一个仿函数,去改变删除函数
template <class U, class D> shared_ptr (U* p, D del);

我们可以来实践一下

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << " ~A()" << endl;
	}
	int _a;
};

template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

int main()
{
	std::shared_ptr<A> sp1(new A[10], DeleteArray<A>());
	//std::shared_ptr sp2((A*)malloc(10 * sizeof(A)));
	return 0;
}

运行结果为

【C++从0到王者】第四十站:智能指针_第26张图片

除了仿函数之外,我们还可以使用lambda表达式

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << " ~A()" << endl;
	}
	int _a;
};

template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

int main()
{
	//定制删除器
	std::shared_ptr<A> sp1(new A[10], DeleteArray<A>());
	std::shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); });
	std::shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* fp) {fclose(fp); });
	return 0;
}

运行结果为

【C++从0到王者】第四十站:智能指针_第27张图片

2.模拟实现一个定制删除器

下面是针对于shared_ptr的定制删除器

如下代码所示:我们可以多添加一个构造函数,在构造的时候多传入一个参数,这个参数他可以是仿函数,可以是函数指针,可以是lambda表达式。这样的话,我们最好用一个包装器来接收一下。释放的时候,我们只需要调用这个包装器即可

	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{}

		template<class D>
		shared_ptr(T* ptr, D del)
			:_ptr(ptr)
			,_pcount(new int(1))
			,_del(del)
		{}
		~shared_ptr()
		{
			if (_ptr)
			{
				//先引用计数--,一旦减到0就释放
				if (--(*_pcount) == 0)
				{
					cout << "delete: " << _ptr << endl;
					//delete _ptr;
					_del(_ptr);
					delete _pcount;
				}
			}
		}
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
		{
			++(*_pcount);
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//自己给自己赋值
			//这里一定要是指针一样的
			if (_ptr == sp._ptr)
			{ 
				return *this;
			}
			//清理原来的资源
			if (--(*_pcount) == 0)
			{
				//delete _ptr;
				_del(_ptr);
				delete _pcount;
			}
			//更换新的资源
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);
			return *this;
		}
		size_t use_count() const
		{
			return *_pcount;
		}

		T* get() const
		{
			return _ptr;
		}

	private:
		T* _ptr;
		int* _pcount;
		function<void(T*)> _del = [](T* ptr) {delete ptr; };
	};

用如下代码进行测试

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << " ~A()" << endl;
	}
	int _a;
};

template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

int main()
{
	//定制删除器
	Sim::shared_ptr<A> sp1(new A[10], DeleteArray<A>());
	Sim::shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr) {cout << "free:" << ptr << endl; free(ptr); });
	Sim::shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* fp) {cout << "fclose:" << fp << endl;  fclose(fp); });

	Sim::shared_ptr<A> sp4(new A);
	return 0;
}

【C++从0到王者】第四十站:智能指针_第28张图片

其实定制删除器还有一个是对于unique_ptr的,他的跟shared_ptr不一样的是,他的定制删除器要在类模板参数上传递

image-20240205004348616

这里我们传递类型即可

八、总结四种指针

  • auto_ptr 管理权转移,会导致被拷贝对象悬空,建议不要使用
  • unique_ptr 禁止拷贝,简单粗暴。日常使用不需要拷贝的场景,建议使用它
  • shared_ptr 引用计数支持拷贝,需要拷贝的场景,就使用它。但是要小心构成循环引用
  • weak_ptr 专门解决shared_ptr的循环引用问题

九、内存泄漏

1.什么是内存泄漏,内存泄漏的危害

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

2.内存泄漏分类

C/C++程序中一般我们关心两种方面的内存泄漏:

  • 堆内存泄漏(Heap leak)

堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

  • 系统资源泄漏

指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

3.如何检测内存泄漏

  • 在linux下内存泄漏检测:检测工具

  • 在windows下使用第三方工具:检测工具

  • 其他工具:检测工具

4.如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。

  2. 采用RAII思想或者智能指针来管理资源。

  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。

  4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵

总结一下:

  • 内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。

你可能感兴趣的:(【C++】,c++,算法,开发语言,服务器,linux,运维,数据库)