01 类型转换
(1) 传统的类型转换
传统的类型转换,功能是非常强大的,可以进行任意类型之间的类型转换(但是不能保证正确性)
形式:类型 变量 = (目标类型)表达式;
把表达式转换为目标类型
double b;int x = (int)b;
int *p = (int *)malloc(...);// malloc的返回值是void *
long p = 0x01020304;
int *q = (int *)p;// 可以,把0x01020304当成地址编号来处理
*q = 100;// 有问题,因为0x01020304是自己杜撰的地址,有可能不存在,有可能不能访问
在C++中,传统的类型转换方式仍然可用为了增加安全性,强调风险,让程序员明白自己在做什么
C++增加了自己的类型转换方式
(2) C++中新增的类型转换
1. static_cast<目标类型>(表达式)
只能用于良性转换(可以进行隐式转换的地方)
a. 基本类型之间的相互转换
int,short,double,char,long ......
int ---> double
double ---> int
char ---> int
enum ---> int
b. 可以把void *转成任意类型的指针
c. 把枚举转为int
2. const_cast<目标类型>(表达式)
主要是用于移出指针 / 引用的CV属性(const / volatile)
将const / volatile类型的指针 / 引用转换为非const / 非volatile类型的指针 / 引用const:不可变的
volatile:易变的,防止编译器优化#include <iostream> using namespace std; int main() { const int x = 1024; // int * <=====> const int * // int *p = &x; // error,x是const int类型,不能直接赋值给int * // int *p = (int*)&x; // ok int *p = const_cast<int*>(&x); // ok *p = 250; // 把250写入到x的存储空间 cout << "x: " << x << endl; // x: 1024 cout << "*p: " << *p << endl; // *p: 250 cout << "&x: " << &x << endl; // &x: 0x7ffed03bf40c cout << "p: "<< p << endl; // p: 0x7ffed03bf40c // C++对于常量的处理有点类似于编译时期的宏定义,是一个值替换的过程 // 在编译的时候,代码中所有用到x的地方,都会被替换成1024 // 所以常量在编译时期就已经确定了,不能在运行时修改 volatile int y = 1024; int *q = const_cast<int*>(&y); // ok *q = 250; // 把250写入到y的存储空间 cout << "y: " << y << endl; // y: 250 cout << "*q: " << *q << endl; // *q: 250 cout << "&y: " << &y << endl; // &y: 1 修饰的是易变的,y的地址被隐藏了 cout << "q: "<< q << endl; // q: 0x7ffc5fe98c24 return 0; }
3. reinterpret_cast<目标类型>(表达式)
重新解释,可以用于任意类型之间的强制转换(类似于传统的强制转换)
做法:
对表达式底层存储的二进制数据进行重新的解析,只不过不修改二进制的值
是对static_cast的补充,可以完成良性转换以外的任何类型转换
如:不同指针之间的类型转换
int *----->double *
char *----->int *
还可以用于整数和指针之间的转换
int ----->int * // 把int数值解释为int*
int *----->int
....#include <iostream> using namespace std; // 枚举类,枚举量的值是私有的,不能直接使用,必须通过枚举类来访问, // 枚举类是强类型的,不能隐式转换为int // 在C++11中,引入了枚举类,枚举类是强类型的,不会暴露在最外层的作用域中 // 因此不会与其他枚举类型冲突 enum class Color1 { RED, GREEN, BLUE }; enum Color2 { // 在传统的枚举中,枚举量的值是暴露在最外层的作用域的 RED, GREEN, BLUE }; int main() { double a = 1.1; // static_cast只能用于安全的类型转换(可以进行隐式转换的地方) // int *q = static_cast<int *>(&a); // error // reinterpret_cast可以用于任意类型之间的强制转换(类似于传统的强制转换) int *q = reinterpret_cast<int *>(&a); // 正确 cout << *q << endl; // -1717986918 int b = (int)a; int c = static_cast<int>(a); cout << b << endl; // 1 cout << c << endl; // 1 int d = ::RED; cout << d << endl; // 0 d = Color2::RED; cout << d << endl; // 0 int e = static_cast<int>(Color1::RED); cout << e << endl; // 0 // e = Color1::RED; // error,Color1::RED是枚举类型,不能隐式转换为int e = (int)Color1::RED; // 正确 cout << e << endl; // 0 // 假设有一个硬件寄存器地址为0x010203040506 long addr = 0x010203040506; // char *p = static_cast<char *>(addr); // error char *p = reinterpret_cast<char *>(addr); // 正确 return 0; }
4. dynamic_cast<目标类型>(表达式)
基类和派生类之间的转换,而且必须包含虚函数,否则编译不通过
02. 异常(exception)
异常是C++中新增加的一种错误处理方式
在C++中,错误一般分两种,编译时错误() 和运行时错误
所谓异常,指在程序运行过程中,由于系统条件,操作不当等原因引起的运行错误,常见的异常包括 除零错误、指针访问收保护的空间、数组越界、内存分配失败、要打开的文件不存在......等
为了提高系统的健壮性,加强容错能力,在编程过程中,这些异常必须进行处理,来防止系统奔溃。将异常(错误处理)分散在各处进行处理不利于代码的维护,尤其是对于在不同地方发生的同一种异常,都要编写相同的处理代码,也是一种不必要的重复和冗余,如果能在发生各种异常时让程序都执行到同一个地方,这些地方能够对所有异常进行集中处理,则程序就会更容易编写和维护
鉴于上述原因,C++引入了异常处理机制
其基本思想是:函数A在执行过程中发现异常时可以不加以处理,而只是"抛出一个异常"给A的调用者,假设为B。抛出异常而不加以处理会导致函数A立即中止,在这种情况下,函数B可以选择捕获A抛出的异常进行处理,也可以选择置之不理,如果置之不理,这个异常就会被抛给B的调用者,以此内推如果一层层的函数都不处理异常,异常最终会被抛给最外层的main函数,main函数应该处理异常,如果main函数也不处理异常,那么程序就会立即异常的中止
一层一层的异常传递就是一个通信的过程异常对象就是传递的内容,存在类型和内容
C++中异常处理机制由三部分组成:a. 检测异常 --- 测试这些代码是不是出了问题
try
b. 抛出异常 --- 有错误了我们需要将一些异常值给抛出来,被检测到
throw
c. 捕获异常
catch
C++引入了throw,try,catch关键字用于异常处理
通常将一组可能产生异常的代码放在try包含的块内,若try块中的某一行发生了异常,则try块中该行后面的代码将不再执行,由块内的throw语句抛出异常(抛出异常后,该函数throw语句后面的内容将不再执行),由try后面的catch结构接收异常并处理该异常,如果抛出的异常没有匹配的catch结构,则会从上往下执行第一个符合接收条件的catch语句,如果try块中没有异常发生,则catch结构不会执行,接着执行catch后面的语句,不管有没有产生异常,都会执行catch后面的代码try { // 包含可能抛出异常的语句和函数; } catch (类型名1 [形参名1]) { // 可能出现的异常1 // 异常处理方法 } catch (类型名2 [形参名2]) { // 可能出现的异常2 // 异常处理方法 } catch(...) { // 如果不确定异常类型,在这里可以捕获所有类型异常! }
throw语句抛出异常后,由它的直接或间接调用者处理该异常,如果到最后都没有处理该异常,编译器自动调用系统的 terminate接口 直接终止程序
异常处理机制能够实现错误检测和错误处理的分离
一种思想:在所有支持异常处理的编程语言中(例如:java),要认识到的一个思想在异常处理过程中,由问题检测代码可以抛出一个对象给问题处理代码,通过这个对象的类型和内容,实际上完成了两个部分的通信,通信的内容是“出现了什么错误,该如何处理”
如果throw中抛出一个对象,那么无论在catch中使用什么接收(基类对象、引用、指针或者子类对象、引用、指针),在传递到catch之前,编译器都会另外构造一个对象的副本,也就是说,如果你以一个throw语句抛出一个对象类型,在catch处也是通过一个对象接收,那么该对象经历了两次复制,即调用了两次拷贝构造
一次是在throw时,将“抛出的对象”复制到一个“临时对象”(这一步是必须的)
然后是因为catch处使用对象接收,那么需要再从"临时对象"复制到"catch"的形参“变量中”
如果在catch中使用“引用”来接收参数,那么不需要第二次复制,即形参成为临时变量的别名
C++标准库定义一组异常类,用于报告标准库函数遇到的问题,这些异常类型也可在自己程序中使用,常见如下:
头文件:include <exception>
std::logic_error 逻辑错误
std::runtime_error 只有在运行时才能检测出的错误
std::out_of_range 范围越界
std::bad_alloc 内存分配失败
......
所有异常类型只定义了一个名为 what 的成员函数,该函数没有任何参数,返回一个C风格的字符串 const char * 用于提供异常信息
例如:throw std::runtime_error("stack is empty"); catch (std::runtime_error &err) { cout << err.what() << endl; } ================================================ new失败,会自动抛出异常 bad_alloc try { p = new int[100000000000000000]; } catch (std::bad_alloc &err) { cout << "err: " << err.what() << endl; } catch (...) { cout << "异常" << endl; } cout << "byebye" << endl;
注意:
构造函数中可以抛出异常,普通的成员函数也可以抛出异常
析构函数不能抛出异常!析构函数默认就是 noexcept 的
如果一个函数承诺不抛出异常,可以在函数参数列表后加 noexcept 来进行说明
noexcept 是一种承诺,承诺该函数不抛出异常
如果一个函数用 noexcept 说明了,然后又在函数内部抛出了异常,编译时会有警告,执行时,如果调用了该函数,系统直接终止该进程
#include <iostream> #include <string> #include <exception> // 异常相关的头文件 using namespace std; class Test { private: int m_a; public: Test(int a) : m_a(a) { cout << "构造函数" << endl; } Test(const Test &t) : m_a(t.m_a) { cout << "拷贝构造函数" << endl; } ~Test() { cout << "析构函数" << endl; } void show() const noexcept { // noexcept表示这个函数承诺不抛出异常 cout << "m_a = " << m_a << endl; } }; // 判断一个浮点数是否为0 bool isZero(double a) { const double delta = 0.000001; // 误差范围 return a - 0 < delta && a - 0 > -delta; } // 除法函数 double divide(double a, double b) { // 判断除数是否为0,如果除数为0则抛出一个异常 if (isZero(b)) { // 抛出异常,异常实际上是一个参数 // 参数是有类型和数值的 // int x = 10; // throw x; // throw double{10.1}; throw Test{10}; // 会在异常处理完成之后,才会结束生存期 // throw std::runtime_error{"除数不能为0"}; // 如果上面的throw语句执行了,则下面的代码不会执行 cout << "除数不能为0" << endl; // 不会执行 } return a / b; } int main() { double a = 10, b = 0; try { cout << "a / b =: " << divide(a , b) << endl; cout << "除法成功" << endl; // 如果上面的除法成功,则执行这行代码,否则不执行 } catch (int x) { // 接收try中所有产生的int异常 cout << "异常类型为int" << endl; cout << "捕获到异常,异常的数值为:" << x << endl; } catch (double x) { // 接收try中所有产生的double异常 cout << "异常类型为double" << endl; cout << "捕获到异常,异常的数值为:" << x << endl; } // const catch (Test t) // 一般使用常引用 catch (const Test &t) { // 接收try中所有产生的Test异常 cout << "异常类型为Test" << endl; t.show(); } catch (std::runtime_error &w) { // 接收try中所有产生的runtime_error异常 cout << "异常类型为runtime_error" << endl; cout << "捕获到异常,异常的描述为:" << w.what() << endl; } catch (...) { // 接收try中所有产生的异常 cout << "所有的异常类型" << endl; } cout << "程序结束" << endl; // 不管上面的除法是否成功,这一行代码都会执行 return 0; } // (1)使用引用 // g++ 0.cpp /* a / b =: 构造函数 异常类型为Test m_a = 10 析构函数 程序结束 */ // g++ 0.cpp -fno-elide-constructors /* a / b =: 构造函数 拷贝构造函数 析构函数 // 析构Test{10} 异常类型为Test m_a = 10 析构函数 程序结束 */ // (2)不使用引用 // g++ 0.cpp /* a / b =: 构造函数 拷贝构造函数 异常类型为Test m_a = 10 析构函数 析构函数 // 析构Test{10} 程序结束 */ // g++ 0.cpp -fno-elide-constructors /* a / b =: 构造函数 拷贝构造函数 析构函数 // 析构Test{10} 拷贝构造函数 异常类型为Test m_a = 10 析构函数 析构函数 // 将“抛出的对象”复制到一个“临时对象”,析构产生的临时变量 程序结束 */