C++语法(25)--- 异常与智能指针

C++语法(24) C++11_哈里沃克的博客-CSDN博客https://blog.csdn.net/m0_63488627/article/details/131054426?spm=1001.2014.3001.5501

目录

1.异常

异常的抛出和匹配原则

在函数调用链中异常栈展开匹配原则

2.智能指针

1.引子

1.危害

2.初次尝试解决

3.当前的缺陷(悬空)

2.auto_ptr

1.std::auto_ptr

2.模拟实现auto_ptr

3.unique_ptr

1.std::unique_ptr

2.模拟实现std::unique_ptr

3.shared_ptr

1.std::shared_ptr

2.模拟实现shared_ptr(引用计数)

3.问题

4.加锁

5.分析

6.循环引用问题

4.weak_ptr

模拟实现

3.定制删除器

C++库实现

模拟

缺点

使用仿函数


1.异常

try
{
// 保护的标识代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}

对于抛异常这个动作,其实是为了不让代码中断。要知道所谓的 

异常的抛出和匹配原则

1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
4. catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。
5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获,这个在实际中非常实用

在函数调用链中异常栈展开匹配原则

1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则
调到catch的地方进行处理。
2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行

2.智能指针

1.引子

1.危害

1.当我们知道各种库函数都有抛异常的习惯后,就能知道其实new和delete也会出现所谓的异常抛出。但是此时的异常捕获会变的非常不容易。

2.当出现好几个连续的new时,我们还得i嵌套式的捕获异常很多层。这是因为当出现连续多个new时,我们不能保证每一个都能正常运行。如果是一个new 无所谓,出错直接抛出异常即可;但是面对两个时,第一个没有异常,等到第二个时出现异常,那么第二个理所当然不会new成功的,但是第一个的空间是已经开辟了,那么如果不嵌套式的delete空间,我们会出现内存泄漏。那如果是一片的new没有被delete,那么就会出现大片的内存泄露。

2.初次尝试解决

那么我们设计一个类型,该类型就用来存储对应new出来的空间。只要new开辟了空间,类就生成对象进行管理,那么我们通过对类的构造函数和析构函数的编写,使得资源的生命周期同对象的生命周期一致。

template<class T>
class SmartPtr
{
public:
	//RAII
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		delete _ptr;
	}

	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}

private:
	T* _ptr;
};

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

像指针一样:保持了我们直接可以拿这个对象作为指针来使用。

3.当前的缺陷(悬空)

悬空:此时如果使用拷贝构造或者赋值拷贝时,新旧指针同时指向一片空间。也就意味着两个对象同时指向一个,那么此时如果对象的生命周期结束了,也就意味着该空间被释放两次,这在编译层面是不被允许的,所以此时需要改进。

2.auto_ptr

1.std::auto_ptr

C++98中就存在一个这样类似的智能指针

不过此时的智能指针存在一个问题:

1.作为用户的视角,智能指针也应该和指针一样,但我们同时拥有两个指针指向一个地址时,两个指针都应该能够使用

2.但是auto_ptr不同,它的实现是,一旦有新的智能指针赋值老的智能指针,老的智能指针就会被置空。

3.其实这样的设计会十分鸡肋。一来调换了个对象的名字,而没有将其实现的像真指针一样多个指针指向同一片空间;二来用户使用是可能会不知道老的智能指针已经被置空,而对智能指针进行操作,那么此时就会出现空指针的访问。这一点语法上面不被允许

#include<memory>
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;
}

2.模拟实现auto_ptr

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

		~auto_ptr()
		{
			delete _ptr;
		}

		auto_ptr(const auto_ptr<T>& sp)
			:_ptr(sp._ptr)
		{
			sp._ptr = nullptr;
		}
		
		//像指针一样
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		T& operator[](size_t pos)
		{
			return _ptr[pos];
		}
	private:
		T* _ptr;
	};

该智能指针最好不要使用,由于其过于不合理

3.unique_ptr

1.std::unique_ptr

针对auto_ptr的漏洞,unique_ptr的解决方案就是将指针设置为唯一,不允许智能指针拷贝构造。

