• 【C++】异常 & 智能指针


    1.C++异常

    C++异常是一种处理错误的方式。当一个函数遇到自己无法处理的错误时就可以抛出异常,让该函数的直接或间接调用者通过捕获这个异常来处理错误。
    一些异常关键字的介绍:

    • throw:程序是通过throw关键字来抛出异常的
    • catch:异常要通过catch关键字来进行捕获
    • trytry{}中的程序可能会抛出异常
    // 异常捕获的基本语法如下
    try
    {
    	// ...
    }
    catch(Exception_1 e)
    {
    	// ...
    }
    catch(Exception_2 e)
    {
    	// ...
    }
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    1.1.异常的抛出与捕获

    异常是以对象形式抛出的,当throwtry{}里时,异常抛出,会首先查找匹配的catch。而且该对象的类型决定了由哪个catch进行捕获处理。而被选中的catch处理程序是与对象类型匹配且离异常抛出位置最近的一个。
    如果没有匹配的catch则会跳出当前函数栈,在调用该函数的栈中查找匹配的catch。如果直到main函数栈帧都没有匹配的catch,程序则会终止。
    异常对象可能是一个临时对象,在被抛出时出作用域会被销毁,所以异常对象的抛出会生成一个拷贝被catch捕获。
    catch(...)语句可以捕获任意类型的对象,但是无法确定捕获的错误异常类型是什么。实际中通常会最后再加上一个catch(...),防止程序直接终止。匹配合适的catch处理后,会继续沿着catch语句后面的程序执行。
    抛异常时可以拋任意类型的对象,捕获时要求类型匹配。但实际中,抛出的异常对象类型并不一定要求和catch类型完全匹配。因为可以抛出派生类对象,使用基类进行捕获。

    异常有时候也会被重新抛出,当前catch不能完全处理这样一个异常,在完成一些校正处理后,通过throw重新抛出给更上层的catch处理。

    class Exception
    {
    public:
    	Exception(const string& errmsg, int errid)
    		: _errmsg(errmsg)
    		, _errid(errid)
    	{}
    
    	virtual string what() const
    	{
    		return _errmsg;
    	}
    
    	virtual int get() const
    	{
    		return _errid;
    	}
    protected:
    	string _errmsg;
    	int _errid;
    };
    
    class HttpServerException : public Exception
    {
    public:
    	HttpServerException(const string& errmsg, int id, const string& type)
    		: Exception(errmsg, id)
    		, _type(type)
    	{}
    	virtual string what() const
    	{
    		return "HttpServerException:" + _type + ":" + _errmsg;
    	}
    protected:
    	string _type;
    };
    
    void SendMsg(const string& str)
    {
    	srand((unsigned int)time(0));
    	if (rand() % 3 == 0)
    	{
    		throw HttpServerException("网络错误", 333, "get");
    	}
    	else if (rand() % 4 == 0)
    	{
    		throw HttpServerException("权限不足", 444, "post");
    	}
    
    	cout << str << "发送成功" << endl;
    }
    
    void Server()
    {
    	string str = "hello world";
    
    	// 出现网络错误,重试3次
    	int count = 3;
    	while (count--)
    	{
    		try
    		{
    			SendMsg(str);
    
    			// 程序走到此处,说明没有发生异常
    			break;
    		}
    		catch (const Exception& e)
    		{
    			if (e.get() == 333 && count > 0)
    			{
    				continue;
    			}
    			else
    			{
    				throw e; // 异常重新抛出
    			}
    		}
    	}
    }
    
    void Test1()
    {
    	while (true)
    	{
    		try
    		{
    			Server();
    		}
    		catch (const Exception& e)
    		{
    			// 多态
    			cout << e.what() << endl;
    		}
    		catch (...)
    		{
    			cout << "Unkown Exception" << endl;
    		}
    		Sleep(1000);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101

    1.2.异常体系

    C++本身是提供了一系列标准的异常的,这些异常是以父子类层次结构体系组织起来的,我们可以在程序中直接使用这些标准的异常,抛各种子类对象的异常,用一个父类对象进行接收。
    在这里插入图片描述
    我们也可以去继承execption类来实现自己的异常类。但实际中很多公司会定义一套自己的异常继承体系,一方面是为了规范的异常管理,另一方面C++标准异常还是不够好用。

    1.3.异常安全与规范

    可以在一个函数的后面接 throw(type...),列出这个函数可能抛出的异常类型有哪些。
    如果函数后面接的是throw(),表示函数不抛异常。C++11新增了noexcept,用于表示不会抛异常。

    构造函数中最好不要抛异常,否则可能导致对象构造不完整。
    析构函数中最好不要抛异常,否则可能导致资源泄露。

    1.4.异常优缺点

    C语言处理错误的方式有两种:

    • assert终止程序。
    • 返回错误码errno

    实际中C语言基本都是使用错误码的方式处理错误,有时候会使用终止程序的方式来处理非常严重的错误。
    C++异常的优势主要是相对C语言而言的。

    • 异常对象相比错误码的方式可以展示出错误的各种信息,来帮助更好的定位程序的bug。
    • 错误码的使用有一个很大的问题就是,在函数调用层次中,深层的函数返回错误得层层返回。而异常可以直接跳到catch的地方。

    异常也有缺点。

    • 异常可能导致程序的执行流乱跳,使得跟踪调试以及分析程序时更麻烦。
    • 异常很容易导致内存泄漏、死锁等安全问题。但这个基本可以通过RAII处理。

    但异常总体而言利大于弊,另外面向对象的语言基本都是使用异常处理错误的,这也是大势所趋。

    2.智能指针

    int div()
    {
    	int a, b;
    	cin >> a >> b;
    	if (b == 0)
    		throw invalid_argument("Division by zero error");
    	return a / b;
    }
    
    void Func()
    {
    	// 1、如果p1处new 抛异常会如何?
    	// 2、如果p2处new 抛异常会如何?
    	// 3、如果div调用处又抛异常会如何?
    	int* p1 = new int;
    	int* p2 = new int;
    	cout << div() << endl;
    
    	delete p1;
    	delete p2;
    }
    
    void Test2()
    {
    	try
    	{
    		Func();
    	}
    	catch (exception& e)
    	{
    		cout << e.what() << endl;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    通过这个程序我们可以看到,p1抛异常时没什么问题,p1p2都没创建出来;
    p2抛异常时,p1创建出来了,但没有释放,导致内存泄露了;
    div()抛异常时,p1p2都创建出来了,但都没有释放,导致内存泄露了。
    这里简单介绍一下内存泄漏的知识。
    内存泄漏是指因为人为的疏忽或错误,而造成的程序未能释放已经不再使用的内存的情况。
    内存泄漏并不是物理上内存的消失,而是由于程序失去了对该段内存的控制管理,而造成的内存的浪费。
    长期运行的程序如果存在内存泄漏,会导致服务响应越来越慢,直至最终卡死。
    C/C++程序一般关注两种方式的内存泄漏:堆内存泄露 和 系统资源泄露。
    而内存泄漏的解决方式也分为两种:

    • 事前预防型,如智能指针的使用。
    • 事后差错型,如使用内存泄漏工具进行检测。

    2.1.RAII

    RAII(Resource Acquisition Is Initialization) - 资源获得即初始化。
    RAII是一种通过利用对象生命周期来控制程序资源的简单技术。
    在对象构造时获取资源,使控制的资源在对象生命周期内始终保持有效,最后在对象析构时释放资源。
    这样做就不需要显式地释放资源,而且对象所需的资源在其生命周期内始终保持有效。

    template<class T>
    class SmartPtr
    {
    public:
    	SmartPtr(T* ptr)
    		: _ptr(ptr)
    	{}
    
    	~SmartPtr()
    	{
    		cout << "~SmartPtr()" << endl;
    		delete _ptr;
    	}
    private:
    	T* _ptr;
    };
    
    int div()
    {
    	int a, b;
    	cin >> a >> b;
    	if (b == 0)
    		throw invalid_argument("Division by zero error");
    	return a / b;
    }
    
    void Func()
    {
    	int* p1 = new int;
    	SmartPtr<int> sp1(p1);
    	int* p2 = new int;
    	SmartPtr<int> sp2(p2);
    
    	cout << div() << endl;
    }
    
    void Test3()
    {
    	try
    	{
    		Func();
    	}
    	catch (exception& e)
    	{
    		cout << e.what() << endl;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    在这里插入图片描述
    这里有了RAII的设计,就不用担心p1p2的内存泄漏问题了。

    2.2.智能指针的使用及原理

    像上面的SmartPtr还不能称其为智能指针,因为它还不具备指针的行为。因此还需要重载*->,让其像指针一样去使用。

    T& operator*()
    {
    	return *_ptr;
    }
    
    T* operator->()
    {
    	return _ptr;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.2.1.auto_ptr

    C++98库中就提供了auto_ptr智能指针。其实现原理就是将资源管理权进行转移。

    // 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<T>& operator=(auto_ptr<T>& ap)
    	{
    		if (this != &ap)
    		{
    			// 被赋值对象需要先被清理
    			delete _ptr;
    
    			_ptr = ap._ptr;
    			ap._ptr = nullptr;
    		}
    		
    		return *this;
    	}
    
    	~auto_ptr()
    	{
    		delete _ptr;
    	}
    
    	T& operator*()
    	{
    		return *_ptr;
    	}
    
    	T* operator->()
    	{
    		return _ptr;
    	}
    private:
    	T* _ptr;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    auto_ptr可以说是一个失败的设计, 所以能不用就不用。

    2.2.2.unique_ptr

    unique_ptr的实现就是简单粗暴的防拷贝。

    // unique_ptr 简单模拟实现
    template<class T>
    class unique_ptr
    {
    public:
    	unique_ptr(T* ptr = nullptr)
    		: _ptr(ptr)
    	{}
    
    	// 防拷贝
    	unique_ptr(const unique_ptr<T>& up) = delete;
    	unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
    
    	~unique_ptr()
    	{
    		delete _ptr;
    	}
    
    	T& operator*()
    	{
    		return *_ptr;
    	}
    
    	T* operator->()
    	{
    		return _ptr;
    	}
    private:
    	T* _ptr;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    2.2.3.shared_ptr

    shared_ptr支持拷贝。
    shared_ptr是通过引用计数的方式来支持拷贝,支持多个shared_ptr对象共享资源的。
    shared_ptr中,为每份资源都维护着一份计数。当一个shared_ptr对象被析构时,引用计数就减一,直到引用计数减为0,才释放资源。

    // shared_ptr 简单模拟实现
    template<class T>
    class shared_ptr
    {
    public:
    	// 构造 引用计数初始化为1
    	shared_ptr(T* ptr = nullptr)
    		: _ptr(ptr)
    		, _pCount(new int(1))
    	{}
    	
    	// 拷贝 ++引用计数
    	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)
    		{
    			// 需要先处理好当前资源的释放
    			Release();
    
    			_ptr = sp._ptr;
    			_pCount = sp._pCount;
    			++(*_pCount);
    		}
    		return *this;
    	}
    
    	void Release()
    	{
    		if (--(*_pCount) == 0)
    		{
    			cout << "void Release()" << endl;
    			delete _ptr;
    			delete _pCount;
    		}
    	}
    	
    	// 直到引用计数减为0才彻底释放资源
    	~shared_ptr()
    	{
    		Release();
    	}
    
    	T& operator*()
    	{
    		return *_ptr;
    	}
    
    	T* operator->()
    	{
    		return _ptr;
    	}
    
    	int use_count()
    	{
    		return *_pCount;
    	}
    
    	T* get()
    	{
    		return _ptr;
    	}
    private:
    	T* _ptr;
    	int* _pCount; // 一个资源,配一个计数,多个智能指针对象共管
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    shared_ptr引用计数不能采用静态计数。静态成员属于整个类,属于类的所有对象。如果申请了两份相同类型的资源,无法做到区分。

    2.2.4.shared_ptr的循环引用问题 & weak_ptr

    class Node
    {
    public:
    	int _val;
    	std::shared_ptr<Node> _prev;
    	std::shared_ptr<Node> _next;
    
    	~Node()
    	{
    		cout << "~Node()" << endl;
    	}
    };
    
    void Test4()
    {
    	std::shared_ptr<Node> n1(new Node);
    	std::shared_ptr<Node> n2(new Node);
    	cout << n1.use_count() << endl;
    	cout << n2.use_count() << endl;
    	n1->_next = n2;
    	n2->_prev = n1;
    	cout << n1.use_count() << endl;
    	cout << n2.use_count() << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在这里插入图片描述
    上面的程序结束后资源有没有被释放呢?
    结果告诉我们是没有的(如果释放了的话会调用析构打印~Node())。
    那为什么没有释放呢?
    如图,展示了程序中节点的指向情况:
    在这里插入图片描述
    在Test4()结束的时候,n2先析构,n1再析构,此时左边的节点和右边的节点,它们的引用计数都由2减为1;
    此时,由左边节点的_next管着右边的节点,由右边节点的_prev管着左边的节点;
    只要左边节点的_next被析构,右边的节点引用计数减为0,就可以被delete;
    而左边节点的_next要析构,就必须让左边的节点被delete,左边节点的_next作为成员才会被析构;
    而左边的节点要被delete,就必须析构右边节点的_prev,使左边的节点引用计数减为0;
    而右边节点的_prev要析构,就必须让右边的节点被delete,右边节点的_prev作为成员才会被析构;
    而右边的节点要被delete,就必须析构左边节点的_next,使右边的节点引用计数减为0。

    由于左边节点的_next和右边节点的_prev相互牵制着,最后谁也释放不了。这就是循环引用问题。
    weak_ptr可以用于解决循环引用问题,而且weak_ptr正是作为辅助型智能指针,为了配合解决shared_ptr的循环引用问题才被设计出来的。

    // 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(const weak_ptr<T>& wp)
    		: _ptr(wp._ptr)
    	{}
    
    	weak_ptr<T>& operator=(const shared_ptr<T>& sp)
    	{
    		_ptr = sp.get();
    		return *this;
    	}
    
    	weak_ptr<T>& operator=(const weak_ptr<T>& wp)
    	{
    		_ptr = wp._ptr;
    		return *this;
    	}
    
    	T& operator*()
    	{
    		return *_ptr;
    	}
    
    	T* operator->()
    	{
    		return _ptr;
    	}
    private:
    	T* _ptr;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    weak_ptr不是常规的智能指针,它没有RAII,它可以访问和修改资源,但不参与资源释放管理。
    weak_ptr主要是用shared_ptr构造,用来解决shared_ptr的循环引用问题。

    class Node
    {
    public:
    	int _val;
    	std::weak_ptr<Node> _prev;
    	std::weak_ptr<Node> _next;
    
    	~Node()
    	{
    		cout << "~Node()" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述
    智能指针总结
    智能指针的设计主要考虑三点:

    1. 利用RAII思想来管理释放资源
    2. 保持有指针一样的行为
    3. 解决拷贝问题

    2.3.定制删除器

    对于申请的资源需要释放,但对于不同的申请方式,就需要不同的释放方式。定制删除器正是为了使申请与释放的方式进行匹配。

    void Test5()
    {
    	// shared_ptr 支持构造时传删除器 仿函数版
    	std::shared_ptr<Node> n1(new Node[5], DeleteArray<Node>());
    	std::shared_ptr<Node> n2(new Node, Delete<Node>());
    
    	std::shared_ptr<int> n3(new int[5], DeleteArray<int>());
    	std::shared_ptr<int> n4(new int, Delete<int>());
    
    	std::shared_ptr<int> n5((int*)malloc(sizeof 12), Free<int>());
    	
    	// shared_ptr lambda版
    	std::shared_ptr<Node> n6(new Node[5], [](Node* ptr) {cout << "delete[] ptr;" << endl; delete[] ptr; });
    	std::shared_ptr<Node> n7(new Node, [](Node* ptr) {cout << "delete ptr;" << endl; delete ptr; });
    
    	std::shared_ptr<int> n8(new int[5], [](int* ptr) {cout << "delete[] ptr;" << endl; delete[] ptr; });
    	std::shared_ptr<int> n9(new int, [](int* ptr) {cout << "delete ptr;" << endl; delete ptr; });
    
    	std::shared_ptr<int> n10((int*)malloc(sizeof 12), [](int* ptr) {cout << "free(ptr);" << endl; free(ptr); });
    
    	std::shared_ptr<FILE> n11(fopen("test.cpp", "r"), [](FILE* ptr) {cout << "fclose(ptr);" << endl; fclose(ptr); });
    	
    	// unique_ptr 支持类模板传迭代器类型,不支持构造时传迭代器
    	std::unique_ptr<Node, DeleteArray<Node>> n12(new Node[5]);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    在这里插入图片描述

    看到这里,你能回答下列问题了吗?

    1. 为什么需要智能指针?
    2. 什么是RAII
    3. auto_ptr/unique_ptr/shared_ptr/weak_ptr之间的使用区别?
    4. 什么是循环引用?如何解决循环引用?
  • 相关阅读:
    信安软考——第七章 访问控制技术原理与应用
    如何构建一台机器学习服务器
    ZnCdTe/ZnS三元荧光量子点
    基于FPGA的图像自适应阈值二值化算法实现,包括tb测试文件和MATLAB辅助验证
    三大数据库 sequence 之华山论剑 (下篇)
    IDEA小技巧:Markdown里的命令行可以直接运行了
    Golang GMP解读
    SQL触发器
    nodejs+vue旅行社网站系统-计算机毕业设计
    数据结构:AVL树
  • 原文地址:https://blog.csdn.net/weixin_62172209/article/details/134303697