C++之类型转换

时间:2020-11-28 20:20:08

类型转换

表达式是否合法取决于操作数的类型,而且合法的表达式其含义也由其操作数类型决定。但是,在C++中,某些类型之间存在相关的依赖关系。若两种类型相关,则可在需要某种类型的操作数位置上,使用该类型的相关类型对象或值。如果两个类型之间可以相互转换(conversion),则称这两个类型相关。

考虑下面的例子:

int ival = 0;
ival = 3.541 + 3; //typically compiles with a warning

ival的值为6。

首先做加法操作,其操作数是两个不同类型的值:3.541 是 double 型的字面值常量,而 3 则是 int 型的字面值常量。C++并不是把两个不同类型的值直接加在一起,而是提供一组转换规则,以便在执行算术操作之前,将两个操作数转换为同一种数据类型。这些转换规则由编译器自动执行,无需程序员介入–有时甚至不需要程序员了解。因此,它们也被称为隐式类型转换(implicit type conversion).

C++定义了算术类型之间的内置转换以尽可能防止精度损失。通常,如果表达式的操作数分别为整型和浮点型,则整型的操作数被转换为浮点型。本例中,整数 3 被转换为 double 类型,然后执行浮点类型的加法操作,得 double 类型的结果 6.541.

下一步是将 double 类型的值赋给 int 型变量 ival。在赋值操作中,因为不可能更改左操作数对象的类型,以为左操作数的类型占主导地位。如果赋值操作的左右操作数类型不相同,则右操作数会被转换为左边的类型。本例中,double 型的加法结果转换为 int 型。double 向 int 的转换自动按截尾形式进行,小数部分被舍弃。于是 6.541 变成 6 ,然后赋给 ival 。因为从 double 到 int 的转换会导致精度损失,因此大多数编译器会给出警告。例如,本书所用的测试例程的编译器给出如下警告:

warning: assignment to 'int' from 'double'

为了理解饮食类型转换,我们需要知道它们在什么时候发生,以及可能出现什么类型的转换。

何时发生隐式类型转换

编译器在必要时将类型转换规则应用到内置类型和类类型的对象上。在下列情况下,将发生隐式类型转换:

  • 在混合类型的表达式中,其操作数被转换为相同类型
int ival;
double dval;
ival >= dval // ival converted to double
  • 用作条件的表达式被转换为bool类型
int ival;
if (ival) //ival converted to bool
while (cin) //cin converted to bool
条件表达式(?:)中的第一个操作数以及逻辑非(!)、逻辑与(&&)和逻辑或(||)的操作数都是条件表达式。出现在if、while、for和do while语句中的同样也是条件表达式。
  • 用一表达式初始化某个变量,或将一表达式赋值给某个变量,则该表达式被转换为该变量的类型
int ival = 3.14; // 3.14 converted to int
int *ip;
ip = 0; // the int 0 converted to a null pointer of type int *

另外,在函数调用中也可能发生隐式类型转换。

算术转换

C++ 语言为内置类型提供了一组转换规则,其中最常用的是算术转换(arithmetic conversion)。算术转换保证在执行操作之前,将二元操作符(如算术或逻辑操作符)的两个操作数转换为同一类型,并使用表达式的值也具有相同的类型。
算术转换规则定义了一个类型转换层次,该层次规定了操作数应按什么次序转换为表达式中最宽的类型。在包含多种类型的表达式中,转换规则要确保计算值的精度。例如,如果一个操作数的类型是 long double,则无论另一个操作数是什么类型,都将被转换为long double。
最简单的类型转换为整型提升(integral promotion):对于所有比int小的整数,包括 char、signed char、unsigned char、short和unsigned short,如果该类型的所有可能的值都能包容在int内,他们就会被提升为 int 型,否则,它们将被提升为 unsigned int。如果将 bool 值提升为 int,则 false 转换为 0,而 true 则转换为 1。

有符号与无符号类型之间的转换

若表达式中使用了无符号(unsigned)数值,所定义的转换规则需要保护操作数的精度。unsigned 操作数的转换依赖于机器中整型的相对大小,因此,这类转换本质上依赖于机器。
包含 short 和 int 类型的表达式,short 类型的值转换为 int。如果 int 型足够表示所有unsigned short型的值,则将unsigned short转换为 int,否则,将两个操作数均转换为unsigned int。例如, 如果short用半字表示而int用一个字表示,则所有的unsigned 值都能包含在 int 内,在这种机器上,unsigned short 转换为 int。
long 和 unsigned int转换也是一样的。只要机器上的long型足够表示unsigned int型的所有值,就将unsigned int转换为long型,否则,将两个操作数均转换为unsigned long。
在32位的机器上,long 和 int 通常用一个字长表示,因此当表达式包含unsigned int 和 long两种类型,其操作数都应转换为unsigned long型。
对于包含signed 和 unsigned int型的表达式,其转换可能出乎我们的意料。表达式中的signed 型数值会被转换为unsigned型。

其他隐式转换

指针转换

在使用数组时,大多数情况下数组都会自动转换为指向第一个元素的指针:

itn ia[10];      //array of ints
int* ip = ia; //convert ia to pointer of first element

不将数组转换为指针的例外情况有:数组用作取地址(&)操作符的操作数或 sizeof 操作符的操作数时,或用数组对数组的引用进行初始化时,不会将数组转换为指针。C++还提供了另外两种指针转换:指向任意数据类型的指针都可转换为 void* 类型;整型数值常量 0 可转换为任意指针类型。