int mian()
{
	unique_ptr<int>up1(new int);
	unique_ptr<int>up2(up1);
	return 0;
}

2.模拟实现std::unique_ptr

其实之前就已经说明过不想使得默认函数生成的方法:

1.针对C++98就是将函数写到私有里:

在内部,默认函数可能还可以被间接使用,还是有风险的

private:
	unique_ptr(const unique_ptr<T>& sp)

2.指针C++11就是在函数后面加上 = delete表示不会生成默认函数:

unique_ptr(const unique_ptr<T>& sp) = delete;

3.shared_ptr

1.std::shared_ptr

该智能指针就符合我们之前觉得应该接近指针的实现,他能共享指针。

int mian()
{
	shared_ptr<int>up1(new int);
	shared_ptr<int>up2(up1);
	return 0;
}

不会出现悬空问题

2.模拟实现shared_ptr(引用计数)

如何引用计数也是一个问题,我们提出几种方案进行比较

1.在类的私有中追加一个计数

private:
	T* _ptr;
	int count;

这样是不可以的,首先我们需要清楚如果两个指针指向同一个位置,那么我们势必需要一个空间让两个对象同时看到。但是如果我们这样写,那么一个对象就有一个count,这样就十分荒唐了,我们到底的计数会十分混乱

2.在类的私有中追加静态计数

private:
	T* _ptr;
	static int count;

要知道,static在类中使用,那么整个类都共享了。如果我们指向的指针是不同的空间,但是得到的计数却是共享的,那么也就无法分辨什么时候释放哪个指针指向的空间了。

3.引用计数

private:
	T* _ptr;
	int* _pcount;

传入指针

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

		~shared_ptr()
		{
			release();
		}

		void release()
		{
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			
			++(*_pcount);
		}
		
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			
			if(sp._ptr!=_ptr)
			{
				release();
				_ptr = sp._ptr;
				_pcount=(sp._pcount);
				++(*_pcount);
			}
			return *this;
		}

        int use_count()
		{
			return *_pcount;
		}

		//像指针一样
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		T& operator[](size_t pos)
		{
			return _ptr[pos];
		}
	private:
		T* _ptr;
		int* _pcount;
	};

3.问题

由于智能指针涉及到公共资源,难免会在多线程下使用,那么此时就会出现线程安全问题。

	void test_share_ptr()
	{
		shared_ptr<int> sp1(new int(1));

		thread t1([&]()
		{
				for (int i = 0; i < 10000; i++)
				{
					shared_ptr<int> sp2(sp1);
				}
		});

		thread t2([&]()
		{
				for (int i = 0; i < 10000; i++)
				{
					shared_ptr<int> sp3(sp1);
				}
		});

		t1.join();
		t2.join();

		cout << sp1.use_count() << endl;
	}

4.加锁

1.此时智能指针的设计就需要加锁来保护线程安全

2.不过,对于我们而言,放在类的对象中的锁这个做法是行不通的,因为这样构造出来的指针明明指向的是同一个结构,但是所谓的锁不是同一把锁,那么就算是加锁这个操作也是没有意义的

3.基于2的问题,我们需要的是不同对象拥有同一把锁,那么做法其实加入计数器的做法一样,都传入的是其指针

shared_ptr(T* ptr)
	:_ptr(ptr)
	,_pcount(new int(1))
	,_pmtx(new mutex)
{}

void release()
{
	bool flag = false;
	_pmtx->lock();
	if (--(*_pcount) == 0)
	{
		delete _ptr;
		delete _pcount;
		flag = true;
	}
	_pmtx->unlock();
	if (flag)
	{
		delete _pmtx;
	}
}

shared_ptr(const shared_ptr<T>& sp)
	:_ptr(sp._ptr)
	, _pcount(sp._pcount)
	, _pmtx(sp._pmtx)
{
	_pmtx->lock();
	++(*_pcount);
	_pmtx->unlock();
}

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
			
	if(sp._ptr!=_ptr)
	{
		release();
		_ptr = sp._ptr;
		_pcount=(sp._pcount);
		_pmtx->lock();
		++(*_pcount);
		_pmtx->unlock();
	}
	return *this;
}

5.分析

