隐式转换

C++语言不会直接将两个不同类型的值相加,而是先根据类型转换规则设法将运算对象的类型统一后再求值。上述的类型转换是自动执行的,无须程序员的主动介入,因此,它们被称作隐式转换

1
int ival = 3.5 + 3 // 编译器警告,精度丢失

算术类型之间的隐式转换被设计得尽可能避免损失精度。比如在表达式中既有int,也有double时,int会转化为double,然后执行浮点数的加法,最后得到double。将double用于初始化int时,加法运算得到的double类型转化成int类型的值,这个值被用来初始化ival。

何时发生?

  • 在大多数表达式中,小的整型值首先提示为较大的整数类型。
  • 在条件中,非布尔值转换为布尔类型。
  • 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
  • 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一中类型。
  • 函数调用时也会发生类型转换。

算术转换

算术转换的含义是把一种算术类型转换成另外一种算术类型。算术转换的规则定义了一套类型转换的层次,其中运算符的运算对象将转换成最宽的类型。当表达式中既有浮点类型也有整数类型时,整数值将转换成相应的浮点类型。

整型提升

整型提升负责把小整数类型转换成较大的整数类型。对于bool,char,signed char,unsigned char,short和unsigned short等类型来说,只要它们所有可能的值都能存在int里,它们就会提升成int类型;否则提升成unsigned int类型。

1
2
3
4
5
6
int main() {
short a = 1, b = 1;
cout << sizeof(a) << endl; // 2
cout << sizeof(b) << endl; // 2
cout << sizeof(a + b) << endl; // 4,哪怕是两个short相加,也会首先进行整型提升。
}

无符号类型的运算对象

首先会执行整型提升。如果结果的类型相匹配,无须进行进一步的转换,如果两个(提升后的)运算对象的类型要么都是带符号的,要么都是无符号的,则小类型的运算对象转换成较大的类型。

如果一个运算对象是无符号类型,另外一个是带符号的,而且其中无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的。例如int和unsigned int,则int类型的运算对象转换成unsigned int类型,如果int恰好是负值,则会出现问题。

1
2
3
int a = 1;
unsigned int b = 1;
auto x = a + b // unsigned int

如果带符号类型大于无符号类型,则依赖于机器。如果无符号类型的所有值都能存在该带符号类型中,则无符号类型的运算对象转换成带符号类型。如果不能,那么带符号类型的运算对象转换成无符号类型。

其他隐式类型转换

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

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

当数组被用作decltype关键字的参数,或者作为取地址符,sizeof及typeid等运算符的运算对象时,上述转换不会发生。

指针的转换:0和nullptr能转换成任意指针类型;指向任意非常量的指针能转换成void*;指向任意对象的指针能转换成const void*。

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

转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。

1
2
3
int i;
const int &j = i;
const int *p = &i;

类类型定义的转换:类类型能定义由编译器自动执行的转换。

1
2
string s, t = "a value"; // 字符串字面量转换成string
while(cin >> s); // 把cin转换成布尔值。

显式转换

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

cast-name<type>(expression)

  • type:转换的目标类型
  • expression:要转换的值
  • cast-name:static_cast,dynamic_cast,const_cast,reinterpret_cast中的一种。

static_cast

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

1
2
3
4
5
int i, j;
double ans = static_cast<double>(j)/i;

void* p = &d;
double *dp = static_cast<double*>(p);

当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用,我们使用它时,编译器不再给出警告信息。

const_cast

const_cast只能改变运算对象的底层const。目标类型必须是一个指针,引用或者指向对象类型成员的指针。

1
2
const char *pc;
char *p = const_cast<char*>(pc);

我们可以通过const_cast将常量对象转换成非常量对象。一旦我们去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的。如果对象是一个常量,再使用const_cast执行写操作就会产生未定义行为。

1
2
3
4
const char *p;
char *q = static_cast<char*>(cp); // 错误:static_cast 不能转换掉const性质
static_cast<string>(cp); // 正确:但转换后仍具有const性质
const_cast<string>(cp); // 错误: const_cast只改变常量属性

reinterpret_cast

reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。(听起来有点抽象)

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

我们可以将int指针转换为char指针,我们使用char指针时,是对int对象这块内存使用char类型的读取方式。

1
2
3
4
5
6
7
8
int num = 0x00636261; // 转换成ascii码,分别是'\0','c','b','a'
int *pnum = &num;
char *pchar = reinterpret_cast<char*>(pnum);
cout << pnum << endl; // 0x7fffe4e9ac84
cout << static_cast<void*>(pchar) << endl; // 0x7fffe4e9ac84,两个指针指向了同一块内存空间
cout << *pnum << endl; // 对pnum解引用,取出4字节,所以输出6513249
cout << *pchar << endl; // 对pchar解引用,因为是char类型,取一个字节,输出a
cout << pchar << endl; // 相当于输出字符串,直到遇到'\0'为止,所以输出abc

我们将num的高位设置成了’\0’所以成功输出了abc,如果我们将其置为其他值,由于没有结束符,会造成段错误,输出abcT�xW�。

使用reinterpret_cast是非常危险的操作,通常我们应该避免使用它。

dynamic_cast

dynamic_cast用于将基类的指针或引用安全地转换成派生类的指针或引用。一般适用于以下情况:我们想使用基类对象的指针或引用执行某个派生类操作并且该操作不是虚函数。一般来说,只要有可能我们应该尽量使用虚函数。当操作被定义成虚函数时,编译器将根据对象的动态类型自动地选择正确的函数版本。

使用形式

1
2
3
dynamic_cast<type*>(e);
dynamic_cast<type&>(e);
dynamic_cast<type&&>(e);

其中,type必须是一个类类型,并且通常情况下该类型应该含有虚函数。在第一种形式中,e必须是一个有效的指针;在第二种形式中,e必须是一个左值;在第三种形式中,e不能是左值。

转换条件

e的类型必须符合以下三个条件中的任意一个:

  • e的类型是目标type的公有派生类。
  • e的类型是目标type的公有基类。
  • e的类型就是目标type的类型。

如果符合则转换成功,否则转换失败。如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0。如果转换目标是引用类型并且失败了,则dynamic_cast运算符将抛出一个bad_cast异常。

TBD