既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
- 逻辑错误是说我们编写的代码思路有问题,不能够达到最终的目标,这种错误可以通过调试来解决。
- 运行时错误是指程序在运行期间发生的错误,例如除数为 0、内存分配失败、数组越界、文件不存在等。C++ 异常(Exception)机制就是为解决运行时错误而引入的。
运行时错误如果放任不管,系统就会执行默认的操作,终止程序运行,也就是我们常说的程序崩溃(Crash)。C++ 提供了异常(Exception)机制,让我们能够捕获运行时错误,给程序一次“起死回生”的机会,或者至少告诉用户发生了什么再终止程序。
【例1】一个发生运行时错误的程序:
#include <iostream>
#include <string>
using namespace std;
int main(){
string str = "http://c.biancheng.net";
char ch1 = str[100]; //下标越界,ch1为垃圾值
cout<<ch1<<endl;
char ch2 = str.at(100); //下标越界,抛出异常
cout<<ch2<<endl;
return 0;
}
运行代码,在控制台输出 ch1 的值后程序崩溃。下面我们来分析一下原因。
at() 是 string 类的一个成员函数,它会根据下标来返回字符串的一个字符。与[ ]
不同,at() 会检查下标是否越界,如果越界就抛出一个异常;而[ ]
不做检查,不管下标是多少都会照常访问。
所谓抛出异常,就是报告一个运行时错误,程序员可以根据错误信息来进一步处理。
上面的代码中,下标 100 显然超出了字符串 str 的长度。由于第 6 行代码不会检查下标越界,虽然有逻辑错误,但是程序能够正常运行。而第 8 行代码则不同,at() 函数检测到下标越界会抛出一个异常,这个异常可以由程序员处理,但是我们在代码中并没有处理,所以系统只能执行默认的操作,也即终止程序执行。
捕获异常
我们可以借助 C++ 异常机制来捕获上面的异常,避免程序崩溃。捕获异常的语法为:
try{
// 可能抛出异常的语句
}catch(exceptionType variable){
// 处理异常的语句
}
try
和catch
都是 C++ 中的关键字,后跟语句块,不能省略{ }
。try 中包含可能会抛出异常的语句,一旦有异常抛出就会被后面的 catch 捕获。从 try 的意思可以看出,它只是“检测”语句块有没有异常,如果没有发生异常,它就“检测”不到。catch 是“抓住”的意思,用来捕获并处理 try 检测到的异常;如果 try 语句块没有检测到异常(没有异常抛出),那么就不会执行 catch 中的语句。
这就好比,catch 告诉 try:你去检测一下程序有没有错误,有错误的话就告诉我,我来处理,没有的话就不要理我!
catch 关键字后面的exceptionType variable
指明了当前 catch 可以处理的异常类型,以及具体的出错信息。我们稍后再对异常类型展开讲解,当务之急是演示一下 try-catch 的用法,先让读者有一个整体上的认识。
【例2】修改上面的代码,加入捕获异常的语句:
#include <iostream>
#include <string>
#include <exception>
using namespace std;
int main(){
string str = "http://c.biancheng.net";
try{
char ch1 = str[100];
cout<<ch1<<endl;
}catch(exception e){
cout<<"[1]out of bound!"<<endl;
}
try{
char ch2 = str.at(100);
cout<<ch2<<endl;
}catch(exception &e){ //exception类位于<exception>头文件中
cout<<"[2]out of bound!"<<endl;
}
return 0;
}
运行结果:
(
[2]out of bound!
可以看出,第一个 try 没有捕获到异常,输出了一个没有意义的字符(垃圾值)。因为[ ]
不会检查下标越界,不会抛出异常,所以即使有错误,try 也检测不到。换句话说,发生异常时必须将异常明确地抛出,try 才能检测到;如果不抛出来,即使有异常 try 也检测不到。所谓抛出异常,就是明确地告诉程序发生了什么错误。
第二个 try 检测到了异常,并交给 catch 处理,执行 catch 中的语句。需要说明的是,异常一旦抛出,会立刻被 try 检测到,并且不会再执行异常点(异常发生位置)后面的语句。本例中抛出异常的位置是第 17 行的 at() 函数,它后面的 cout 语句就不会再被执行,所以看不到它的输出。
说得直接一点,检测到异常后程序的执行流会发生跳转,从异常点跳转到 catch 所在的位置,位于异常点之后的、并且在当前 try 块内的语句就都不会再执行了;即使 catch 语句成功地处理了错误,程序的执行流也不会再回退到异常点,所以这些语句永远都没有执行的机会了。本例中,第 18 行代码就是被跳过的代码。
执行完 catch 块所包含的代码后,程序会继续执行 catch 块后面的代码,就恢复了正常的执行流。
为了演示「不明确地抛出异常就检测不到异常」,大家不妨将第 10 行代码改为char ch1 = str[100000000];
,访问第 100 个字符可能不会发生异常,但是访问第 1 亿个字符肯定会发生异常了,这个异常就是内存访问错误。运行更改后的程序,会发现第 10 行代码产生了异常,导致程序崩溃了,这说明 try-catch 并没有捕获到这个异常。
关于「如何抛出异常」,我们将在下节讲解,这里重点是让大家明白异常的处理流程:
抛出(Throw)–> 检测(Try) --> 捕获(Catch)
发生异常的位置
异常可以发生在当前的 try 块中,也可以发生在 try 块所调用的某个函数中,或者是所调用的函数又调用了另外的一个函数,这个另外的函数中发生了异常。这些异常,都可以被 try 检测到。
- 下面的例子演示了 try 块中直接发生的异常:
#include <iostream>
#include <string>
#include <exception>
using namespace std;
int main(){
try{
throw "Unknown Exception"; //抛出异常
cout<<"This statement will not be executed."<<endl;
}catch(const char* &e){
cout<<e<<endl;
}
return 0;
}
运行结果:
Unknown Exception
throw
关键字用来抛出一个异常,这个异常会被 try 检测到,进而被 catch 捕获。关于 throw 的用法,我们将在下节深入讲解,这里大家只需要知道,在 try 块中直接抛出的异常会被 try 检测到。
- 下面的例子演示了 try 块中调用的某个函数中发生了异常:
#include <iostream>
#include <string>
#include <exception>
using namespace std;
void func(){
throw "Unknown Exception"; //抛出异常
cout<<"[1]This statement will not be executed."<<endl;
}
int main(){
try{
func();
cout<<"[2]This statement will not be executed."<<endl;
}catch(const char* &e){
cout<<e<<endl;
}
return 0;
}
运行结果:
Unknown Exception
func() 在 try 块中被调用,它抛出的异常会被 try 检测到,进而被 catch 捕获。从运行结果可以看出,func() 中的 cout 和 try 中的 cout 都没有被执行。
- try 块中调用了某个函数,该函数又调用了另外的一个函数,这个另外的函数抛出了异常:
#include <iostream>
#include <string>
#include <exception>
using namespace std;
void func_inner(){
throw "Unknown Exception"; //抛出异常
cout<<"[1]This statement will not be executed."<<endl;
}
void func_outer(){
func_inner();
cout<<"[2]This statement will not be executed."<<endl;
}
int main(){
try{
func_outer();
cout<<"[3]This statement will not be executed."<<endl;
}catch(const char* &e){
cout<<e<<endl;
}
return 0;
}
运行结果:
Unknown Exception
发生异常后,程序的执行流会沿着函数的调用链往前回退,直到遇见 try 才停止。在这个回退过程中,调用链中剩下的代码(所有函数中未被执行的代码)都会被跳过,没有执行的机会了。
C++异常类型以及多级catch匹配
首先来回顾一下上节讲到的 try-catch 的用法:
try{
// 可能抛出异常的语句
}catch(exceptionType variable){
// 处理异常的语句
}
我们还遗留下一个问题,就是 catch 关键字后边的exceptionType variable
,这节就来详细分析一下。
exceptionType
是异常类型,它指明了当前的 catch 可以处理什么类型的异常;variable
是一个变量,用来接收异常信息。当程序抛出异常时,会创建一份数据,这份数据包含了错误信息,程序员可以根据这些信息来判断到底出了什么问题,接下来怎么处理。
异常既然是一份数据,那么就应该有数据类型。C++ 规定,异常类型可以是 int、char、float、bool 等基本类型,也可以是指针、数组、字符串、结构体、类等聚合类型。C++ 语言本身以及标准库中的函数抛出的异常,都是 exception 类或其子类的异常。也就是说,抛出异常时,会创建一个 exception 类或其子类的对象。
exceptionType variable
和函数的形参非常类似,当异常发生后,会将异常数据传递给 variable 这个变量,这和函数传参的过程类似。当然,只有跟 exceptionType 类型匹配的异常数据才会被传递给 variable,否则 catch 不会接收这份异常数据,也不会执行 catch 块中的语句。换句话说,catch 不会处理当前的异常。
我们可以将 catch 看做一个没有返回值的函数,当异常发生后 catch 会被调用,并且会接收实参(异常数据)。
但是 catch 和真正的函数调用又有区别:
- 真正的函数调用,形参和实参的类型必须要匹配,或者可以自动转换,否则在编译阶段就报错了。
- 而对于 catch,异常是在运行阶段产生的,它可以是任何类型,没法提前预测,所以不能在编译阶段判断类型是否正确,只能等到程序运行后,真的抛出异常了,再将异常类型和 catch 能处理的类型进行匹配,匹配成功的话就“调用”当前的 catch,否则就忽略当前的 catch。
总起来说,catch 和真正的函数调用相比,多了一个「在运行阶段将实参和形参匹配」的过程。
另外需要注意的是,如果不希望 catch 处理异常数据,也可以将 variable 省略掉,也即写作:
try{
// 可能抛出异常的语句
}catch(exceptionType){
// 处理异常的语句
}
这样只会将异常类型和 catch 所能处理的类型进行匹配,不会传递异常数据了。
多级 catch
前面的例子中,一个 try 对应一个 catch,这只是最简单的形式。其实,一个 try 后面可以跟多个 catch:
try{
//可能抛出异常的语句
}catch (exception_type_1 e){
//处理异常的语句
}catch (exception_type_2 e){
//处理异常的语句
}
//其他的catch
catch (exception_type_n e){
//处理异常的语句
}
当异常发生时,程序会按照从上到下的顺序,将异常类型和 catch 所能接收的类型逐个匹配。一旦找到类型匹配的 catch 就停止检索,并将异常交给当前的 catch 处理(其他的 catch 不会被执行)。如果最终也没有找到匹配的 catch,就只能交给系统处理,终止程序的运行。
下面的例子演示了多级 catch 的使用:
#include <iostream>
#include <string>
using namespace std;
class Base{ };
class Derived: public Base{ };
int main(){
try{
throw Derived(); //抛出自己的异常类型,实际上是创建一个Derived类型的匿名对象
cout<<"This statement will not be executed."<<endl;
}catch(int){
cout<<"Exception type: int"<<endl;
}catch(char *){
cout<<"Exception type: cahr *"<<endl;
}catch(Base){ //匹配成功(向上转型)
cout<<"Exception type: Base"<<endl;
}catch(Derived){
cout<<"Exception type: Derived"<<endl;
}
return 0;
}
运行结果:
Exception type: Base
在 catch 中,我们只给出了异常类型,没有给出接收异常信息的变量。
本例中,我们定义了一个基类 Base,又从 Base 派生类出了 Derived。抛出异常时,我们创建了一个 Derived 类的匿名对象,也就是说,异常的类型是 Derived。
我们期望的是,异常被catch(Derived)
捕获,但是从输出结果可以看出,异常提前被catch(Base)
捕获了,这说明 catch 在匹配异常类型时发生了向上转型(Upcasting)。
catch 在匹配过程中的类型转换
C/C++ 中存在多种多样的类型转换,以普通函数(非模板函数)为例,发生函数调用时,如果实参和形参的类型不是严格匹配,那么会将实参的类型进行适当的转换,以适应形参的类型,这些转换包括:
- 算数转换:例如 int 转换为 float,char 转换为 int,double 转换为 int 等。
- 向上转型:也就是派生类向基类的转换,请猛击《C++向上转型(将派生类赋值给基类)》了解详情。
- const 转换:也即将非 const 类型转换为 const 类型,例如将 char * 转换为 const char *。
- 数组或函数指针转换:如果函数形参不是引用类型,那么数组名会转换为数组指针,函数名也会转换为函数指针。
- 用户自定的类型转换。
catch 在匹配异常类型的过程中,也会进行类型转换,但是这种转换受到了更多的限制,仅能进行「向上转型」、「const 转换」和「数组或函数指针转换」,其他的都不能应用于 catch。
向上转型在上面的例子中已经发生了,下面的例子演示了 const 转换以及数组和指针的转换:
#include <iostream>
using namespace std;
int main(){
int nums[] = {1, 2, 3};
try{
throw nums;
cout<<"This statement will not be executed."<<endl;
}catch(const int *){
cout<<"Exception type: const int *"<<endl;
}
return 0;
}
运行结果:
Exception type: const int *
nums 本来的类型是int [3]
,但是 catch 中没有严格匹配的类型,所以先转换为int *
,再转换为const int *
。
数组也是一种类型,数组并不等价于指针,这点已在《数组和指针绝不等价,数组是另外一种类型》和《数组到底在什么时候会转换为指针》中进行了详细讲解。
C++ throw(抛出异常)详解
在《C++异常处理》一节中,我们讲到了 C++ 异常处理的流程,具体为:
抛出(Throw)–> 检测(Try) --> 捕获(Catch)
异常必须显式地抛出,才能被检测和捕获到;如果没有显式的抛出,即使有异常也检测不到。
在 C++ 中,我们使用 throw 关键字来显式地抛出异常,它的用法为:
throw exceptionData;
exceptionData 是“异常数据”的意思,它可以包含任意的信息,完全有程序员决定。exceptionData 可以是 int、float、bool 等基本类型,也可以是指针、数组、字符串、结构体、类等聚合类型,请看下面的例子:
char str[] = "http://c.biancheng.net";
char *pstr = str;
class Base{};
Base obj;
throw 100; //int 类型
throw str; //数组类型
throw pstr; //指针类型
throw obj; //对象类型
一个动态数组的例子
C/C++ 规定,数组一旦定义后,它的长度就不能改变了;换句话说,数组容量不能动态地增大或者减小。这样的数组称为静态数组(Static array)。静态数组有时候会给编码代码不便,我们可以通过自定义的 Array 类来实现动态数组(Dynamic array)。所谓动态数组,是指数组容量能够在使用的过程中随时增大或减小。
下面这段代码虽然有点长,但它是一个典型的使用异常的场景,请大家耐心阅读。
#include <iostream>
#include <cstdlib>
using namespace std;
//自定义的异常类型
class OutOfRange{
public:
OutOfRange(): m_flag(1){ };
OutOfRange(int len, int index): m_len(len), m_index(index), m_flag(2){ }
public:
void what() const; //获取具体的错误信息
private:
int m_flag; //不同的flag表示不同的错误
int m_len; //当前数组的长度
int m_index; //当前使用的数组下标
};
void OutOfRange::what() const {
if(m_flag == 1){
cout<<"Error: empty array, no elements to pop."<<endl;
}else if(m_flag == 2){
cout<<"Error: out of range( array length "<<m_len<<", access index "<<m_index<<" )"<<endl;
}else{
cout<<"Unknown exception."<<endl;
}
}
//实现动态数组
class Array{
public:
Array();
~Array(){ free(m_p); };
public:
int operator[](int i) const; //获取数组元素
int push(int ele); //在末尾插入数组元素
int pop(); //在末尾删除数组元素
int length() const{ return m_len; }; //获取数组长度
private:
int m_len; //数组长度
int m_capacity; //当前的内存能容纳多少个元素
int *m_p; //内存指针
private:
static const int m_stepSize = 50; //每次扩容的步长
};
Array::Array(){
m_p = (int*)malloc( sizeof(int) * m_stepSize );
m_capacity = m_stepSize;
m_len = 0;
}
int Array::operator[](int index) const {
if( index<0 || index>=m_len ){ //判断是否越界
throw OutOfRange(m_len, index); //抛出异常(创建一个匿名对象)
}
return *(m_p + index);
}
int Array::push(int ele){
![img](https://img-blog.csdnimg.cn/img_convert/8feea7eb4f41818cb82ec49c51439053.png)
![img](https://img-blog.csdnimg.cn/img_convert/96d17c2adab2fbbfde64558b09f1c7f9.png)
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618658159)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
ndex) const {
if( index<0 || index>=m_len ){ //判断是否越界
throw OutOfRange(m_len, index); //抛出异常(创建一个匿名对象)
}
return *(m_p + index);
}
int Array::push(int ele){
[外链图片转存中...(img-glyXIIie-1715561071774)]
[外链图片转存中...(img-Y6OOVfxd-1715561071774)]
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618658159)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**