在C++中,如果一个运算符的运算对象类型不一致,这些运算对象将转换成同一种类型。
类型转换分为隐式转换、和显式转换。
首先我们先来了解一下C++中内置类型的含义和标准定义大小:
类型 | 含义 | 最小尺寸 |
---|---|---|
bool | 布尔类型 | 未定义 |
char | 字符 | 8位 |
wchar_t | 宽字符 | 16位 |
char16_t | Unicode字符 | 16位 |
char32_t | Unicode字符 | 32位 |
short | 短整型 | 16位 |
int | 整型 | 16位 |
long | 长整型 | 32位 |
long long | 长整型 | 64位 |
float | 单精度浮点型 | 6位有效数字 |
double | 双精度浮点型 | 10位有效数字 |
long double | 扩展精度浮点型 | 10位有效数字 |
下面这段代码可以用来检测不同系统(32bit和64bit)上内置类型之间的大小
1 | printf("%11s\t%3d byte\t%3d bit\n","bool",sizeof(bool),sizeof(bool)*8); |
隐式类型转换
在C++中触发**(隐式)类型转换**的三种情况:
- 混合类型表达式中,其操作数被转换为相同的类型。(整型提升)
- 用作条件表达式被转换为bool类型(非0即true)
- 用一表达式初始化某个变量,或将一表达式赋值给某个变量,则该表达式的值被转换为该变量的类型。
在C++中**(隐式)类型转换**基本遵循以下几点:
- 整型提升:将小整数类型转换为较大的整数类型.
- 截断:从高精度向低精度转换时发生截尾动作(比如从float->int是将float的小数点后部分舍去)
- 算数类型到bool类型:非0即true,其余皆为false.
- signed到unsigned之间的转换:具有副作用(将负值赋值给unsigned类型)
整型提升(intergral promotion)
对于bool
,char
,signed char
,unsigned char
,short
和unsigned short
等类型来说,只要它们所有可能的值都能存在int
里,他们就会提升至int
类型,否则提升为unsigned int
类型
较大的char
类型(wchar_t
,char16_t
,char32_t
)提升为int
,unsigned int
,long
,unsigned long
,long long
和unsigned long long
中最小的一种类型,前提是转换后的类型必须保证能够容纳原类型所有可能的值.
如果位域的全部值都能用int表示,则它转换为int;否则,如果全部值能用unsigned int表示,则它转换为unsigned int;如果int和unsigned int都不行,则不执行任何整型提升。位域
:可以使用struct来指定成员所占的位数,就能把它定义成位域了。
1 | // 一个32bit位域示例 |
bool值转换为int,其中,false变为0,而true变为1.
浮点类型的转换
关于浮点数运算和舍入可以参照一下这两篇文章:IEEE 754和两个迭代器初始化容器出现浮点数舍入
给定一个浮点值,我们能把它转换成其他浮点类型的值。如果原值能用目标类型完整地表示,则所得结果与原值相等。如果原值介于两个相邻的目标值之间,则取他们中的一个。其他情况下,结果是未定义的。
1 | double d1=DBL_MAX; //最大的双精度浮点值 |
高精度到低精度的转换为截尾。
1 | double dPI=3.14159; |
低精度到高精度的转换为扩充精度。
1 | int ival=3; |
另外,C++早期版本允许结果为负值的上向上或向下取整,C++11新标准规定,商一律向0取整(即截尾->直接切除小数部分)
1 | int x=-12; |
通过使用numeric_limits
能确保截断以一种可移植的方式进行。在初始化过程中,{}
初始化器,有助于避免截断的发生。
无符号类型(unsigned type)间的转换
如果目标类型是unsigned的,则结果值所占的二进制位数以目标类型(如果有必要会丢掉靠前的二进制位)。更准确的说,转换前的整数值对$2^n$取模后的结果值就是转换结果,其中n是目标类型所占的位数。
1 | // 二进制1111111111:uc的值变为二进制111111,即255 |
如果目标类型是signed的,则当原值能用目标类型表示时,它不发生改变;反之,结果值依赖于具体实现:
1 | // 依赖于实现,结果是127或者-1 |
bool值或者普通枚举类型的值能隐式转换为等值的整数类型。
如果某个运算对象时无符号类型,那么转换的结果就要依赖于机器中各个整数类型的相对大小了。
因为标准规定int不小于short,long不小于int,long long不小于long,有一种可能是int(缺省为signed)**有可能存不下unsigned short或者long存不下unsigned int、long long** 存不下unsigned long,对于这种情况要特别注意一下。
当表达式包含short和int时,short转换为int
当int型足够表示所有unsigned short类型的值,则将unsigned short转换为int,否则将两个操作数均转换为unsigned int.
unsigned int->long和unsigned long->long long的转换也同理。
另外还需要注意的是:
- 赋给unsigned类型一个值:**结果是初始值对无符号类型表示数值的总数$2^n$(比如unsigned char能容纳从0~255,则它的能容纳数值的个数为256个)**取模后的结果(上面已经提到)
- 将signed(缺省)类型转换为unsigned类型有可能会导致副作用(即将负数赋值给unsigned)
- 赋给signed类型一个超出他范围的值:**其结果是未定义的**,
1 | unsigned char x=258; |
将负数赋值给无符号(unsigned)类型
负数在计算机中的表示方式涉及到补码的概念,在这里不赘述,详细可以看下这几篇文章:IEEE 754:二进制浮点数算术标准,关于2的补码,原码, 反码, 补码 详解
在此我们先简单了解一下,原码反码和补码的基本概念:
**原码:**符号及值(sign & magnitude)的处理办法是分配一个符号位(sign bit)来表示这个符号:设置这个位(通常为最高有效位)为0表示一个正数,为1表示一个负数。
**反码:**一个数的二进制数反码形式为其绝对值部分按位取反(即符号位不变,其余各位按位取反)。
**补码:**一个数的补码在该数大于或等于0时与原码相同,若该数小于零,则是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)
有符号数据在计算机中通常是以补码形式编码的,因为不论是原码还是反码对于0都有两种表示方式-0
和+0
。
我们以最简单的8bit来描述一下原码反码和补码:0x16
:
原码:0001 0110
反码:0001 0110
补码:0001 0110-0x16
原码:1001 0110
反码:1110 1001
补码:1110 1010
我们来测试一下将一个负数赋值给unsigned类型:
1 | // 为了方便解释,在此使用1byte的char |
编译运行得到的结果为:254
因为一个数的补码即是该数在计算机中的存储的方式,所以将负数的赋值给unsigned类型时,会丧失符号位的作用(unsigned和signed的char类型均为8bit,所以将signed类型赋值给unsigned时,会将符号位当做数值的2进制数据的一员赋值给unsigned类型),使负数的符号位表示的也是数值,所以负数赋值给unsigned类型后所存储的实际值是它的能容纳数值的个数减去将该负数的绝对值
将-2赋值给unsigned char图示:
测试一下其他的类型:
假定一个平台上int型变量的大小为4byte,即32bit,则其能表示的数的范围为[−2147483648($-2^{31}$),21474836487($2^{31}$-1)]
我们就选一个数吧:−111
[原]1 0000000000000000000000001101111
[反]1 1111111111111111111111110010000
[补]1 1111111111111111111111110010001
我们来推测一下:将其赋值给unsigned int变量后的值为4294967185
用我自己造进制转换的轮子来验证一下:
binary 11111111111111111111111110010001 conver to decimalism is:4294967185
1 | unsigned int x = -111; |
运行结果:
数组转换成指针
在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针。
1 | // 含有10个int型元素的数组 |
当数组用作decltype
关键字的参数,或者作为取地址符(&)**、sizeof及typeid**等运算符的运算对象时,上述转换不会发生。
同样的,如果用一个引用来初始化数组,上述转换也不会发生。
1 | int *ia[10]; |
指针的转换
C++还规定了几种其他指针的转换的方式。
- 常量整数值0或者字面值
nullptr
(C++11特性),能转换成任意指针类型。 - 指向任意非常量的指针能转换成
void*
- 指向任意对象的指针能转换成
const void*
- 具有继承关系的类之间的指针转换:可以把派生类对象或者派生类对象的引用在需要基类引用的地方,也可以把派生类对象的指针用在需要基类指针的地方。
注意:指向函数的指针和指向成员的指针不能隐式地转换为void*;以及不存在从指向到数值类型的转换。
求值结果为0的常量表达式能隐式地转换为任意指针类型的空指针。类似地,求值结果为0的常量表达式也能隐式地转换成指向成员的指针类型。例如:
1 | int *p=(1+2)*(2*(1-1)); //正确,但让人奇怪 |
最好直接使用nullptr
T*
可以隐式地转换为const T*
,类似地,T&
能隐式地转换成const T&
。
具有继承关系的类之间的指针转换
具有继承关系的类之间的指针转换具有另一种方式:
1 | Base item; // 基类对象 |
这种转换称为**派生类到基类(derived to base)**类型转换。和其他类型转换一样,编译器会隐式地执行派生类到基类的转换。
指向派生类的指针(或引用)能隐式地转换成指向其可访问的且明确无二义的基类的指针(或引用)。
这种隐式特性意味着:
- 可以把派生类对象或者派生类对象的引用在需要基类引用的地方
- 也可以把派生类对象的指针用在需要基类指针的地方
派生类对象中含有与其基类对应的组成部分,这一事实是继承的关键。
bool类型的转换
指针、整数和浮点数都能隐式地转换为bool类型。非0值对应true,0值对象false.
指针隐式转换为bool型时,只有当其为nullptr
/NULL
时为false。
1 | int *p=nullptr; |
将一个非bool型的值赋值给bool型时,只有初始值为0时结果为false否则即为true.
1 | //仅当test=0时才为fasle,当test_bool>0||test_bool<0时均为true |
将一个bool类型值赋值给一个非bool类型值时,初始值为flase则结果为0,初始值为true结果为1.
1 | int boolToInt_1=true; |
枚举类型(enum)的转换
C++自动将枚举成员(enumerator)转换为整型,其转换结果可以用于任何要求使用整数值的地方。
1 | // point2d is 2,point2w is 3,point3d is 3,point3w is 4 |
需要注意以下几点
- 将enum对象或者枚举成员提升为什么类型有机器定义(取决于机器类型能否容纳),并且依赖于枚举成员的最大值
- enum对象或者枚举成员至少提升为int型
- 如果int型无法表示枚举成员的最大值(可以使用auto来推断能容纳枚举成员的类型),则提升到能够表示所有枚举成员值的、大于int型的最小类型(unsigned int,long,unsigned long,long long,unsigned long long)
转换为常量(const)对象
- 当使用非const对象初始化const对象的引用时,系统将非const对象转换为const对象。
- 还可以将指向非const对象的指针(或非const指针)转换为指向const对象的指针。
- 不允许将const对象转换为非const对象
1 | int i=10; |
非const指针被转换为const指针:
1 | int i=10; |
如果T是一种类型,我们就能将指向T的指针或引用分别转换成指向const T的指针或引用。
1 | int i; |
相反的转换并不存在,因为它试图删除掉底层const
显式类型转换
有时我们希望显式地将对象转换成另一种类型(比如两个整型的值进行除法运算结果的类型也为整型,舍弃了小数点后部分的精度,这有时并不是我们想要的结果),这时就需要用到显式类型转换。
C++的显式类型转换操作指定了几种不同能力范围的转换操作,对于转换权限的控制更强,虽然这四种转换操作的行为都可以用C风格转换实现,但是为了更安全还是使用显式类型转换。
C-Style的显式类型转换
在C语言中,具有**指派运算符(type)**,在我们想要类型转换的时候可以用它来强制转换类型。
参照如下代码:
1 | int x=12,y=5; |
如果我们不使用指派运算符的话,输出z的结果为2.000000,而我们使用了指派运算符的作用即是先将x转换为float类型,然后再执行除法运算,因为不同类型在同一个表达式中的类型转换隐式是从低精度到高精度转换的,所以y也会被转换成float型(隐式),所以使用指派运算符的结果为:2.400000
早期的C++还支持
type(expression)
这样的方式进行类型转换。
C++中的显式类型转换
C++虽然支持C-Style的显式类型转换(指派运算符),但是C++中有更好的替代方案。C++提供了几种不同的方式来进行显式类型转换。
一个命名的强制类型转换具有如下形式:
1 | cast-name<type>(expression); |
其中,cast-name执行了执行的是哪种转换,type是转换的目标类型,而expression是要转换的值。
- 如果type是引用类型,则结果是左值。
- cast-name是
static_cast
,dynamic_cast
,const_cast
,reginterpret_cast
中的一种。
static_cast
static_cast的作用是反转一个定义良好的隐式类型转换。
任何具有明确定义的类型转换,只要不包含底层const(表示指针所指向的对象是一个常量),都可以使用static_cast.
注意:static_cast
不能处理指针间的转换,指针间的转换是位模式的转换应该使用reinterpret_cast
(伪装成另一种类型的指针):
1 | char x='a'; |
适用范围:
- 具有浮点精度损失(double -> float)**和将较大的算数类型赋值给较小的算数类型(int -> char),使用static_cast**表示我们明白并且不care这些风险:)
- 编译器无法自动执行的类型转换*(void -> otherType***).
- 当编译器发现一个较大的数据类型试图赋值给较小的数据类型时会产生警告信息,当我们使用了显式类型转换之后,警告信息就会被关闭了。
参照如下几份代码:
1 | int x=12,y=5; |
1 | double dPI=3.14159; |
1 | int x=65; |
使用static_cast找回存在的*void**指针
void*是一种特殊的指针类型,可用于存入任何非常量对象的地址。
但是void能做的操作十分有限:*和别的指针作比较、作为函数的输入或输出,赋值给另外一个void对象。*我们不能直接操作void指针所指向的对象,因为我们不知道这个对象到底是什么类型,也无法确定能在这个对象上进行什么操作。
所以,当我们拥有一个明确知道void*指向对象的类型时,但是我们仍然不能对该void类型进行操作。
现行的办法是可以使用static_cast将void转换为我们知道的该void所指向的对象的指针类型。
1 | double dval=3.1415926; |
当我们把指针存放在void*中,并且使用static_cast将其强制转换为原来的类型时,应该确保指针的值保持不变。也就是说,强制转换的结果与原始的地址值相等。
1 | // 输出dval的地址和强制转换void*到double*后指针的地址 |
输出了一样的地址。
因此,我们必须确保转换后所得的类型就是指针所指的类型。类型一旦不符,将产生未定义错误(不知道会产生什么样的结果)。
const_cast
const_cast的作用是为某些声明为const的对象获得写入的权利。
const_cast只能改变运算对象的底层const(指针指向的对象是一个常量)**,即是使常量指针所指向的对象可以被修改的操作**。
1 | const char *pchar; |
对于将常量对象转换成非常量对象的行为,称其为”去掉const性质(cast away the const)“。一旦去掉了一个对象的const性质,编译器就不在阻止我们对该对象进行写操作了。
如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为。如果对象是一个常量,再使用const_cast执行写操作就会产生未定义的后果。
底层const对象本身是一个变量:
1 | char a='a'; |
底层const对象本身是一个常量:
1 | char *a="HelloWorld"; |
const_cast用在重载函数中时很有用。
当我们的函数接收const引用的对象并返回函数操作后的该对象的引用时,因为接收的形参为const,所以函数的返回值也会是const属性,这显然有时候不是我们需要得到的结果。
假设我们需要一个需要判断并返回两个字符串中较长的那个串的时,有如下代码:
1 | // 函数的参数和返回值均为const string |
当我们提供两个非常量的string实参调用longString时,它返回的结果仍然是const string的引用。因此我们需要一种新的longString函数,当它的实参不是常量时,得到的结果是一个普通的引用,使用const_cast可以做到这一点。
1 | // 重载longString函数,其实参为非const时调用该版本,并返回非const对象 |
reinterpret_cast
reinterpret_cast
通常为运算对象的位模式提供了较低层次上的重新解释(改变位模式的函数)。
如下代码:
1 | int *ipoint; |
我们必须牢记cpoint所指向的是一个int而非char,如果把cpoint当成普通的字符指针(char*)使用就有可能在运行时发生错误。
1 | // 这样并不会报错,但是在运行中可能出现很多问题。 |
当我们不使用reinterpret_cast
而直接用ipoint来初始化string对象时,会报错:
1 | int *ipoint; |
使用reinterpret_cast是非常危险的。如上例所示,其中的问题是,类型改变了,但编译器并没有警告或者报错。由于显式声明这么做是合法的,所以编译器不会发出任何警告和错误。在使用
reinterpret_cast<char*>(ipoint);
之后我们使用cpoint时就会认定它的值是char*类型,编译器无法知道它实际存储的是指向int的指针。
还可以手动指定一个地址转换为指定类型的指针:
1 | int *p=reinterpret_cast<int*>(0xff00); |
注意:编译器并不能确定整数0xff00是否是一个有效的int型地址,因此这条语句的正确性完全依赖于程序员。
dynamic_cast
dynamic_cast
:动态地检查类层次关系。
dynamic_cast的作用是:将基类(base calss)**的指针或引用安全第一转换成派生类(继承 (Derived class)**的指针或引用。
dynamic_cast的使用形式如下:
1 | // e必须是一个有效的指针 |
其中type
必须是一个类类型,并且通常该类型具有虚函数。
在使用dunamic_cast的三种形式中,e的类型必须符合以下三个条件中的任意一个:
- e的类型是目标type的公有派生类(public)
- e的类型是目标type的公有基类
- e的类型是目标type的类型
如果符合其中之一,则可以转换成功,否则转换失败。
如果dynamic_cast语句转换目标是指针类型并且失败了,则结果为0.
如果dynamic_cast语句转换目标是引用类型并且失败了,dynamic_cast运算符将抛出一个bad_cast异常。
指针类型的dynamic_cast
假定Base类至少含有一个虚函数,Derived是Base的公有派生类型,如果有一个指向Base的指针bp,则我们可以在运行时将它转换成Derived的指针。
1 | // 判断是否转换成功,并且指针bp在if外部是无法访问的 |
如果bp指向Derived对象,则上面的类型转换初始化dp并令其指向bp所指的Derived对象。此时,在if语句的内部使用Derived操作代码是安全的,否则,类型转换的结果为0,dp为0意味着if语句的条件失败,此时else子句执行相应的Base操作。
可以对一个空指针执行dynamic_cast操作,结果是所需类型的空指针。
引用类型的dynamic_cast
引用类型dynamic_cast与指针类型的dynamic_cast在表示错误发生的方式上有所不同。
因为不存在空引用,所以对于引用类型来说无法使用于指针类型完全相同的错误报告策略。当对引用的类型转换失败时,程序抛出一个名为std::bad_cast
的异常,该异常定义在typeinfo
标准头文件中。
改写上面的程序使其使用引用类型:
1 | void f(const Base &b){ |