目录
强制类型转换
static_cast
reinterpret_cast
dynamic_cast
const_cast
const_cast笔试
异常处理
什么是异常处理机制?
异常的基本语法
异常处理基本思想
栈解旋
异常接口声明
throw抛出类对象
异常案例
标准异常库
C++的文件操作
C++输入输出流
C++的IO操作
相关的头文件
流对象
标准输入流
cin.get()
cin.get(一个参数)
cin.get(两个参数)
cin.getline()
cin.ignore()
cin.peek()
cin.putback()
标准输出流
标准输出流-控制符
标准输出流-成员函数
文件流类
写文件(创建输出流对象)
读文件
读写文件
上节学习了函数模板和类模板,本节开始学习异常和文件!
强制类型转换
C语言强制类型转换问题:
1、过于粗暴:
任意类型都可以相互转换,编译器很难判断其正确性;
2、难于定位:
在源码中无法快速找到使用类型转换的语句。
在程序设计理论中,强制类型转换时不被推荐的,应该尽量避免。
C++把强制类型转换分成了四种:
static_cast、reinterpret_cast、dynamic_cast、const_cast
其中dynamic_cast用于基类和派生类之间的转换
我们之前在学习多态的时候写过的关于dynamic_cast的应用场景的代码,
直接这样强转时Chinese *c=(Chinese*)p,如果p本来指向的就是派生类对象,那么用派生类指针指向派生类对象没有问题,这样强转就你能成功,但是如果p本来指向的时候派生类对象,而是基类对象话,这样强转就会失败。也就是是Chinese *c=(Chinese*)p什么时候能强转成功我们不知道,于是dynamic_cast就能替我们判断,如果不能强转就返回空。
static_cast
1、用于基本类型之间的转换、但是不能用于基本类型指针之间的转换;
2、用于有继承关系类对象之间的转换和类指针之间的转换。
static_cast是编译期进行转换的,无法在运行时检测类型,所以类类型之间的转换可能存在风险。
直接代码演示
很多时候编译器都会帮我们自动转换类型,比如这段代码,编译器会将char类型转换成int类型
如果我们要手动强转就加个括号就行了,其实这种情况我们完全可以省略掉。
以上是C语言强转的方式,在C++里面,这种手动强转的情况我们写成
所以static_cast的使用场景就是这种,一般在可以省略的情况下都可以用static_cast,作用可能就是让我们一眼就能看出这里用了一个强制类型转换吧。
但是注意,static_cast不能用于基本类型指针之间的转换
比如这样写的话编译器就会报错
因为&ch的类型是char*的地址,不能这样强转成int*
所以结论就是:
另外它还用于有继承关系类对象之间的转换和类指针之间的转换。
reinterpret_cast
1、用于指针类型之间的转换;
2、用于整数和指针类型之间的转换。
reinterpret_cast是直接从二进制位进行复制,是一种极其不安全的转换。
直接上代码讲解:
像这样写代码的话是会报错的
如果想要成功就得用reinterpret_cast强转类型转换
还有别的用法
dynamic_cast
1、主要用于类层次之间的转换,还可以用于类之间的交叉转换;
2、dynamic_cast具有类型检查的功能,比static_cast更安全。
代码我们之前写过了。
const_cast
用于去除变量的const属性,比如常量、常引用。
下面看一道笔试题
const_cast笔试
void main()
{
const int &j = 1;
int &k = const_cast<int &>(j);
const int x = 2;
int &y = const_cast<int &>(x);
k = 5;
printf("j = %d\n", j);
printf("k = %d\n", k);
y = 3;
printf("x = %d\n", x);
printf("y = %d\n", y);
printf("&x = %x\n", &x);
printf("&y = %x\n", &y);
}
补充命令37:底行模式 :set paste
如果要将上面这段代码复制到vim中,就输入冒号进入底行模式,然后输入set paste,进入粘贴模式,
然后就可以右键粘贴了
用到printf要包含头文件stdio.h
首先来看第一句:
常量本身不占内存,引用的时候必须引用一块内存。这个时候编译器会分配四个字节,,这四个字节里面填的是数字1,然后给这四个字节取个别名叫j,而且这个j是被const修饰的,如果我们之后写j=2,那编译器就会报错。
然后接下来第二句
j是const int类型,k是int类型的引用,因此我们要将j的const属性掉,用到const_cast转换。这句代码可以理解成k也是别名,引用的也是这块内存。
但是k没有被const修饰,j被const修饰,接下来k=5;k是这块内存的别名,也就是将1改成了5
接下来打印的j和k都是5
区别就是可以通过k修改这块内存中的值,但是不能通过j修改内存中的值。
接下来看x和y
这个x是存放在符号表里面的
所以接下来不管下面怎么操作,x都是2,这就像预处理的时候处理宏定义一样,遇到这个x就替换成2。
然后接下来y引用一块内存,编译器分配四个字节,并且把2填进去内存,这块内存的别名叫y。
接下来y=3;那这内存中的3变成了3
接下来打印的x就等于2,y就是3
输出的&y就是这四个字节的地址,x是常量存放在符号表里面的,是不占内存的,但是&强行对一个常量取地址,编译器也会分配一块内存,而且这块内存就是刚刚给y分配的4个字节的那块内存。因此&y和&x输出是一样的。
如果此时对这块地址取值的话就是3。
异常处理
比如以前我们在学习C语言的时候,我们调用一个函数,然后通常通过函数的返回值来判断这个函数调用成功还是失败
这样写代码就比较麻烦,于是有人想调用函数就是函数,能不能把这些判断处理的过程放在别的我们不知道的地方去处理就好了,于是在C++中就有一个异常处理的概念。
直接上代码讲解:
以前我们在写函数的时候入参判断会经常写这样类似的代码
但是这样比较麻烦,而且a和b取整也有可能是-1,这样异常时我们return -1不太合适。
现在我们可以尝试抛出异常,抛出的异常可以是任意类型的对象
抛出异常那就必须有个地方接收这个异常,并且C++规定可能抛出异常的代码必须要放在try语句里面。
如果改成抛出一个整型:
如果我们改成抛出一个float类型就不行,因为我们没有写根据float类型捕获
目前为止还没有体现这种异常语法的好处,接着看......
我们可以把函数调用和异常处理两个环节给分开就能看到异常的好处就是跨越函数。
这就是异常存在的意义。
什么是异常处理机制?
1、异常是一种程序控制机制,与函数机制独立和互补
函数是一种以栈结构展开的上下函数衔接的程序控制系统,异常是另一种控制结构,它依附于栈结构,却可以同时设置多个异常类型作为网捕条件,从而以类型匹配在栈机制中跳跃回馈。
2、异常设计目的:
栈机制是一种高度节律性控制机制,面向对象编程却要求对象之间有方向、有目的的控制传动,从一开始,异常就是冲着改变程序控制结构、以适应面向对象程序更有效地工作这个主题,而不是仅为了进行错误处理。
异常设计出来之后,却发现在错误处理方面获得了最大的好处。
异常的基本语法
1、若有异常则通过throw操作创建一个异常对象并抛掷。
2、将可能抛出异常的程序段嵌在try块之中;控制通过正常的顺序执行到达try语句,然后执行try块内的保护段。
3、如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行;程序从try块后跟随的最后一个catch子句后面的语句继续执行下去。
4、catch子句按其在try块后出现的顺序被检查,匹配的catch子句将捕获并处理异常(或继续抛掷异常)。
5、如果匹配的catch未找到,则运行函数terminate将被自动调用,其缺省功能是调用abort终止程序。
6、处理不了的异常,可以在catch的最后一个分支,使用throw语法,向上扔。
7、异常机制与函数机制互不干涉,但捕捉的方式是基于类型匹配;捕捉相当于函数返回类型的匹配,而不是函数参数的匹配,所以捕捉不用考虑一个抛掷中的多种数据类型匹配问题
8、异常捕捉严格按照类型匹配,异常捕捉的类型匹配之苛刻程度可以和模板的类型匹配媲美,它不允许相容类型的隐式转换,比如抛掷char类型用int型就捕捉不到。
异常处理基本思想
传统的错误处理机制:通过函数返回值处理错误;
1、C++的异常处理机制使得异常的引发和异常的处理不必在同一个函数中,这样底层的函数可以着重解决具体问题,而不必过多的考虑异常的处理。上层调用者可以在适当的位置设计对不同类型异常的处理。
2、异常是专门针对抽象编程中的一系列错误处理的,C++中不能借助函数机制,因为栈结构的本质是先进后出,依次访问,无法进行跳跃,但错误处理的特征却是遇到错误信息就想要转到若干级之上进行重新尝试。
3、异常超脱于函数机制,决定了其对函数的跨越式回跳。
4、异常跨越函数
只要前面的代码能懂就行了,这些概念就不必纠结了。
栈解旋
异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上的构造的所有对象,都会被自动析构。析构的顺序与构造的顺序相反。这一过程称为栈的解旋(unwinding)。
比如我们在try块里面创建了一个对象,那么一旦有异常,这两块地方之间写的所有东西都会被释放。
异常接口声明
1、为了加强程序的可读性,可以在函数声明中列出可能抛出的所有异常类型,例如:
void func() throw (A, B, C , D); //可以抛出A, B, C , D这四种类型的异常,以及可以抛出子类型的异常(异常也可以使用继承)
2、如果在函数声明中没有包含异常接口声明,则此函数可以抛掷任何类型的异常,例如:
void func();(一般写成这样最好)
3、一个不抛掷任何类型异常的函数可以声明为:
void func() throw();
4、如果一个函数抛出了它的异常接口声明所不允许抛出的异常,unexpected函数会被调用,该函数默认行为调用terminate函数中止程序。
throw抛出类对象
通过抛出类对象来验证异常变量的生命周期。
既然throw能抛出一个类的对象,那我们就写一个类,并在这里抛出一个类的对象
然后这边根据对象类型捕获异常
为什么析构了两次?
那是不是说明里面有两个对象?
因为这里其实这里就类似于函数传参的过程
我们这里其实是int a,如果我们throw 1的话,它就会把这个1传给int a,只不过我们没有用这个a,所以一般都不会把这个a写出来,但实际上是有的。
那么我们这里其实也是有一个对象的
那抛出对象有什么好处呢?
我们可以在这里写一个函数
然后我们就可以在这里用这个对象访问这个成员函数打印出来。
如果我们这个函数体内要写很多东西的话,那通过这种方式就可以让捕获异常这里更加简洁。
但是问题又来了,为什么析构函数打印了两次,而构造函数却只打印了一次?
原因是这个对象赋值给对象e的时候调用的是拷贝构造函数,将对象e传给拷贝构造函数,然后在拷贝构造函数体内将该对象(this)赋值给对象e
我们没有写拷贝构造函数所以就没打印什么。
如果我们写出来就是这样的
整个过程涉及两个对象,这就是一个缺点。
我们可以用引用来接效率会更高一些,这样它就少创建一个对象。
这样这个e相当于是throw抛过来的那个对象的别名。
还有这里我们也不推荐这样写
我们可以抛出对象指针
然后接收的话就用一个指针去
但是最终我们还是推荐用引用来接的这种方式
以下案例可以帮助我们理解为什么要抛出一个异常对象,因为如果是类话就可以继承派生出子类,那一个子类就可以代表一个异常。
异常案例
设计一个数组类 MyArray,重载[ ]操作,数组初始化时,对数组的个数进行有效检查:
1、index<0 抛出异常eNegative;
2、index = 0 抛出异常 eZero;
3、index>1000抛出异常eTooBig;
4、index<10 抛出异常eTooSmall;
5、eSize类是以上类的父类,实现有参数构造、并定义virtual void printErr()输出错误。
代码演示:
由于这几个类叫做内部类,都是在MyArray这个类里面才会用
所以这几个类我们就在MyArray里面写,esize是他们的父类,类似于结构体里面嵌套结构体。里面还要写一个函数把这些字符串打印出来
然后实现各个子类
完整代码:
我们上面写了这么多,其实C++已经为了我们准备了一个这样的异常类,我们直接拿来用就可以了:
标准异常库
C++标准提供了一组标准异常类,这些类以基类Exception开始,标准程序库抛出的所有异常,都派生于该基类。该基类提供了一个成员函数 what() 用于返回错误信息,函数声明如下:
virtual const char *what() const throw();
函数名叫做what()
const char *是返回值类型,其实就是返回字符串
what() 后面的const表示这个函数是个常成员函数,里面不能修改东西。
throw()表示它不会抛出任意类型的异常。
virtual表示它是个虚函数
常用的异常类:
bad_alloc:new分配空间失败
bad_cast:执行 dynamic_cast 失败
bad_typeid:对某个空指针 p 执行 typeid(*p)
代码演示使用这个异常库,记得包含头文件<exception>
运行提示内存申请异常。
所以有了这个标准库我们几行代码就可以搞定了。
下面开始看C++里面的文件操作
C++的文件操作
我们之前在学习C语言和Linux的时候都学习过文件操作,C语言给我们提供的这套接口叫做标准IO操作,就是fopen,fread,fwrite等几个函数,效率比较高。
Linux给我们提供的这套接口叫系统调用,就是open,write,read等这个几个函数,效率比较低。如果读写大文件的话,建议用C语言的标准IO操作。
其实C语言和Linux提供的这两套接口已经够我们用了,C++这套做个了解即可。
C++输入输出流
输入和输出(相对内存来说)
程序的输入指的是从输入文件将数据传送给程序,程序的输出指的是从程序将数据传送给输出文件。
写内存就是输入操作,读内存就是输出操作。
操作文件的时候,比如读文件,我们就得创建一个对象去读,这个对象就叫做输入流对象,因为要读到内存中。
C++输入输出包含以下三个方面的内容:
对系统指定的标准设备的输入和输出。即从键盘输入数据,输出到显示器屏幕。这种输入输出称为标准的输入输出,简称标准I/O。
以外存磁盘文件为对象进行输入和输出,即从磁盘文件输入数据,数据输出到磁盘文件。以外存文件为对象的输入输出称为文件的输入输出,简称文件I/O。
对内存中指定的空间进行输入和输出。通常指定一个字符数组作为存储空间(实际上可以利用该空间存储任何信息)。这种输入和输出称为字符串输入输出,简称串I/O。
C++的IO操作
在C语言中,用printf和scanf进行输入输出,往往不能保证所输入输出的数据是可靠的安全的。在C++的输入输出中,编译系统对数据类型进行严格的检查,凡是类型不正确的数据都不可能通过编译。因此C++的I/O操作是类型安全(type safe)的。C++的I/O操作是可扩展的(主要是因为它通过继承来产生的),不仅可以用来输入输出标准类型的数据,也可以用于用户自定义类型的数据。
C++编译系统提供了用于输入输出的 iostream 类库。iostream这个单词是由3个部分组成的,即i-o-stream,意为输入输出流。
相关的头文件
iostream 包含了对输入输出流进行操作所需的基本信息。
fstream 用于用户管理的文件的I/O操作(之后我们要读写文件的话就用这个头文件)。
strstream 用于字符串流I/O。
stdiostream 用于混合使用C和C + +的I/O机制时,例如想将C程序转变为C++程序。
iomanip 在使用格式化I/O时应包含此头文件。
流对象
cin:标准输入流;
cout:标准输出流;
cerr:标准错误流;
clog:标准错误流。
在 iostream 头文件中定义以上4个流对象用以下的形式(以cout为例):
ostream cout ( stdout);
在定义 cout 为 ostream 流类对象时,把标准输出设备stdout作为参数,这样它就与标准输出设备(显示器)联系起来,如果有
cout <<3;
就会在显示器的屏幕上输出3。
标准输入流
标准输入流对象cin,重点掌握的函数
cin.get() //一次只能读取一个字符 遇到EOF结束
cin.get(一个参数) //读一个字符
cin.get(两个参数) //可以读字符串
cin.getline()
cin.ignore()
cin.peek()
cin.putback()
代码演示:
cin.get()
cin.get(一个参数)
cin.get(两个参数)
cin.getline()
cin本身不可以获得空格,所以就可以使用cin.getline()
cin.ignore()
cin.peek()
cin.peek()从缓冲区读取一个字节,数据还留在缓冲区
cin.peek()也是从标准输入里面获取一个字符,和cin.get的区别就是:当我们用键盘上输入字符放到标准输入里面,然后cin.get从标准输入里面都走了,那这个字符就不再留在标准输入里面了。但是cin.peek从标准输入里面读走字符的时候,这个字符还留在标准输入里面了。
cin.putback()
这个运行的时候根本就不会停止让我们输数据,因为缓冲区有东西,直接读到ch里面,然后打印出来。
通过以上操作我们就知道和cin是一个对象,我们通过对象调用类里面的成员函数。
内存和键盘中间还有一个缓冲区,一个叫标准输入缓冲区(cin),一个叫标准输出缓冲区(cout)。
标准输出流
在输出数据时,为简便起见,往往不指定输出的格式,由系统根据数据的类型采取默认的格式,但有时希望数据按指定的格式输出,如要求以十六进制或八进制形式输出一个整数,对输出的小数只保留两位小数等。有两种方法可以达到此目的。
1、使用控制符的方法;
2、使用流对象的有关成员函数。
标准输出流-控制符
cout << "dec:" << dec << a << endl; //以十进制形式输出整数
cout << "hex:" << hex << a << endl; //以十六进制形式输出整数a
iomanip 在使用格式化I/O时应包含此头文件。
cout << "oct:" << setbase(8) << a << endl; //以八进制形式输出整数a
cout << setw(10) << pt << endl; //指定域宽为,输出字符串
cout << setfill('*') << setw(10) << pt << endl;
cout << setiosflags(ios::scientific) << setprecision(8); //指数输出 小数8位
注:ios::后面可能是这个类里面的一个枚举或者宏定义
cout << "pi=" << setprecision(4) << pi << endl; //改为位小数 //保留的有效位数
cout << "pi=" << pi << endl; //输出pi值
cout << "pi=" << setiosflags(ios::fixed) << pi << endl; //改为小数形式输出
以上是通过控制符的形式来实现的
接下来用成员函数也能实现以上功能
标准输出流-成员函数
除了可以用控制符来控制输出格式外,还可以通过调用流对象cout中用于控制输出格式的成员函数来控制输出格式。用于控制输出格式的常用的成员函数如下:
precision(n) //设置实数的精度为n
unsetf() //终止输出格式状态
width(n) //设置字段宽度为n
fill(c) //设置填充字符c
setf() //设置输出格式状态(ios::left ios::right ios::hex ios::dec)
文件流类
和文件有关系的输入输出类主要在 fstream 这个头文件中被定义,它是从iostream类派生的,用来支持对磁盘文件的输入输出。
写文件(创建输出流对象)
注:写文件是输出操作,将内存中的数据通过流的方式写入文件中,保存完之后程序处理完毕,相关对象被回收销毁。写操作对应着输出,而读操作对应着输入。程序里的读操作是往程序里输入,写操作是将程序里的数据输出到文件中。
代码演示
也可以这样
读文件
把我们刚刚写进hello.txt文件里面的Helloworld读出来
读写文件
也有一个对象既能读也能写
C++里面这套输入输出接口比linux的系统调用效率也高一点,但是我们几乎不用。
文件和异常基本都不用,做到能看懂代码即可。
下节开始学习STL!
如有问题可评论区或者私信留言,如果想要进扣扣交流群请私信!