C++之智能指针

为什么会有智能指针

前面我们知道使用异常可能会导致部分资源没有被正常释放, 因为异常抛出之后会直接跳转到捕获异常的地方从而跳过了一些很重要的的代码, 比如说下面的情况:

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	cout << "delete p1" << endl;
	delete p1;
	cout << "delete p2" << endl;
	delete p2;
}
int main()
{
	try{Func();}
	catch (exception& e)
	{cout << e.what() << endl;}
	return 0;
}

main函数中调用了func函数, func函数里面调用了div函数, func函数中没有捕捉异常, 但是在main函数里面却捕捉了异常, 所以出现异常的话就会导致func函数中的部分代码没有被执行, 进而导致内存泄漏:

无异常抛出正常内存释放: 

有异常抛出, 内存泄漏:  

为了解决这个问题就可以异常重新抛出, 在func函数里面添加捕获异常的代码, 然后在catch里面对资源进行释放最后重新将异常进行抛出, 最后交给main函数中的catch进行处理, 比如说下面的代码: 

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	int* p1 = new int;
	int* p2 = new int;

	try 
	{
		cout << div() << endl;
	}
	catch (...)
	{
		cout << "delete p1" << endl;
		delete p1;
		cout << "delete p2" << endl;
		delete p2;
		throw;
	}
	cout << "delete p1" << endl;
	delete p1;
	cout << "delete p2" << endl;
	delete p2;
}
int main()
{
	try { Func(); }
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

无论除零是否有异常都会正常释放资源: 

但是这么写就完成正确了吗? 肯定没有, 因为new本身也是会抛异常的, 当内存不足却又使用new申请空间的话就会导致开辟空间失败从而抛出异常, 那new抛异常会导致什么结果呢?

首先p1抛出异常会有什么问题吗?

没有, p1抛出异常会直接跳转到main函数里面进行捕捉并且p2还没有开辟, p1没有开辟成功, 从而不会导致任何的内存泄漏.

那要是p2开辟失败了呢?

这时候也是会直接跳转到main函数里面进行捕捉, 但是p1已经开辟空间了, 如果p2开辟空间失败他会导致p1的申请的资源没有被正常释放, 所以为了安全起见我们给p2也添加一个try上去并且try块里面还得含有后面的代码, 因为一旦内存申请失败后面的调用函数也无需执行了.

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	int* p1 = new int;
	try
	{
		int* p2 = new int;
		try
		{
            cout << div() << endl;
        }
		catch (...){
			cout << "delete p1" << endl;
			delete p1;
			cout << "delete p2" << endl;
			delete p2;
			throw;
		}
		cout << "delete p1" << endl;
		delete p1;
		cout << "delete p2" << endl;
		delete p2;
	}
	catch (...)
	{
		cout << "delete p1" << endl;
		delete p1;
        throw;
	}
}
int main()
{
	try { Func(); }
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

我们这里只new了两个对象, 那如果有三个有四个甚至更多的话又该如何来嵌套try catch呢? 所以当出现连续抛出异常的情况时, 我们之前学习的try catch语句就很难进行应对, 那么为了解决这个问题就有了智能指针


智能指针的使用及原理

RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期控制程序资源(如内
存, 文件句柄, 网络连接, 互斥量等等)的简单技术.
在对象构造时获取资源, 接着控制对资源的访问使之在对象的生命周期内始终保持有效, 最后在对象析构的时候释放资源. 借此, 我们实际上把管理一份资源的责任托管给了一个对象, 这种做法有两大好处:
1. 不需要显式地释放资源. 
2. 采用这种方式, 对象所需的资源在其生命期内始终保持有效.

注意: 智能指针是RAII的一种实现方式.


智能指针实现 

首先智能指针是一个类, 并且这个类要处理各种各样的数据, 所以这个类就要是模板类, 比如:

#pragma once
template<class T>
class SmartPoint
{
public:
	// RAII
	SmartPoint(T* ptr = nullptr)
		:_ptr(ptr)
	{}
	~SmartPoint()
	{
		cout << "~SmartPoint" << endl;
		delete _ptr;
	}
private:
	T* _ptr;
};

构造函数拿T类型的指针来初始化内部的_ptr就行, 析构函数在内部使用delete释放指针指向的空间即可.

#include<iostream>
using namespace std;
#include"SmartPoint.h"

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}

