(黑马C++)L09 C++类型转换 异常 输入输出流
一、C++类型转换
类型转换(cast)是将一种数据类型转换成另一种数据类型,一般情况下要尽量少的去使用类型转换,除非解决非常特殊的问题。
(1)静态转换(static_cast)
static_cast使用方式:static_cast<目标类型>(原始对象);
- 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。
- 没有父子关系的自定义类型不能进行转换。
#include <iostream>
using namespace std;
class Base{};
class Child : public Base{};
void test() {
Base* base = NULL;
Child* child = NULL;
//把base转成child,向下类型转换,不安全
Child* child2 = static_cast<Child*> (base);
//把child转成base,向上类型转换,安全
Base* base2 = static_cast<Base*> (child);
}
int main() {
test();
system("pause");
return 0;
}
- 用于基本数据类型之间的转换,如把int转换成char,或者把char转换成int。
#include <iostream>
using namespace std;
void test() {
char a = 'a';
double d = static_cast<double>(a);
cout << "d = " << d <<endl;
}
int main() {
test();
system("pause");
return 0;
}
(2)动态转换(dynamic_cast)
- 动态转关非常严格,失去精度或者不安全都不可以转换。
- 没有发生多态的情况下,可以用于子类转基类,但是不能用于基类转子类。
- dynamic_cast如果发生了多态,可以让基类转为派生类,向下转换。
#include <iostream>
using namespace std;
class Base{
public:
virtual void func() {};
};
class Child : public Base{
virtual void func() {};
};
void test() {
Base* base = NULL;
Child* child = NULL;
Base* base2 = new Child;
Child* child = dynamic_cast<Child*> (base2);
}
int main() {
test();
system("pause");
return 0;
}
(3)常量转换(const_cast)
用于修改类型的const属性。
- 常量指针被转化为非常量指针,并且仍然指向原来的对象。
- 常量引用被转换成非常量引用,并且仍然指向原来的对象。
- 注意:不能直接对非指针和非引用的变量使用const_cast操作符直接移除它的const。
#include <iostream>
using namespace std;
void test() {
//去除const
const int * p = NULL;
int * newp = const_cast<int*> (p);
//加上const
int * p2 = NULL;
const int * newp2 = const_cast<const int*> (p2);
}
int main() {
test();
system("pause");
return 0;
}
(4)重新解释转换(reinterpret_cast)
- 最不安全的转换机制,最有可能出现问题,不推荐使用。
- 主要用于将一种数据类型转换为另一种类型,可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针。
二、C++异常
(1)异常的基本使用
- 异常处理就是处理程序中的错误,即程序运行过程中发生的一些异常事件(如:除0溢出,数组下标越界,读取的文件不存在。空指针,内存不足等)。
- 在C语言中对错误的处理有两种方法:一是使用整型的返回值标识错误,二是使用errno宏,用不同的数字变量返回错误,该方法的局限是会出现不一致的问题,可能无法判断是返回值还是异常。
int myDevide(int a, int b) {
if(b == 0) return -1;
return a/b;
}
- C++的异常机制:使用抛出异常和处理异常,抛出的异常必须进行处理,否则程序会出错。
- 如果异常种类较多,都要进行处理,也可以使用以下方式:
catch(...) { //捕获异常
cout << "其他类型异常捕获" << endl;
}
#include <iostream>
using namespace std;
int myDevide(int a, int b) {
if(b == 0) {
throw -1; //抛出int类型的异常
}
return a/b;
}
void test() {
int a = 10;
int b = 0;
try{
myDevide(a, b);
}
catch(int) { //捕获异常
cout << "int类型异常捕获" << endl;
}
}
int main() {
test();
system("pause");
return 0;
}
- 异常处理可以在调用跳级,假设在有多个函数的调用栈中出现了某个错误,使用整型返回码要求在每一级函数中都进行处理,而使用异常处理的栈展开机制,只需要在一处进行处理就行,不需要每级函数都处理。
- 以下代码会打印 double类型异常捕获,但是不想处理时可以继续向上抛出
- 如果异常没有处理,会调用terminate函数,使程序中断
#include <iostream>
using namespace std;
int myDevide(int a, int b) {
if(b == 0) {
throw 3.14;
}
return a/b;
}
void test() {
int a = 10;
int b = 0;
try{
myDevide(a, b);
}
catch(int) { //捕获异常
cout << "int类型异常捕获" << endl;
}
catch(double) { //捕获异常
//如果不想处理这个异常需要向上抛出
//throw; 此时会返回main函数中double类型异常捕获
cout << "double类型异常捕获" << endl;
}
}
int main() {
try{
test();
}
catch(double) {
cout << "main函数中double类型异常捕获" << endl;
}
system("pause");
return 0;
}
(2)对自定义异常进行捕获
可以抛出自定义对象,捕获自定义异常。
#include <iostream>
using namespace std;
//自定义异常
class myException {
public:
void printError() {
cout << "自定义异常" << endl;
}
};
int myDevide(int a, int b) {
if(b == 0) {
throw myException(); //匿名对象
}
return a/b;
}
void test() {
int a = 10;
int b = 0;
try{
myDevide(a, b);
}
catch(myException e) { //捕获异常
e.printError();
}
}
int main() {
test();
system("pause");
return 0;
}
(3)栈解璇
异常被抛出后,从进入try块起,到异常被抛出前,这期间在栈上构造的所有对象,都会被自动析构,析构的顺序与构造的顺序相反,这一过程称为栈的解璇。
(4)异常的接口声明
- 为了加强程序的可读性,可以在函数声明中列出可能抛出异常的所有类型,例如:void func() throw(A, B, C) } {};这个函数func只能抛出类型A, B, C及其子类型的异常。
- 如果函数声明中没有包含异常接口声明,则此函数可以抛出任何类型的异常。
- 一个不抛出任何类型异常的函数可声明为void func() throw() {}。
- 如果一个函数抛出了它的异常接口声明不允许抛出的异常,unexcepted函数会被调用,该函数默认调用terminate函数中断程序。
(5)异常变量生命周期
#include <iostream>
using namespace std;
class myException {
myException() {
cout << "默认构造函数调用" <<endl;
}
myException(const myException& e) {
cout << "拷贝构造函数调用" <<endl;
}
~myException() {
cout << "析构函数调用" <<endl;
}
};
void doWork() {
throw myException();
}
void test() {
try{
doWork();
}
catch(myException e) {
cout << "捕获异常" << endl;
}
}
int main() {
test();
system("pause");
return 0;
}
以上函数会输出以下结果,因为myException()调用默认构造,myException e调用拷贝构造,此时拷贝构造会多花费一份开销,所以一般写成myException &e,此时不会再调用拷贝构造。
默认构造函数调用
拷贝构造函数调用
捕获异常
析构函数调用
析构函数调用
当异常写成以下形式时,会先析构,执行结果为默认构造函数调用,析构函数调用,捕获异常。
#include <iostream>
using namespace std;
class myException {
myException() {
cout << "默认构造函数调用" <<endl;
}
myException(const myException& e) {
cout << "拷贝构造函数调用" <<endl;
}
~myException() {
cout << "析构函数调用" <<endl;
}
};
void doWork() {
throw &myException();
}
void test() {
try{
doWork();
}
catch(myException* e) {
cout << "捕获异常" << endl;
}
}
int main() {
test();
system("pause");
return 0;
}
(6)异常的多态使用
利用多态可以实现printError同一个接口调用,抛出不同的错误对象。
#include <iostream>
using namespace std;
//异常基类
class BaseException{
public:
virtual void printError() {
}
};
class NullPointerException : public BaseException {
public:
virtual void printError() {
cout << "空指针异常" << endl;
}
};
void doWork() {
throw NullPointerException();
}
void test() {
try{
doWork();
}
catch(BaseException& e) { //父类的引用调用子类的对象
e.printError();
}
}
int main() {
test();
system("pause");
return 0;
}
(7)C++标准异常库
标准库中提供了很多的异常类,它们是通过类继承组织起来的,异常类继承层级结构图如下:
- 在上述继承中,每个类都提供了构造函数,拷贝构造函数,析构函数和赋值运算符的重载;
- logic_error类以及其子类、runtime_error类以及其子类,他们的构造函数接收一个string类型的参数,用于异常信息的描述;
- 所有的异常都有一个what方法,返回 const char* 类型的值,描述异常信息。
exception的直接派生类:
logic_error的派生类:
runtime_error 的派生类:
以out_of_range
异常为例,看一下它具体的使用方法,使用时需要包含头文件<stdexcept>
#include <iostream>
#include <string>
using namespace std;
//系统提供的异常
#include <stdexcept>
class Person {
public:
Person(string name, int age) {
this->m_Name = name;
if(age < 0 || age > 200) {
//抛出越界异常
throw out_of_range("年龄越界!");
}
else {
this->m_Age = age;
}
}
string m_Name;
int m_Age;
};
void test01() {
try{
Person p("张三", 300);
}
catch(out_of_range& e) {
cout << e.what() << endl;
}
}
int main() {
test01();
system("pause");
return 0;
}
三、C++输入与输出流
程序的输入指的是从输入文件将数据传送给程序,程序的输出指的是从程序将数据传送给输出文件。
C++的输入输出包含三种:
标准I/O:从键盘输入数据,输出到显示器屏幕
文件I/O:以外存磁盘文件为对象进行输入和输出
串I/O:对内存中指定的空间进行输入和输出,通常指定字符数组作为存储空间进行信息存储
(1)标准输入流
- 缓冲区:输入和输出的所有数据,都不是直接放在程序中,而是先放在缓冲区中,然后再从缓冲区中取出数据。
- cin.get():一次读取一个字符,输入abc 输出a
void test01()
{
char ch;
ch = cin.get(); //输入as
cout << "ch = " << ch << endl; //输出a
ch = cin.get();
cout << "ch = " << ch << endl; //输出s
ch = cin.get();
cout << "ch = " << ch << endl; //输出换行
ch = cin.get();
cout << "ch = " << ch << endl; //等待下一次输入
}
- cin.get(一个参数):读一个字符,与上面效果一样
- cin.get(两个参数):可以读字符串
void test() {
char buf[1024];
//cin >> buf; //采用>>运算符输入字符窜,遇到空格就会停止
//cout << buf << endl;
cin.get(buf, 1024); //不会拿走换行,换行还在缓冲区
cout << buf<< endl;
}
- cin.getline():读取一行字符串
void test() {
char buf[1024];
cin.getline(buf, 1024); //会把换行符也读取并扔掉换行符
cout << buf<< endl;
}
- cin.ignore():默认忽略缓冲区的一个字符,带参数N,代表忽略N个字符
void test() {
cin.ignore(); //输入as
char c = cin.get();
cout << c << endl; //输出s
}
- cin.peek():从缓冲区查看一个字符,但不会从缓冲区取走
void test()
{
char c = cin.peek(); //输入as
cout <<"c = "<< c <<endl; //输出a
ch = cin.get();
cout << "c = " << c << endl; //输出a
}
- cin.pushback():将缓冲区取出的字符放回原位置
void test()
{
char c = cin.get(); //输入helloworld h被拿走
cin.putback(c); //h被放回
char buf[1024];
cin.getline(buf, 1024); //读取字符串
cout << "buf = " << buf << endl;
}
- 标准输入流案例
- (1)判断用户输入的是字符串还是数字
void test() {
cout << "请输入一串数字或者字符串" << endl;
//判断第一个字母(peek)
char c = cin.peek();
if(c >= '0' && c <= '9') {
int num;
cin >> num; //在缓冲区读取
cout << "您输入的是数字,数字为:" << num <<endl;
}
else {
char buf[1024];
cin >> buf;
cout << "您输入的是字符串,字符串为:" << buf <<endl;
}
}
- (2)让用户输入1-10的数字,如果输入有误,重新输入
void test() {
int num;
while(true) {
cout << "请输入数字:" << endl;
cin >> num; //如果输入的是char形,标志位会被修改
if(num > 0 && num <= 10) {
cout << "输入的数字为:" << num << endl;
break;
}
else {
cout << "对不起,请重新输入!" << endl;
cin.clear(); //重置标志位
cin.sync(); //清空缓冲区
//cout <<"标志位:" << cin.fail() << endl; //标志位0正常 1不正常
}
}
}
(2)标准输出流
- cout.flush():刷新缓冲区,Linux下有效
- cout.put():向缓冲区写字符
void test() {
cout.push('a').put('b'); //输入ab
}
- cout.write():从buffer中写num个字节到当前的输出流中
void test() {
char buf[1024] = "helloworld";
cout.write(buf, strlen(buf));
}
- 格式化输出:使用控制符方法;使用流对象的有关成员函数
- 使用成员函数
void test()
{
int num = 99;
cout.width(20); //设置宽度,前面加18个空格
cout.fill('*'); //设置填充,将空格填充为*
cout.setf(ios::left); //左设置对齐 set format
cout.unsetf(ios::dec); //卸载十进制
cout.setf(ios::hex); //安装十六进制
cout.setf(ios::showbase);//显示进制基数,如果是十六进制,前面加上0x
cout.unsetf(ios::hex); //卸载十六进制
cout.setf(ios::oct); //安装八进制
cout << num << endl;
}
- 使用控制符(包含头文件iomanip)
void test03()
{
int num = 99;
cout << setw(20) //设置宽度
<< setfill('~') //设置填充
<< setiosflags(ios::showbase) //显示进制基数
<< setiosflags(ios::left) //左对齐
<< hex //安装16进制
<< num << endl;
}
(3)文件的读写操作
文件读写定义了三个类,分别是ofstream、ifstream、fstream。
- 打开文件
用一个流对象打开一个文件的成员函数是open (filename, mode);其中 filename 是一个字符串,表示要打开的文件的名称,mode 是一个可选参数,由以下标志组合而成:
- 写文件操作示例如下:
#include <iostream>
using namespace std;
//文件读写头文件
#include <fstream>
//写文件
void test01() {
//以输出方式打开文件
ofstream ofs("./test.txt", ios::out | ios::trunc);
//后期指定打开方式
ofstream ofs;
ofs.open("./test.txt", ios::out | ios::trunc);
//判断是否打开成功
if(!ofs.is_open()) {
cout << "打开失败!" << endl;
}
//输入内容
ofs << "姓名:abc" <<endl;
ofs << "年龄:20" <<endl;
ofs << "性别:男" <<endl;
//写入完毕后关闭文件
ofs.close();
}
- 读文件操作示例如下:
void test02() {
ifstream ifs;
ifs.open("./test.txt", ios::in);
//判断是否打开成功
if(!ifs.is_open()) {
cout << "打开失败" << endl;
}
//第一种方式 -- 用数组存储
char buf[1024];
while(ifs >> buf) { //按行读取
cout << buf <<endl;
}
//第二种方式
char buf2[1024];
while(!ifs.eof()) { //eof读到文件的尾部
ifs.getline(buf2, sizeof(buf2));
cout << buf2 << endl;
}
//第三种方式 -- 按单个字符读取(不推荐)
char c;
while((c = ifs.get()) != EOF) { //EOF 文件尾
cout << c;
}
}