3.4.4 类型转换
C++丰富的类型允许根据需求选择不同的类型,这也使计算机的操作更复杂。例如,将两个short值相加涉及到的硬件编译指令可能会与将两个long值相加不同。由于有11种整型和3种浮点类型,因此计算机需要处理大量不同的情况,尤其是对不同的类型进行计算时。为处理这种潜在的混乱,C++自动执行很多类型转换:
l 将一种算术类型的值赋给另一种算术类型的变量时,C++对其值进行转换;
l 表达式种包含不同的类型时,C++将对值进行转换;
l 将参数传递给函数时,C++将对其值进行转换。
如果不知道进行这些自动转换时将发生的情况,将无法理解一些程序的结果,因此下面详细地介绍这些规则。
1. 初始化和赋值进行的转换
C++允许将一种类型的值赋给另一种类型的变量。这样做时,值将被转换为接收变量的类型。例如,假设so_long的类型为long,thirty的类型为short,而程序中包含这样的语句:
so_long=thirty;
则进行赋值时,程序将thirty的值(通常是16位)扩展为long值(通常为32位)。扩展后将得到一个新值,这个值被存储在so_long中,而thirty的内容不变。
将一个值赋给值取值范围更大的类型通常不会导致什么问题。例如,将short值赋给long变量并不会改变这个值,只是占用的字节更多而已。然而,将一个很大的long值(如2111222333)赋给float变量将降低精度。因为float只有6位有效数字,因此这个值将被四舍五入为2.11122E9。因此,有些转换是安全的,有些则会带来麻烦。表3.3列出了一些可能出现的转换问题。
表3.3 潜在的数值转换问题
转换 | 潜在的问题 |
将较大的浮点数类型转换为较小的浮点类型,如将double转换为float | 精度(有效数位)降低,值可能超出目标类型的取值范围,在这种情况下,结果将是不确定的 |
将浮点类型转换为整型 | 小数部分丢失,原来的值可能超过目标类型的取值范围,在这种情况下,结果将是不确定的 |
将较大的整型转换为较小的整型,如将long转换为short | 原来的值可能超出目标类型的取整范围,通常只复制右边的字节 |
将0赋给bool变量时,将被转换位false;而非零值将被转换位true。
将浮点值赋给整型将导致两个问题。首先,将浮点值转换位整型会将数字截短(除掉小数部分)。其次,float值对于int变量来说可能太大了。在这种情况下,C++并没有定义结果应该是什么;这意味着不同的实现的反应可能不同。
传统初始化的行为与赋值相同,程序3.13演示了一些初始化进行的转换
//init.cpp——type changes on initialization
#include<iostream>
int main()
{
using namespace std;
cout.setf(ios_base::fixed,ios_base::floatfield);
float tree = 3;
int guess(3.9832);
int debt = 7.2E12;
cout<<"tree = "<<tree<<endl;
cout<<"guess = "<<guess<<endl;
cout<<"debt = "<<debt<<endl;
return 0;
}
在这个程序中,将浮点值3.0赋给了tree。将3.9832赋给int变量guess导致这个值被截取为3。将浮点型转换为整型时,C++采取截取(丢弃小数部分)为不是四舍五入(查找最接近的整数)。最后,int变量debt无法存储3.0E12,这导致C++没有对结果进行定义的情况发生。在这种系统中,debt的结果为1634811904,或大约1.6E09。
2. 以{}方式初始化时进行的转换(C++11)
C++11将使用大括号的初始化称为列表初始化,因为这种初始化常用于给复杂的数据类型提供值列表。与程序3.13所示的初始化方式相比,它对类型转换的要求更严格。具体地说,列表初始化不允许缩窄,即变量的类型可能无法表示赋给它的值。例如,不允许将浮点数转换为整型。在不同的整型之间转换或将整型转换为浮点型可能被允许,条件是编译器知道目标变量能够正确地存储赋给它的值。例如,可将long变量初始化为int值,因为long总是至少与int一样长;相反方向的转换也可能被允许,只要int变量能够存储赋给它的long常量:
const int code = 66 ;
int x = 66 ;
char c1 {31325};
char c2 = {66};
char c3 {code};
char c4 = {x};
x = 31325 ;
char c5 = x;
在上述代码中,初始化c4时,您知道x的值为66,但在编译器看来,x是一个变量,其值可能很大。编译器不会跟踪下述阶段可能发生的情况;从x被初始化到它被用来初始化c4。
3.表达式中的转换
当同一个表达式中包含两种不同的算术类型时,将出现什么情况呢?在这种情况下,C++将执行两种自动转换;首先,一些类型在出现时便会自动转换;其次,有些类型在与其他类型同时出现在表达式中时将被转换。
先来看看自动转换。在计算表达式时,C++将bool、char、unsigned char、signed char和short值转换为int。具体地说,true被转换为1,false被转换为0。这些转换被称为整体提升。例如,请看下面的语句:
short chickens = 20 ;
short ducks = 35 ;
short fowl = chickens + ducks ;
为执行第3行语句,C++程序取得chickens和ducks的值,并将它们转换为int。然后,程序将结果转换为short类型,因为结果将被赋值给一个short变量。这种说法可能有点拗口,但是情况确实如此。通常将int类型选择为计算机最自然的类型,这意味着计算机使用这种类型时,运算速度可能最快。
还有其他一些整数提升:如果short比int短,则unsigned short 类型将被转换为int,如果两种类型的长度相同,则unsigned short类型将被转换为unsigned int。这种规则确保了在对unsigned short进行提升时不会损失数据。
同样,wchar_t被提升成为下列类型中第一个宽度足够存储wchat_t取值范围的类型:int、unsigned int、long或unsigned long。
将不同类型进行算术运算时,也会进行一些转换,例如将int和float相加时。当运算涉及两种类型时,较小的类型将被转换为较大的类型。例如,程序3.11中的程序用9.0除以5。由于9.0的类型为double,因此程序在用5除之前,将5转换为double类型。总之,编译器通过校验表,编译器将依次查阅该列表。
(1)如果有一个操作数的类型是long double,则将另一个操作数转换为long double。
(2)否则,如果有一个操作数的类型是double,则将另一个操作数转换为double。
(3)否则,如果有一个操作数的类型是float,则将另一个操作数转换为float。
(4)否则,说明操作数都是整型,因此执行整型提升。
(5)在这种情况下,如果两个操作数都是有符号或无符号的,且其中一个操作数的级别比另一个低,则转换为级别高的类型。
(6)如果一个操作数为有符号的,另一个操作数为无符号的,且无符号操作数的级别比有符号操作数高,则将有符号操作数转化为无符号操作数所属的类型。
(7)否则,如果有符号类型可表示无符号类型的所有可能取值,则将无符号操作数转换为有符号操作数所属的类型。
(8)否则,将两个操作数都转换为有符号类型的无符号版本。
ANSI C遵循的规则与ISO 2003 C++相同,这与前述规则稍有不同;而传统K&R C的规则又与ANSI C稍有不同。例如,传统C语言总是将float提升为double,即使两个操作数都是float。
前面的列表谈到了整型级别的概念。简单地说,有符号整型按级别从高到低依次为long long 、int、short和signed char。无符号整型的排列顺序与有符号整型相同。类型char、signed char和unsigned char的级别相同。类型bool的级别最低。wchar_t、char16_t和char32_t的级别与其底层类型相同。
4、传递参数时的转换
正如第7章将介绍的,传递参数时的类型转换通常有C++函数原型控制。然而,也可以取消原型为参数传递的控制,尽管这样做并不理智。在这种情况下,C++将对char和short类型(signed 和unsigned)应用整型提升。另外,为保持为传统C语言中大量代码的兼容性,在将参数传递给取消原型对参数传递控制的函数时,C++将float参数提升为double。
5、强制类型转换
C++还允许通过强制类型转换机制显式地进行类型转换。(C++认识到,必须有类型规则,而有时有需要推翻这些规则。)强制类型砖汉的格式有两种。例如,为将存储在变量thor n中的int值转换为long类型,可以使用下述表达式中的一种:
(long) thorn;
long (thorn);
强制转换的通用格式如下:
(typeName) value;
typeName (value);
第一种格式来自C语言,第二种格式是纯粹的C++。新格式的想法是,要让强制类型转换就像是函数调用。这样的内置类型大的强制类型转换就像是为用户定义的类设计的类型转换。
C++还引入可4个强制类型转换运算符,对它们的使用要求更为严格,这将在第15章介绍。在这四个运算符种,static_cast<>可用于将值从一种数值类型转换为另一种数值类型。例如,可以像下面这样将thorn转换为long类型:
static_cast<long> (thorn)
推广而至,可以这样做:
static_cast<typeName> (value)
Stroustrup认为,C语言式的强制类型转换由于有过多的可能性而极其危险,这将在第15章更深入的讨论。运算符static_cast<>比传统强制类型转换为更严格。
程序3.14演示了这两种基本的强制类型转换和static_cast<>。可以将该程序第一部分想象为一个功能强大的生态模拟程序的一部分,该程序执行浮点计算,结果被转换为鸟和动物的数目。得到的结果取决于合适进行转换。计算auks时,首先将浮点值相加,然后再赋值时,将总数转换为int。但计算bats和coots时,首先通过强制类型转换将浮点值转换为int,然后计算总和。程序的最后一部分演示了如何通过强制类型转换来显示char值的ASCII码。
//typecast.cpp——forcing type changes
#include<iostream>
int main()
{
using namespace std;
int auks,bats,coots;
auks = 19.99+11.99;
bats=(int)19.99+(int)11.99;
coots=int (19.99)+int (11.99);
cout<<"auks = "<<auks<<" , bats = "<<bats<<" , coots = "<<coots<<endl;
char ch='Z';
cout<<"The code for "<<ch<<" is "<<int(ch)<<endl;
cout<<"Yes , the code is "<<static_cast<int>(ch)<<endl;
return 0;
}
首先,将19.99和11.99相加,结果为31.98。将这个值赋给int变量auks时,它被截断为31。但再进行加法运算之前使用强制类型转换时,这两个值将被截断为19和11,因此bats和coots的值都为30.接下来,两条cout语句使用强制类型转换将char类型的值转换为int,再显示它。这些转换导致cout将值打印为整数,而不是字符。
该程序指出了使用强制类型转换的两个原因。首先,可能有一些值被存储为double类型,但要使用它们来计算得到一个int类型的值。例如,可能要用浮点数来对齐网络或者模拟整数值(如人口)。程序员可能希望在计算时将值视为int,强制类型转换允许直接这样做。注意,将值转换为int,然后相加得到的结果,与先将值相加,然后转换为int是不同的,至少对于这些值来说是不同的。
程序的第二部分指出了最常见的使用强制类型转换的原因——使一种格式的数据能够满足不同的期望。例如,在程序3.14种,char变量ch存储的是字母Z的编码。将cout用于ch将显示字符Z,因此ch的类型为char。但通过将ch强制转换为int类型,cout将采取int模式,从而打印存储在ch中的ASCII码。
3.4.5 C++11中的auto声明
C++11新增了一个工具,让编译器能够根据初始值的类型推断变量的类型。为此,它重新定义了auto的含义。auto是一个C语言关键字,有关其以前的含义,第九章再讲。在初始化声明中,如果使用关键字auto,而不指定变量的类型,编译器将把变量的类型设置成与初始值相同:
auto n =100; //n is int
auto x=15; //x is double
auto y=1.3e12L; //y is long double
然而,自动推断类型并非为这种简单情况而设计的,事实上,如果将其用于这种简单情形,甚至可能让您误入歧途。例如,假设您要将x、y和z都指定为double类型,并编写了如下代码:
auto x = 0.0 ;
double y= 0;
auto z = 0;
显式地声明类型时,将变量初始化为0(而不是0.0)不会导致任何问题,但采用自动类型推断时,这却会导致问题。
处理复杂类型,如标准模块库(STL)中的类型时,自动类型推断的有时才能显现出来。例如,对于下述C++98代码:
std::vector<double> scores ;
std::vector<double>::iterator pv = scores.begin();
C++11允许您将其重写为下面这样:
std::vector<double> scores ;
auto pv = scores.begin();
以后在后面讨论相关的主题时,将再次提到auto的这种新含义。
3.5 总结
C++基本类型分为两组:一组由存储为整数的值组成,另一组由存储为浮点格式的值组成。整数之间通过存储值时使用的内存量及有无符号来区分。整型从最小到最大依次是:bool、char、signed char、unsigned char、short、unsigned short、int、unsigned int、long、unsigned long以及C++新增的long long 和unsigned long long。还有一种wchat_t类型,它在这个序列中的位置取决于实现。C++11新增了类型char16_t和char32_t,它们的宽度足以分别存储16位和32位的字符编码。C++确保了char足够大,能够存储系统基本字符集中的任何成员,而wchar_t则可以存储系统扩展字符集中的任意成员,short至少为16位,而int至少与short一样从,long至少为32位,且至少和int一样长。确切的长度取决于实现。
字符通过其数值编码来表示。I/O系统决定了编码是被解释为字符还是数字。
浮点类型可以表示小数值以及比整型能够表示的值大得多的值。3种浮点类型分别是float、double和long double。C++确保float不必double长,而double不比long double长。通常,float使用32位内存,double使用64位,long double使用80到128位。
通过提供各种长度不同,有符号或无符号的类型,C++使程序员能够根据特定的数据要求选择合适的类型。
C++使用运算符来提供对数字类型的算术运算:加、减、乘、除和求模。当两个运算符对同一个操作数进行操作时,C++的优先级和结合性规则可以确定先执行哪种操作。
对变量赋值、在运算中使用不同类型、使用强制类型转换时,C++把值从一种类型转换为另一种类型。很多类型转换都会死“安全的”,即可以在不损失和改变数据的情况下完成转换。例如可以把int值转换为long值,而不会出现任何问题,对于其他一些转换,如将浮点类型转换为整型,则需要更加小心。
开始,大家可能觉得大量的C++基本类型有些多余,尤其是考虑到各种转换规则时。但是很可能最终将发现,某些时候,只有一种类型是需要的,此时大家将感谢C++提供了这种类型。