void Func()
{
	SmartPoint<int> sp1 = new int;
	SmartPoint<int> sp2 = new int;
	cout << div() << endl;

    //有了智能指针就不需要显示释放资源
	//cout << "delete p1" << endl;
	//delete p1;
	//cout << "delete p2" << endl;
	//delete p2;
}

int main()
{
	try { Func(); }
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

可以看到出现了除0错误这里也可以将两个申请的空间进行释放, 原理就是智能指针对象的生命周期属于Func函数, 当除0错误抛出异常的时候会从当前函数递归式的匹配catch直到main函数, 在匹配catch的时候虽然函数不会向下执行,但是函数栈帧会随之销毁, 其中类对象的生命周期也就跟着结束, 就会自动调用析构函数来释放空间, 就解决了之前的问题. 而且这样做不需要显式地释放资源, 而且对象所需的资源在其生命期内始终保持有效.


智能指针的原理

上述的SmartPtr还不能将其称为智能指针, 因为它还不具有指针的行为. 指针可以解引用, 也可以通过->去访问所指空间中的内容, 因此模板类中还需要将重载* 、->, 才可让其像指针一样去使用.

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

	~SmartPoint()
	{
		cout << "~SmartPoint" << endl;
		delete _ptr;
	}

	// 像指针一样
	T& oprator* ()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};
#include<iostream>
using namespace std;
#include"SmartPoint.h"

struct Date
{
	int _year;
	int _month;
	int _day;
};

int main()
{
	SmartPoint<int> sp1(new int);
	*sp1 = 10;
	cout << *sp1 << endl;

	SmartPoint<Date> sparray(new Date);
	// 需要注意的是这里应该是sparray.operator->()->_year = 2018;
	// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
	sparray->_year = 2023;
	sparray->_month = 12;
	sparray->_day = 16;

	cout << "year:" << sparray->_year<< endl;
	cout << "month:" << sparray->_month << endl;
	cout << "day:" << sparray->_day << endl;

	return 0;
}

总结一下智能指针的原理:

1. RAII特性.
2. 重载operator*和opertaor->, 具有像指针一样的行为.


C++库中的智能指针

在这之前我们首先来看看下面这段代码:

void func2()
{
	SmartPoint<int>sp1(new int(10));
	SmartPoint<int>sp2(sp1);
}

int main()
{
	func2();
	return 0;
}

原因很简单我们自己实现的类里面没有拷贝构造函数, 所以默认的拷贝构造函数以浅拷贝的方式进行构造, 所以对象的生命周期结束时就会调用delete将同一份空间析构两次, 所以就会报错.

那么为了解决这个问题我们就得自己来实现一个拷贝构造函数, 可是这里的拷贝构造并不能是深拷贝, 因为我们这个类的目的是让它和指针一样对资源进行管理, 而并不是一个容器对资源进行存储. 所以这里就不能采用深拷贝的形式来进行拷贝构造, 那么库中是如何来解决这个问题的呢?

先看看最早的auto_ptr如何解决这个问题: 

std::auto_ptr

std::auto_ptr文档

 C++98版本的库中就提供了auto_ptr的智能指针. auto_ptr的实现原理是 管理权转移的思想.

简化模拟实现一份test::auto_ptr来了解它的原理 :

namespace test
{
	// C++98
	// 管理权转移,最后一个拷贝对象管理资源,被拷贝对象都被置空
	template<class T>
	class auto_ptr
	{
	public:
		// RAII
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete->" << _ptr << endl;
				delete _ptr;
				_ptr = nullptr;
			}
		}

		// ap2(ap1)
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			//管理权转移
			ap._ptr = nullptr;
		}

		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			// 检测是否为自己给自己赋值
			if (this != &ap)
			{
				// 释放当前对象中资源
				if (_ptr)
					delete _ptr;
				// 转移ap中资源到当前对象中
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}

		// 像指针一样
		T& operator*()
		{
			return *_ptr;
		}

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

	private:
		T* _ptr;
	};

}

