【C++】异常

目录

    • [1. 异常基本概念](#1. 异常基本概念)
      • [1.1 C语言异常处理错误的方式](#1.1 C语言异常处理错误的方式)
      • [1.2 异常处理](#1.2 异常处理)
    • [2. 异常的使用](#2. 异常的使用)
      • [2.1 异常的抛出和捕获](#2.1 异常的抛出和捕获)
      • [2.2 异常的重新抛出](#2.2 异常的重新抛出)
      • [2.3 异常安全](#2.3 异常安全)
      • [2.4 异常规范](#2.4 异常规范)
    • [3. 异常体系](#3. 异常体系)
      • [3.1 C++ 标准库的异常体系](#3.1 C++ 标准库的异常体系)
      • [3.2 自定义异常体系](#3.2 自定义异常体系)
    • [4. 异常的优缺点](#4. 异常的优缺点)

1. 异常基本概念

1.1 C语言异常处理错误的方式

在C语言中, 处理错误的方式主要有两种:

  • 返回错误码 缺陷: 需自行查找对应的错误码;
  • 终止程序 缺陷: 程序直接被终止;

但都有其缺陷, 都只能提供简略的错误信息, 所以在 C++11 中引入了新的处理方案---异常.

1.2 异常处理

异常是一种面向对象的 处理错误的方式, 可以抛出异常的详细信息.

C++11 中新增了三个关键字:

  • try: 监测当前代码区是否存在异常.
  • catch: 当异常被抛出时, 捕获异常.
  • throw: 当出现异常时, 抛出异常.

在 try 块中放置可能出现异常的代码, 由 throw 抛出, catch 捕获, 就是异常处理的大概流程.

所以在 try 块内的代码又被称为保护代码.

cpp 复制代码
int main()
{
    try 
    {
    	// 抛出
        throw /*异常对象*/;
    }
    catch (/*异常对象*/)
    {
        // 异常处理...
    }

    return 0;
}

2. 异常的使用

2.1 异常的抛出和捕获

异常由 throw 关键字抛出, 抛出的的是一个对象, 可以包含异常信息等内容;

并且 catch 只会捕获和参数部分类型相匹配的异常对象.

cpp 复制代码
void func(const void* ptr)
{
    if (!ptr)
    {
        throw "异常: 空指针";
    }
}

int main()
{
    try 
    {
        func(nullptr);
    }
    catch (const char* s)
    {
        cout << s << endl;
    }
    
    return 0;
}

若没有异常, 那么会直接跳过 catch 块, 执行之后的代码.

cpp 复制代码
void func(const void* ptr)
{
    if (!ptr)
    {
        throw (string("异常: 空指针"));
    }
}

int main()
{
    try 
    {
        func("nullptr");
    }
    catch (const char* s)
    {
        cout << s << endl;
    }

    return 0;
}

若不匹配, 则不会捕获; 若异常没有被捕获, 程序将会异常终止.

cpp 复制代码
void func(const void* ptr)
{
    if (!ptr)
    {
        throw (string("异常: 空指针"));
    }
}

int main()
{
    try 
    {
        func(nullptr);
    }
    catch (const char* s)
    {
        cout << s << endl;
    }

    return 0;
}

所以一个 try 块 可以对应多个 catch, 具体看 throw 的对象类型.

cpp 复制代码
void func(const void* ptr)
{
    if (!ptr)
    {
        throw (string("异常: 空指针"));
    }
}

int main()
{
    try 
    {
        func(nullptr);
    }
    catch (const char* s)
    {
        cout << s << endl;
    }
    catch (const string& s)
    {
        cout << s << endl;
    }

    return 0;
}

并且一个 try 块 不可以有多个相同类型的 catch.


当存在多个 try | catch 块时, throw 抛出会根据栈帧顺序, 被最近的, 类型匹配的 catch 块捕获;

并且跳过的栈帧会被自动清理.

catch 块只能进入一次, 异常被捕获后, 无法再进入其他 catch 块.

cpp 复制代码
void func2(const void* ptr)
{
    if (!ptr)
    {
        throw (string { "异常: 空指针" });
    }
}

void func1()
{
    try
    {
        func2(nullptr);
    }
    catch (const string& s)
    {
        cout << "func1: " << s << endl;
    }
}

int main()
{
    try 
    {
        func1();
    }
    catch (const string& s)
    {
        cout << "main: " << s << endl;
    }

    return 0;
}

throw 抛出的异常对象处理类似函数的传值返回, 都是局部对象.


catch(...) 可以捕获任意类型的异常, 类似一个保底, 避免程序因异常无法捕获而终止.

cpp 复制代码
void func(const void* ptr)
{
    if (!ptr)
    {
        string s("异常: 空指针");
        throw s;
    }
}

int main()
{
    try 
    {
        func(nullptr);
    }
    catch (...)
    {
        cout << "未知异常"<< endl;
    }

    return 0;
}

异常捕获支持继承和多态, 可以使用基类对象捕获 抛出的派生类对象

通常被应用于自定义的异常体系, 规范异常的行为, 可以统一进行异常处理.

例:

cpp 复制代码
// 基类
class Exception
{
public:
    Exception(const string& errmsg, int errcode)
        : _errmsg(errmsg), _errno(errcode)
    {}

    virtual string what() const
    {
        return to_string(_errno) + " : " + _errmsg;
    }

protected:
	string _errmsg;
	int _errno = 0;
};

class SqlException : public Exception
{
public:
	SqlException(const string& errmsg, int id, const string& sql)
		:Exception(errmsg, id)
		, _sql(sql)
	{}

	virtual string what() const
	{
		string str = "SqlException:";
		str += _errmsg;
		str += "->";
		str += _sql;
		return str;
	}
private:
	const string _sql;
};

class CacheException : public Exception
{
public:
	CacheException(const string& errmsg, int id)
		:Exception(errmsg, id)
	{}

	virtual string what() const
	{
		string str = "CacheException:";
		str += _errmsg;
		return str;
	}
};

class HttpServerException : public Exception
{
public:
	HttpServerException(const string& errmsg, int id, const string& type)
		:Exception(errmsg, id)
		, _type(type)
	{}

	virtual string what() const
	{
		string str = "HttpServerException:";
		str += _type;
		str += ":";
		str += _errmsg;
		return str;
	}
private:
	const string _type;
};

void SQLMgr()
{
	if (rand() % 6 == 0)
	{
		throw SqlException("权限不足", 100, "select * from name = '张三'");
	}

	cout << "!!!!!!!!!!!!!!!!!!!!!!!!运行成功" << endl;
}

void CacheMgr()
{
	if (rand() % 4 == 0)
	{
		throw CacheException("权限不足", 100);
	}
	else if (rand() % 5 == 0)
	{
		throw CacheException("数据不存在", 101);
	}
	SQLMgr();
}

void HttpServe()
{
	if (rand() % 2 == 0)
	{
		throw HttpServerException("请求资源不存在", 100, "get");
	}
	else if (rand() % 3 == 0)
	{
		throw HttpServerException("权限不足", 101, "post");
	}

	CacheMgr();
}
int main()
{
	srand(time(0));
	int i = 10;
	while (i--)
	{
		try
		{
			HttpServe();
		}
		catch (const Exception& e)
		{
			// 异常处理
			cout << e.what() << endl << "===================" << endl;
		}
		catch (...)
		{
			cout << "未知异常" << endl;
		}
	}

	return 0;
}

2.2 异常的重新抛出

由于异常抛出会直接跳转至 catch 中, 导致部分代码为执行, 可能会导致内存泄漏.

cpp 复制代码
int main()
{
    try
    {
        int* i = new int;

        //func(nullptr);

        cout << "delete" << endl;
        delete i;
    }
    catch (const string& s)
    {
        cout << s << endl;
    }

    return 0;
}

那么就需要重新抛出, 在可能出现异常, 且影响空间释放的的位置, 主动捕获, 释放空间后, 再次抛出.

并且异常捕获一般由最外层统一处理.

cpp 复制代码
int main()
{
    try
    {
        int* i = new int;

        try
        {
            func(nullptr);
        }
        catch (const string& s)
        {
            cout << "delete" << endl;
            delete i;
            throw s;
        }

        cout << "delete" << endl;
        delete i;
        // 其他操作...
    }
    catch (const string& s)
    {
        cout << s << endl;
    }

    return 0;
}

2.3 异常安全

  • 尽量不要在构造函数中抛出异常, 可能导致对象不完整或没有完全初始化;
  • 尽量不要在析构函数内抛出异常, 可能导致资源泄漏;
  • 在使用 malloc/free, new/delete, fopen/fclose, lock/unlock 等资源管理配套函数时, 需注意资源泄漏或死锁等问题.

2.4 异常规范

C++98 标准规定:

  • 可以在函数的后面接 throw(type1, type2, type3 ...), 表示此函数可能抛出的所有异常类型;
cpp 复制代码
void func() throw(/*type*/, /*type*/, /*type*/) // 可能抛出三种中的一种
  • 若函数的后面接 throw(), 表示函数不抛出异常, 但也可以抛出;
cpp 复制代码
void func() throw()	// 函数不抛出异常
  • 若无异常接口声明, 则此函数可以抛出任何类型的异常.
cpp 复制代码
void func()	// 可以抛出任意类型的异常

但 非强制性规定.

C++11 中也更新了一个异常规范:

  • 若函数的后面接 noexcept 关键字, 也表示函数不抛出异常.
cpp 复制代码
void func() noexcept  // 函数不抛出异常

但若函数或内部的调用仍会抛出异常, 那么程序将直接终止.

cpp 复制代码
void func1(const void* ptr) // noexcept 这里和下面结果一样, 并且这里有 throw, 编译器会报 warning 
{
    if (!ptr)
    {
        string s("异常: 空指针");
        throw s;
    }
}


void func(const void* ptr) noexcept
{
    func1(nullptr);
}
int main()
{
    try
    {
        func(nullptr);
    }
    catch (const string& s)
    {
        cout << s << endl;
    }

    return 0;
}

3. 异常体系

3.1 C++ 标准库的异常体系

C++ 标准库中提供了一套 异常体系, 可以直接使用或继承 std::exception 基类, 实现其他异常处理.

3.2 自定义异常体系

由于 C++ 标准库的异常体系并不能满足需求, 所以可以根据需求自定义异常体系.

4. 异常的优缺点

优点:

  • 可以清晰准确的展示出错误的各种信息, 更好的定位程序 Bug;
  • 比起错误码, 异常可以直接被捕获;
  • 为了兼容第三方库, 很多第三方库包含了异常, 比如 boost, gtest, gmock;
  • 部分函数使用异常更好处理, 比如 越界访问等.

缺点:

  • 会导致执行流跨度过大,并且非常混乱, 导致部分调试比较困难.
  • 异常有一些性能上的开销, 但现在基本可以忽略不计.
  • C++ 没有垃圾回收机制, 自行管理资源, 易导致内存泄漏, 死锁等问题, 需要使用 RAII 来处理资源管理问题.
  • C++ 标准库的异常体系定义不够好, 导致出现了各种异常体系, 比较混乱.

异常尽量规范使用:

  • 抛出异常类型都继承自一个基类;
  • 函数是否抛异常可以使用 throw() 注明.
相关推荐
ragnwang1 小时前
C++ Eigen常见的高级用法 [学习笔记]
c++·笔记·学习
lqqjuly4 小时前
特殊的“Undefined Reference xxx“编译错误
c语言·c++
冰红茶兑滴水5 小时前
云备份项目--工具类编写
linux·c++
刘好念5 小时前
[OpenGL]使用 Compute Shader 实现矩阵点乘
c++·计算机图形学·opengl·glsl
酒鬼猿5 小时前
C++进阶(二)--面向对象--继承
java·开发语言·c++
姚先生976 小时前
LeetCode 209. 长度最小的子数组 (C++实现)
c++·算法·leetcode
小王爱吃月亮糖6 小时前
QT开发【常用控件1】-Layouts & Spacers
开发语言·前端·c++·qt·visual studio
aworkholic7 小时前
opencv sdk for java中提示无stiching模块接口的问题
java·c++·opencv·jni·opencv4android·stiching
程序员老冯头7 小时前
第十六章 C++ 字符串
开发语言·c++
Xenia2237 小时前
复习篇~第二章程序设计基础
c++·算法