转换为 bool 类型

算术值和指针值都可以转换为 bool 类型。如果指针或者算术值为0,则其 bool 值为 false,而其他值则为true:

算术类型与 bool 类型的转换

可将算术对象转换为 bool 类型, bool 对象也可转化为 int 型。将算术类型转换为 bool 型时,零转换为 false,而其他值则转换为 true 。将 bool对象转换为算术类型时,true变成 1,而 false则为 0:

转换与枚举类型

C++自动将枚举类型的对象或枚举成员转换为整型,其转换结果可用于任何要求使用整数值的地方。
将 enum 对象或枚举成员提升为什么类型由机器定义,并且依赖于枚举成员的最大值。无论其最大值是什么,enum 对象或者枚举成员至少提升为 int 型。如果 int 型无法表示枚举成员的最大值,则提升到能表示所有枚举成员值得,大于 int 型的最小类型(unsigned int、 long或unsigned long)。

转换为const对象

当使用非const对象初始化const对象的引用时,系统将非const对象转换为const对象。此外,还可以将非const对象的地址(或非const指针)转换为指向相关const类型的指针:

int i;
const int ci = 0;
const int &j = i; //ok:convert non-const to reference to const int
const int *p = &ci; // ok: convert address of non-const to address of a const

由标准库类型定义的转换

类类型可以定义由编译器自动执行的类型转换。迄今为止,我们使用过的标准库类型中,有一个重要的类型转换。从istream中读取数据,并将此表达式作为while循环条件:

string s;
while (cin >>s)

这里隐式使用了 IO 标准库定义的类型转换。在与此类似的条件中,求解表达式 cin>> s,即读 cin。 无论读入是否成功,该表达式的结果都是 cin。
while循环条件应为bool类型的值,但此时给出的却是istream类类型的值,于是istream类型的值应转换为bool类型。将istream类型转换为 bool类型意味着要检验流的状态。如果最后一次读入cin的尝试是成功的,则流的状态将导致上述类型转换为 bool 类型后获得 true值–while循环条件成立。如果最后一次尝试失败,比如说已经读到文件尾了,此时将istream类型转换为bool类型后得false,while循环条件不成立。

显示转换

显示转换也称为强制类型转换(cast),包括以下列名字命名的强制类型转换操作符:**static_cast、dynamic_cast、const_cast和 reinterpret_cast.

dynamic_cast

dynamic_cast支持运行时识别指针或引用所指向的对象。

const_cast

const_cast ,顾名思义,将转换掉表达式的const性质。

static_cast

编译器隐式执行的任何类型转换都可以由 static_cast显式完成:

double d = 97.0;
//cast specified to indicate that the conversion is intentional
char ch = static_cast<char> (d);

当需要将一个较大的算术类型赋值较小的类型时,使用强制转换非常有用。此时,强制类型转换告诉程序的读者和编译器:我们知道并且不关心潜在的精度损失。对于一个较大的算术类型到一个较小类型的赋值,编译器通常会产生警告。当我们显式地提供强制类型转换时,警告信息就会关闭。
如果编译器不提供自动转换,使用 static_cast来执行类型转换也是很有用的,例如,下面的程序使用static_cast 找回存放在void*指针中的值:

void* p = &d;//ok: address fo any data object can be stored in a void*
//ok: converts void* back to the original pointer type
double *dp = static_cast<double*>(p);

可通过static_cast 将存放在 void* 中的指针值强制转换为原来的指针类型,此时我们应确保保持指针值。也就是说 ,强制转换的结果应与原来的地址值相等。

reinterpret_cast

reinterpret_cast 通常为操作数的位模式提供较低层次的重新解释。
例如,对于下面的强制转换:

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

程序员必须永远记得 pc 所指向的真实对象其实是 int型,而并非字符数组。任何假设pc是普通字符指针的应用,都有可能带来有趣的运行时错误。例如,下面语句用 pc 来初始化一个string对象:

string str(pc);

它可能会引起运行时的怪异行为。
用 pc 初始化 str 这个例子很好地说明了显示强制转换时多么的危险。问题源于类型已经改变时编译器没有提供任何警告或错误提示。当我们用 int 型地址初始化 pc 时,由于显式地声明了这样的转换时正确的,因此编译不提供任何错误或警告信息。后面对pc的使用都假设它存放的是 char*型对象的地址,编译器确实无法知道 pc 实际上是指向int型对象的指针。因此用pc初始化 str是完全正确的–虽然实际上是无意义的或是错误的。查找这类问题的原因相当困难,特别是如果ip到pc的强制转换和使用pc初始化string对象这两个应用发生在不同文件中的时候。

建议:避免使用强制类型转换

强制类型转换关闭或者挂起了正常的类型检查。强烈建议程序员避免使用强制类型转换,不依赖强制类型转换也能写出很好的C++程序。
这个建议在如何看待reinterpret_cast 的使用时非常重要。此类型转换总是非常危险的。相似地,使用 const_cast 也总是预示着设计缺陷。设计合理的系统应不需要使用强制转换抛弃const特性。其他的强制转换,如static_cast 和 dynamic_cast,各有各的用途,但都不应频繁使用。每次使用强制类型转换前,程序员应该仔细考虑是否还有其他不同的方法可以达到同一目的。如果非强制转换不可,则应限制强制转换值得作用域,并且记录所有假定涉及的类型,这样能减少错误发生的机会。