auto_ptr的解决方法就是将管理权进行转移, 把原来的智能指针变为空, 让新的智能指针指向这个空间, 

int main()
{
	 std::auto_ptr<int> sp1(new int);
	 std::auto_ptr<int> sp2(sp1); // 管理权转移

	 // sp1悬空
	 *sp2 = 10;
	 cout << *sp2 << endl;
	 cout << *sp1 << endl;//解引用空指针
	 return 0;
}

结论: auto_ptr是一个失败设计, 很多场景明确要求不能使用auto_ptr, 原因就是auto_ptr使用的方式太不合常理了.

后来为了解决auto_ptr难用的问题, 就有了unique_ptrshare_ptr/weak_ptr .


std::unique_ptr

unique_ptr文档

	// C++11 
	template<class T>
	class unique_ptr
	{
	public:
		// RAII
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~unique_ptr()
		{
			cout << "delete->" << _ptr << endl;

			delete _ptr;
		}

		// 像指针一样
		T& operator*()
		{
			return *_ptr;
		}

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

		// C++11
		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

	private:
		// C++98
		// 1、只声明不实现
		// 2、限定为私有
		//unique_ptr(const unique_ptr<T>& up);
		//unique_ptr<T>& operator=(const unique_ptr<T>& up);
	private:
		T* _ptr;
	};

unique_ptr解决拷贝构造问题的思路简单粗暴, 直接禁止拷贝构造(拷贝构造设为私有或者C++11中设为delete).

int main()
{
	test::unique_ptr<int> sp1(new int);
	test::unique_ptr<int> sp2(sp1);

	test::unique_ptr<int> sp3(new int);
	sp3 = sp1;

 return 0;
}


std::shared_ptr

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr 

std::shared_ptr文档

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

1. shared_ptr在其内部, 给每个资源都维护了着一份计数, 用来记录该份资源被几个对象共享
2. 在对象被销毁时(也就是析构函数调用), 就说明自己不使用该资源了, 对象的引用计数减一
3. 如果引用计数是0, 就说明自己是最后一个使用该资源的对象, 必须释放该资源;
4. 如果不是0, 就说明除了自己还有其他对象在使用该份资源, 不能释放该资源, 否则其他对象就成野指针了。 

 一个智能指针指向两块空间, 一块用于存储数据, 另一块记录当前空间被几个智能指针所指向, 当前只有对象sp1指向这个空间所以当前的计数就为1:

当我们再创建一个对象sp2并指向这个空间时图片就变成了:

当sp1对象生命周期结束, 或者sp1指向其他内容时, 就变成了: 

当引用计数为0时, 空间被释放: 

int main()
{
	std::shared_ptr<string> sp1(new string("xxxxxxxxxx"));
	std::shared_ptr<string> sp2 = sp1;
	std::shared_ptr<string> sp3;
	sp3 = sp1;

	std::shared_ptr<string> sp4(new string("xxxxxxxxxxxxxxxxx"));
	sp3 = sp4;

	cout << *sp1 << endl;
	cout << *sp2 << endl;
	cout << *sp3 << endl;
	cout << *sp4 << endl;
	return 0;
}

可以看到使用share_ptr既不会出现拷贝完原指针置空(auto_ptr)的情况, 也不会出现禁止拷贝(unique_ptr)的情况, 而且这个智能指针跟普通的指针一样指向的是同一块区域的内容.

模拟实现一下shared_ptr:

首先有个问题: 引用计数的空间如何来分配?

可以是个普通的整型变量放到对象里面吗?

不可以, 因为当计数变量的值发生改变时, 所有指向该空间对象的内部计数变量都得改变, 此时只改变一个对象的引用计数对其它对象内存储的引用计数毫无影响.

那么可以使用静态成员变量来实现吗?