1.之所以需要将计数器加锁是因为其开辟在堆区,堆区对于所有线程都共享,也就是说malloc得到的所有东西和free释放的东西都有风险。

2.但是release中判断变量flag并不需要被加锁,因为其实它是临时变量,临时变量存储在栈上,也就意味着所有的线程都有自己的独立栈结构来存储这一临时变量,所以不需要被保护

3.这样设计过的智能指针本身是线程安全的,它的安全代表着引用计数的加减操作是线程安全的,但是不代表智能指针指向的数据是线程安全的。这些数据并不是在智能指针内部执行的,所以这样的设计是无法保证数据线程安全的。需要在外面进行自行加锁使得线程安全。

6.循环引用问题

struct ListNode
{
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;
};

void test_share_ptr2()
{
	std::shared_ptr<ListNode> n1(new ListNode);
	std::shared_ptr<ListNode> n2(new ListNode);

	n1->_next = n2;
	n2->_prev = n1;
    cout << n1.use_count() << " " << n2.use_count() << endl;
}

1.创建两个ListNode的智能指针是为了让其能自动释放内存,但是此时依然出现了内存泄露的问题

2.之所以出现是因为:当前n1指针和n2指针其实是临时变量,出了作用域就会调用析构函数销毁,但是由于其是智能指针管理的,所以当前的问题在于n1的next和n2的prev。n1的next指向n2,那么n2的彻底销毁需要n1的销毁;n2的prev指向n1,那么n1的彻底销毁需要n2的销毁。就出现了内存泄漏的问题。

3.我们最终得到的结果证明了,确实两个智能指针的count都为2

4.weak_ptr

weak_ptr:可以指向资源,也可以访问资源;但是它不增加引用计数

struct ListNode
{
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;
};

void test_share_ptr2()
{
	std::shared_ptr<ListNode> n1(new ListNode);
	std::shared_ptr<ListNode> n2(new ListNode);
	n1->_next = n2;
	n2->_prev = n1;
	cout << n1.use_count() << " " << n2.use_count() << endl;
}

得到的结果:n1和n2的count都是1,表面二者的计数器只计数了一次

模拟实现

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

	public:
		T* _ptr;
	};

3.定制删除器

C++库实现

默认的删除其实只是针对指针,但是如果我们构造的智能指针指向一个特定的结构体,也就意味着我们不能直接删除掉智能指针就一了百了。所以诞生了定制删除器,在库中的智能指针在构造时会传入所谓的删除器,随后传入内部自动删除指定结构体的内存,防止内存泄漏。

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

int main()
{
	//MY::test_share_ptr1();
	//MY::test_share_ptr2();
	std::shared_ptr<int> sp1(new int[10], DeleteArray<int>());
	std::shared_ptr<string> sp2(new string[10], DeleteArray<string>());
	std::shared_ptr<string> sp3(new string[10], [](string* ptr) {delete[] ptr; });
	std::shared_ptr<FILE> sp4(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });
	return 0;
}

模拟

	template<class T>
	class default_delete
	{
	public:
		void operator()(T* ptr)
		{
			delete ptr;
		}
	};

	template<class T, class D = default_delete<T>>
	class shared_ptr
	{
	public:
		void release()
		{
			bool flag = false;
			_pmtx->lock();
			if (--(*_pcount) == 0)
			{
				//delete _ptr;
				_del(_ptr);
				delete _pcount;
				flag = true;
			}
			_pmtx->unlock();
			if (flag)
			{
				delete _pmtx;
			}
		}

	private:
		T* _ptr;
		int* _pcount;
		mutex* _pmtx;

		D _del;
	};

缺点

MY::shared_ptr < FILE, decltype([](FILE* ptr) {fclose(ptr); }) > n2(fopen("Text.cpp", "r"));

我们设计的模板传入的是函数,但是所谓的lambda出来的是函数类型,类型和函数不是同一个性质的东西,所以无法运行。

使用仿函数

template<class T>
struct Fclose
{
	void operator()(const T* ptr)
	{
		fclose(ptr);
	}
};

MY::shared_ptr < FILE, Fclose<FILE>> n2(fopen("Text.cpp", "r"));
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哈里沃克

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值