【C++基础】第二十八课:类型转换

隐式转换,显式转换,命名的强制类型转换,static_cast,dynamic_cast,const_cast,reinterpret_cast,旧式的强制类型转换

Posted by x-jeff on August 10, 2021

【C++基础】系列博客为参考《C++ Primer中文版(第5版)》C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.前言

1
int ival=3.541+3;//编译器可能会警告该运算损失了精度

C++语言不会直接将两个不同类型的值相加,而是先根据类型转换规则设法将运算对象的类型统一后再求值。上述的类型转换是自动执行的,因此,也被称为隐式转换(implicit conversion)

‼️算术类型之间的隐式转换被设计得尽可能避免损失精度。很多时候,如果表达式中既有整数类型的运算对象也有浮点数类型的运算对象,整型会转换成浮点型。在上面的例子中,3转换成double类型,然后执行浮点数加法,所得结果的类型是double。然后在接下来的初始化阶段,加法运算得到的double类型的结果转换成int类型的值(忽略掉小数部分),这个值被用来初始化ival

2.算术转换

算术转换(arithmetic conversion)的含义是把一种算术类型转换成另外一种算术类型。算术转换的规则:运算符的运算对象将转换成最宽的类型。

2.1.整型提升

整型提升(integral promotion)负责把小整数类型转换成较大的整数类型。⚠️转换后的类型要能容纳原类型所有可能的值。

2.2.无符号类型的运算对象

如果一个运算对象是无符号类型、另外一个运算对象是带符号类型,而且其中的无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的。例如,假设两个类型分别是unsigned int和int,则int类型的运算对象转换成unsigned int类型。需要注意的是,如果int型的值恰好为负值,则可能会发生的情况:链接

⚠️剩下的一种情况是带符号类型大于无符号类型,此时转换的结果依赖于机器。如果无符号类型的所有值都能存在该带符号类型中,则无符号类型的运算对象转换成带符号类型。如果不能,那么带符号类型的运算对象转换成无符号类型。例如,如果两个运算对象的类型分别是long和unsigned int,并且int和long的大小相同,则long类型的运算对象转换成unsigned int类型;如果long类型占用的空间比int更多,则unsigned int类型的运算对象转换成long类型。

算术类型的尺寸在不同机器上有所差别。

3.其他隐式类型转换

除了算术转换之外还有几种隐式类型转换。

3.1.数组转换成指针

在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针:

1
2
int ia[10];//含有10个整数的数组
int* ip=ia;//ia转换成指向数组首元素的指针

⚠️当数组被用作decltype关键字的参数,或者作为取地址符(&)、sizeof及typeid等运算符的运算对象时,上述转换不会发生。同样的,如果用一个引用来初始化数组,上述转换也不会发生。

1
2
3
4
5
int ia[] = {1, 2, 3};
auto iaa = &ia;//iaa的类型为:int(*) [3],即指向一个整型数组的指针
cout << (*iaa)[0] << endl;//0
cout << (*iaa)[1] << endl;//1
cout << (*iaa)[2] << endl;//2

3.2.指针的转换

  1. 常量整数值0或者字面值nullptr能转换成任意指针类型。
  2. 指向任意非常量的指针能转换成void*。
  3. 指向任意对象的指针能转换成const void*。

3.3.转换成布尔类型

存在一种从算术类型或指针类型向布尔类型自动转换的机制。如果指针或算术类型的值为0,转换结果是false;否则转换结果是true。

3.4.转换成常量

如果T是一种类型,我们就能将指向T的指针或引用分别转换成指向const T的指针或引用。相反的转换并不存在,因为它试图删除掉底层const

3.5.类类型定义的转换

例如:

1
2
string s,t="a value";//字符串字面值转换成string类型
while (cin>>s)//while的条件部分把cin转换成布尔值

4.显式转换

有时我们希望显式地将对象强制转换成另外一种类型。例如,如果想在下面的代码中执行浮点数除法:

1
2
int i,j;
double slope=i/j;

就要使用某种方法将i和/或j显式地转换成double,这种方法称作强制类型转换(cast)

4.1.命名的强制类型转换

一个命名的强制类型转换具有如下形式:

cast-name<type>(expression);

其中,type是转换的目标类型而expression是要转换的值。如果type是引用类型,则结果是左值。

‼️cast-name是static_castdynamic_castconst_castreinterpret_cast中的一种。dynamic_cast支持运行时类型识别(后续博客会有详细介绍,本文不再详述)。cast-name指定了执行的是哪种转换。

4.2.static_cast

任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。例如:

1
2
//进行强制类型转换以便执行浮点数除法
double slope=static_cast<double>(j)/i;

此外,我们还可以使用static_cast找回存在于void*指针中的值:

1
2
3
void* p = &d;//正确:任何非常量对象的地址都能存入void*
//正确:将void*转换回初始的指针类型
double *dp=static_cast<double*>(p);

我们必须确保转换后所得的类型就是指针所指的类型,类型一旦不符,将产生未定义的后果。

static_cast用于进行比较“自然”和低风险的转换,如整型和浮点型、字符型之间的互相转换。

static_cast不能用于在不同类型的指针之间互相转换,也不能用于整型和指针之间的互相转换,当然也不能用于不同类型的引用之间的转换。因为这些属于风险比较高的转换。

1
2
float* fp;
double* dp= static_cast<double*>(fp);//错误

4.3.const_cast

const_cast只能改变运算对象的底层const

1
2
const char *pc;
char *p = const_cast<char*>(pc);//正确:但是通过p写值是未定义的行为

对于将常量对象转换成非常量对象的行为,我们一般称其为“去掉const性质(cast away the const)”。一旦我们去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。

‼️只有const_cast能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误。同样的,也不能用const_cast改变表达式的类型:

1
2
3
4
5
const char *cp;
//错误:static_cast不能转换掉const性质
char *q=static_cast<char*>(cp);
static_cast<string>(cp);//正确:字符串字面值转换成string类型
const_cast<string>(cp);//错误:const_cast只改变常量属性

4.4.reinterpret_cast

reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。举个例子,假设有如下的转换:

1
2
int *ip;
char *pc = reinterpret_cast<char*>(ip);

reinterpret_cast用于进行各种不同类型的指针之间、不同类型的引用之间以及指针和能容纳指针的整数类型之间的转换。转换时,执行的是逐个比特复制的操作。

个人理解就是重新解释表象,本质并没有变。pc指向的仍是int型,只不过是被重新解释为char了而已,并不能真的作为char使用。因此,string str(pc);是错误的。

4.5.旧式的强制类型转换

在早期版本的C++语言中,显式地进行强制类型转换包含两种形式:

1
2
type (expr);//函数形式的强制类型转换
(type) expr;//C语言风格的强制类型转换

当我们在某处执行旧式的强制类型转换时,如果换成const_caststatic_cast也合法,则其行为与对应的命名转换一致。如果替换后不合法,则旧式强制类型转换执行与reinterpret_cast类似的功能:

1
char *pc = (char*) ip;//ip是指向整数的指针

的效果与使用reinterpret_cast一样。

5.参考资料

  1. C++强制类型转换运算符(static_cast、reinterpret_cast、const_cast和dynamic_cast)