看上去可以, 因为不管类实例化出来了多少个对象, 这个静态变量只有一个, 并且所有对象都会共享这个静态变量, 那么这时只要一个对象对这个静态变量进行修改的话, 其他对象都会跟着一起修改, 这是不是达到了我们的目的呢?

其实没有, 因为静态变量虽然一个对象修改所有对象都会共享, 但是这个对象指的是这个类所有实例化出来的对象, 假如又有一个智能指针指向一块新开辟的资源需要管理, 那么这个智能指针的引用计数需要初始化为1, 这就影响了之前的引用计数, 虽然是同一个类实例化出来的对象, 但是管理的资源可能会不同, 引用计数混在一起就乱套了.

正确的方法是在类里面添加一个整型的指针变量, 让指针指向new开辟的一块空间:

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

	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			cout << "delete->" << _ptr << endl;
			delete _ptr;
			delete _pcount;
		}
	}
	// 像指针一样
	T& operator*()
	{
		return *_ptr;
	}

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

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

构造函数中将引用计数初始化为1, 调用一次析构函数引用计数就--一次, 当引用计数为0时就释放被管理的资源和为引用计数开辟的那块资源. 

拷贝构造函数:

// sp2(sp1)
shared_ptr(const shared_ptr<T>& sp)
	:_ptr(sp._ptr)
	,_pcount(sp._pcount)
{
	(*_pcount)++;
}

 拷贝构造函数比较简单, 把_ptr和_pcount拷贝过来然后引用计数++即可.

赋值重载就需要注意了: 

//sp2 = sp1
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
    //要考虑是不是自己给自己赋值
	if (_ptr != sp._ptr)
	{
		if (--*(_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
		}

		_ptr = sp._ptr;
		_pcount = sp._pcount;
		(*_pcount)++;
	}
	return *this;
}

首先要判断要拷贝的被拷贝的对象指向的是不是同一块空间:

1. 如果是同一块空间(也就是自己给自己赋值)就没有必要进行拷贝, 或者说不能进行拷贝, 因为按照这个逻辑会先把引用计数--判断其是否为0决定原来的空间要不要释放, 如果当前引用计数不为1还好, 只是重复拷贝的问题, 但是如果引用计数为1, 先--引用计数然后把空间释放了, 再去拷贝这段已经被释放了的空间, 显然是不对的, 所以要先判断是否是自己给自己赋值.

自己给自己赋值不要用this != &sp来判断了, 因为这里的自己赋值实际指的是底层管理的那段空间是不是一个空间, 所以用_ptr != sp._ptr来判断.

2. 如果不是同一块空间, 先要把原来的引用计数--, 并判断是否为0, 为0就需要把原来的空间释放掉, 处理完原来的空间再去拷贝新的空间.


std::shared_ptr的循环引用

首先我们创建一个名为Listnode的类, 类里面含有两个listnode的指针和一个int的变量用来存储数据, 然后创建一个析构函数用来作为标记, 那么这里的代码就如下:

struct Listnode
{
	Listnode* next;
	Listnode* prev;
	int val;
};

void test_shared_ptr()
{
	test::shared_ptr<Listnode> ptr1 = new Listnode;
	test::shared_ptr<Listnode> ptr2 = new Listnode;

	//ptr1->next = ptr2;
    //ptr2->prev = ptr1;
}

可以看到我希望用shared_ptr去管理创建的Listnode, 但是ptr1->next = ptr2 和ptr2->prev = ptr1的赋值肯定是不能兼容的, 所以改变一下Listnode的成员变量:

struct Listnode
{
	test::shared_ptr<Listnode> next;
	test::shared_ptr<Listnode> prev;
	int val;

    //顺便添加一个析构, 等会以便于观察
    ~Listnode()
	{
		cout << "~Listnode()" << endl;
	}
};

void test_shared_ptr()
{
	test::shared_ptr<Listnode> n1 = new Listnode;
	test::shared_ptr<Listnode> n2 = new Listnode;

	//循环引用
	n1->next = n2;
	n2->prev = n1;
}

调用 test_shared_ptr():

int main()
{
	test_shared_ptr();
	return 0;
}

 

发现没有调用析构, 也就是发生了内存泄漏. 

现在将下面一行代码注释掉, 看看是否能正常析构:

void test_shared_ptr()
{
	test::shared_ptr<Listnode> n1 = new Listnode;
	test::shared_ptr<Listnode> n2 = new Listnode;

	//循环引用
	n1->next = n2;
	//n2->prev = n1;
}

int main()
{
	test_shared_ptr();
	return 0;
}

可以正常析构, 为什么呢? 

 这是一开始n1和n2都只是默认初始化时的状态:

 n1->next = n2之后, 指针指向变成了这样:

这种情况下n1和n2可以正常, 按照构造函数相反的方向, n2的析构函数先调用, 引用计数--为1, 不发生任何改变, n1的析构函数再调用, 引用计数--为0, 调用delete _ptr也就是调用Listnode的析构函数, 打印~ListNode()后, prev先析构, 正常析构即可, next析构引用计数--为0, 释放原来n2指向的那片Listnode空间, 这部分空间内的两个shared_ptr都未初始化, 正常析构没有问题, 至此析构完成.

再来重新分析这段代码的析构:

void test_shared_ptr()
{
	test::shared_ptr<Listnode> n1 = new Listnode;
	test::shared_ptr<Listnode> n2 = new Listnode;

	//循环引用
	n1->next = n2;
	n2->prev = n1;
}

同样的, n2先析构(*_pcount)--为1, n1再析构(*_pcount)--为1, 此时析构已经调用完成了, 引用计数都没有减到0, 不会对资源进行释放, 就发生了内存泄漏, 这也就是shared_ptr发生的循环引用问题.

对于std:: shared_ptr也同样有这个问题, 而且库里的shared_ptr的构造函数加了explicit, 不能隐式类型转换了:

struct Listnode
{
	int val;
	std::shared_ptr<Listnode> next;
	std::shared_ptr<Listnode> prev;
	
	~Listnode()
	{
		cout << "~Listnode()" << endl;
	}
};

void test_shared_ptr2()
{
	std::shared_ptr<Listnode> n1(new Listnode);
	std::shared_ptr<Listnode> n2(new Listnode);

	//循环引用
	n1->next = n2;
	n2->prev = n1;
}

int main()
{
	test_shared_ptr();
	return 0;
}


std::weak_ptr 

上面的问题最根本在于我们默许prev和next指针参与资源的管理, 因此为了解决循环引用的问题,weak_ptr就出现了. 库里面的解决方式是把Listnode里的next和prev换成weak_ptr :

 可以看到weak_ptr是支持用shared_ptr去构造的.

struct Listnode
{
	int val;
	std::weak_ptr<Listnode> next;
	std::weak_ptr<Listnode> prev;

	~Listnode()
	{
		cout << "~Listnode()" << endl;
	}
};

weak_ptr是一个有资源指向能力但没有管理权的指针, 所以无论创建多少个weak_ptr, 对应shared_ptr的引用计数也不会增加.

 weak_ptr特点

1. 不是传统的智能指针, 不支持RAII

2. weak_ptr中只有一个普通指针作为成员变量, 用来指向开辟的内存空间, 它不会去管理引用计数, 但是能去查看引用计数.

3. 能像指针一样使用

 虽然weak_ptr不能去管理引用计数, 但是它需要能查看引用计数, 因为假如有一个shared_ptr和weak_ptr指向同一块空间, shared_ptr释放了但是weak_ptr并不知道, 为了解决这个问题, weak_ptr也有use_count接口可以查看引用计数, 其次它还有一个expired接口检查它是否"过期".

void test_shared_ptr2()
{
	std::shared_ptr<Listnode> n1(new Listnode);
	std::shared_ptr<Listnode> n2(new Listnode);
	cout << n1.use_count()<<endl;
	cout << n2.use_count() << endl;

	//循环引用
	n1->next = n2;
	n2->prev = n1;
	//weak_ptr接口
	cout << n1->next.use_count() << endl;
	cout << n2->prev.use_count() << endl;
	cout << n1->next.expired() << endl;;
	cout << n2->prev.expired() << endl;;
}

 简单实现一个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& operator=(const shared_ptr<T>& wp)
	{
		_ptr = wp.get();
		return *this;
	}

	//像指针一样使用
	T* operator->()
	{
		return _ptr;
	}

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

private:
	T* _ptr;
};

这里用shared_ptr去构造weak_ptr需要调用shared_ptr的get接口去得到管理的那部分空间的地址, 在shared_ptr里添加一个get:


定制删除器 

智能指针不光可以管理一个变量, 也可以管理一个数组。

对于一个变量而言,可以使用delete _ptr,但对于一个数组而言,回收资源就需要用delete[] _ptr,可是我们之前实现的shared_ptr释放用的是delete, 所以去管理一个数组空间时析构会崩溃(std::shared_ptr也会崩溃):

void test_shared_ptr3()
{
	test::shared_ptr<string> n(new string[10]);
    //std::shared_ptr<string> n(new string[10]);
}

所以这里我们需要一个定制删除器来实现对不同资源的不同释放方式 :

 在构造shared_ptr时可以传递第二个 对象参数, 也就是定制删除器, 来实现自定义的删除方式.

1. 我们可以选择传递一个仿函数对象: 

template<class T>
struct ListnodeDL
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};
void test_shared_ptr3()
{
	std::shared_ptr<Listnode> p(new Listnode[10], ListnodeDL<Listnode>());
}

int main()
{
	test_shared_ptr3();
	return 0;
}

2. 还可以选择传递一个lambda对象, 更加方便:

void test_shared_ptr3()
{
	std::shared_ptr<Listnode> p(new Listnode[10], [](Listnode* ptr) {delete[] ptr; });
}

 3. 还可以去管理一个文件指针:

void test_shared_ptr3()
{
	std::shared_ptr<FILE> p3(fopen("test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });
}

 了解完定制删除器后, 现在可以为之前模拟实现的shared_ptr添加上删除器:

 

可以看到新添加了一个包装器成员_del, 因为我们要在释放资源时中去调用删除器自己实现的删除功能, 它是一个返回值类型为void参数为T*类型的函数包装器.

注意: 这里_del要给一个缺省值, 或者在单参数的构造里初始化列表初始化, 因为之前的delete注释掉了, 对于普通场景的资源释放也需要处理.

完整代码: 

	template<class T>
	class shared_ptr
	{
	public:
		// RAII
		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 (--(*_pcount) == 0)
			{
				//cout << "delete->" << _ptr << endl;
				//delete _ptr; 有了定制删除器, 就不能直接用delete了
				_del(_ptr);
				delete _pcount;
			}
		}

		// sp2(sp1)
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
		{
			(*_pcount)++;
		}

		//sp2 = sp1
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				(*_pcount)--;
				if (*_pcount == 0)
				{
					//delete _ptr;
					_del(_ptr);
					delete _pcount;
				}

				_ptr = sp._ptr;
				_pcount = sp._pcount;
				(*_pcount)++;
			}
			return *this;
		}

		T* get() const
		{
			return _ptr;
		}

		// 像指针一样
		T& operator*()
		{
			return *_ptr;
		}

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

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

测试一下: 

void test_shared_ptr3()
{
	test::shared_ptr<Listnode> p1(new Listnode[10], ListnodeDL<Listnode>());
	test::shared_ptr<Listnode> p2(new Listnode[10], [](Listnode* ptr) {delete[] ptr; });
	test::shared_ptr<FILE> p3(fopen("test.txt","r"), [](FILE* ptr) {fclose(ptr); });
	test::shared_ptr<Listnode> p4(new Listnode);
}

int main()
{
	test_shared_ptr3();
	return 0;
}

传alloc参数 

此外, 库中还有一种构造可以传Allocator, 这是为了防止内存碎片化, 因为每一个智能指针都维护一个引用计数, 大量使用智能指针就会有大量的内存碎片


 C++智能指针发展历史

C++ 98的auto_ptr: 管理权的转移->不好的设计, 对象悬空. (不建议使用)

boost的scoped_ptr: 防止拷贝->简单粗暴, 对于不需要拷贝的场景很好.

boost的shared_ptr: 引用计数, 最后一个释放的对象释放资源-> 复杂一些, 但支持拷贝, 很好->问题, 循环引用.

C++11的unique_ptr: 防止拷贝->简单粗暴, 对于不需要拷贝的场景很好.

C++11的shared_ptr: 引用计数, 最后一个释放的对象释放资源-> 复杂一些, 但支持拷贝, 很好->问题, 循环引用.

而C++11的unique_ptr和shared_ptr, 分别对应boost库中的scoped_ptr和shared_ptr. 

什么是boost?

一项改变要正式进入标准是很严谨的事情, 所以标准委员会库工作组织成立了一个boost的第三方库作为标准库的后备:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/429990.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

windows下的反调试探究——原理

原理 我们在前面介绍了一些反调试的手段&#xff0c;基本上都是通过对内核的某个标志进行修改来达到反调试的效果&#xff0c;但是这里有一个问题就是&#xff0c;如果分析人员对我们的样本的API进行了hook&#xff0c;那么我们的反调试手段都将作废&#xff0c;也就是说我们还…

【LeetCode】升级打怪之路 Day 13:优先级队列的应用

今日题目&#xff1a; 23. 合并 K 个升序链表 | LeetCode378. 有序矩阵中第 K 小的元素 | LeetCode373. 查找和最小的 K 对数字 | LeetCode703. 数据流中的第 K 大元素 | LeetCode347. 前 K 个高频元素 | LeetCode 目录 Problem 1&#xff1a;合并多个有序链表 【classic】LC 2…

2核4G服务器支持多少人在线?腾讯云全访问测试

腾讯云轻量应用服务器2核4G5M配置一年优惠价165元、252元15个月、三年756元&#xff0c;100%CPU性能&#xff0c;5M带宽下载速度640KB/秒&#xff0c;60GB SSD系统盘&#xff0c;月流量500GB&#xff0c;折合每天16.6GB流量&#xff0c;超出月流量包的流量按照0.8元每GB的价格支…

WSL2安装Ubuntu18.04到指定路径(非C盘)

1 系统设置开启WSL 1.1 在搜索框搜索“启动或关闭Windows功能”或在“控制面板”->“程序”->“启用或关闭 windows 功能” 开启 Windows 虚拟化和 Linux 子系统&#xff08;WSL2)以及Hyper-V 按照提示重启计算机&#xff0c;开启WSL。 2 将WSL2 设置为默认版本 wsl --se…

Mysql删除重复项:力扣196. 删除重复的电子邮箱

题目链接&#xff1a;196. 删除重复的电子邮箱 - 力扣&#xff08;LeetCode&#xff09; 题目描述 sql语句 # Write your MySQL query statement below delete a from person as a inner join person as b where a.email b.email and a.id > b.id 思路&#xff1a;内连接…

MySQL NDB Cluster 分布式架构搭建 自定义启动、重启和关闭集群Shell脚本

此次NDB Cluster使用三台虚拟机进行搭建&#xff0c;一台作为管理节点&#xff1b;而对于另外两台服务器&#xff0c;每一台都充当着数据节点和SQL节点的角色。注意不是MGR主从复制架构&#xff0c;而是分布式MySQL架构。 创建 /var/lib/mysql-cluster/config.ini Cluster全局…

uipath调用python代码获取网站验证码

用uipath自带的ocr读验证码不是很准确&#xff0c;选择调用python读验证码&#xff0c;需要导入ddddocr&#xff08;3.8以下版本支持ddddocr&#xff09; 用uipath程序将验证码图片保存到本地&#xff08;也可以直接用python处理图片&#xff0c;保存到本地比较简单&#xff0…

xss.haozi.me:0X0D

alert(1) -> 记住要回车一下-->是js的一个注释符但是只能用在最前面前面有一个空格都不行

C++:String的模拟实现

模拟实现的节奏比较快&#xff0c;大家可以先去看看博主的关于string的使用&#xff0c;然后再来看这里的模拟实现过程 C&#xff1a;String类的使用-CSDN博客 String模拟实现大致框架迭代器以及迭代器的获取&#xff08;public定义&#xff0c;要有可读可写的也要有可读不可写…

基于springboot+vue的医院药品管理系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

【Android】源码解析 Activity 的构成

本文是基于 Android 14 的源码解析。 当我们写 Activity 时会调用 setContentView() 方法来加载布局。现在来看看 setContentView() 方法是怎么实现的&#xff0c;源码如下所示&#xff1a; 路径&#xff1a;/frameworks/base/core/java/android/app/Activity.javapublic void…

Linux中服务端开发

1 创建socket,返回一个文件描述符lfd---socket(); 2 将lfd和IP&#xff0c;PROT进行绑定---bind(); 3 将lfd由主动变成被动监听---listen(); 4 接收一个新的连接&#xff0c;得到一个的文件描述符cfd--accept() --该文件描述符用于与客户端通信 5 while(1) { 接受数据&a…

【扩散模型系列3】DiT开源项目

文章目录 DiT原始项目Fast-DiT readmeSamplingTraining训练之前的准备训练DiTPyTorch 训练结果改进训练效果 Evaluation (FID, Inception Score, etc.) 总结 DiT原始项目 该项目仅针对DiT训练&#xff0c;并未包含VAE 的训练 项目地址 论文主页 Fast-DiT readme 该项目仅针…

性能优化篇(七) UI优化注意事项以及使用Sprite Atlas打包精灵图集

UI优化注意事项 1.尽量避免使用IMGUI(OnGUI)来做游戏时的UI&#xff0c;因为IMGUI的开销比较大。 2.如果一个UGUI的控件不需要进行射线检测&#xff0c;则可以取消勾选Raycast Target 3.尽量避免使用完全透明的图片和UI控件。因为即使完全透明&#xff0c;我们看不见它&#xf…

论文笔记:Code Llama: Open Foundation Models for Code

导语 Code Llama是开源模型Llama 2在代码领域的一个专有模型&#xff0c;作者通过在代码数据集上进行进一步训练得到了了适用于该领域的专有模型&#xff0c;并在测试基准中超过了同等参数规模的其他公开模型。 链接&#xff1a;https://arxiv.org/abs/2308.12950机构&#x…

[cg] Games 202 - NPR 非真实感渲染

NPR特性&#xff08;基于真实感渲染&#xff09; 真实感--》翻译成非真实感的过程 NPR风格 需要转换为渲染中的操作 1.描边 B-->普通边界&#xff08;不是下面几种的&#xff09; C-->折痕 M-->材质边界 S-->需要在物体外面一圈上&#xff0c;并且是多个面共享…

win11部署自己的privateGpt(2024-0304)

什么是privateGpt? privategpt开源项目地址 https://github.com/imartinez/privateGPT/tree/main 官方文档 https://docs.privategpt.dev/overview/welcome/welcome PrivateGPT是一个可投入生产的人工智能项目&#xff0c;利用大型语言模型&#xff08;LLMs&#xff09;的…

流行 NFT 的必备指南

​作者&#xff1a;stellafootprint.network 编译&#xff1a;mingfootprint.network 来源&#xff1a;Footprint Analytics Blog 随着爱好者们对 NFT 的兴趣不断高涨&#xff0c;Footprint Analytics 发布了一系列文章&#xff0c;重点介绍各种热门 NFT 系列。这些文章深入…

GBU808-ASEMI整流桥GBU808参数、封装、尺寸

编辑&#xff1a;ll GBU808-ASEMI整流桥GBU808参数、封装、尺寸 型号&#xff1a;GBU808 品牌&#xff1a;ASEMI 封装&#xff1a;GBU-4 最大重复峰值反向电压&#xff1a;800V 最大正向平均整流电流(Vdss)&#xff1a;8A 功率(Pd)&#xff1a;中小功率 芯片个数&#…

【网站项目】075学生信息管理系统

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…