C++丰富的类型允许根据需求的不同选择不同的类型,但这也使得计算机的操作更加复杂。
(如果对数据类型不了解,可以参考本人的文章:
C++爱好者的自我修养(12.1):数据类型——整型
C++爱好者的自我修养(12.2):数据类型——浮点型
C++爱好者的自我修养(12.3):数据类型——字符(串)型)
例如,将两个short类型的值相加所涉及到的硬件编译指令可能和将两个long类型的值相加不同。
由于C++有11种整型和3种浮点型,因此计算机需要处理大量不同的情况,尤其是对不同的类型进行运算时。为了处理这种潜在的混乱,C++自动执行很多种类型转换:
- 将一种算数类型的值赋给另一种算数类型的变量时,C++将对值进行转换
- 表达式中包含不同的类型时,C++将对值进行转换
- 将参数传递给函数时,C++将对值进行转换
C++允许将一种类型的值赋给另一种类型的变量,这样做时,值将被转换为被接受变量的类型,例如:
/*a的类型是long
b的类型是short*/
a = b;
则进行赋值时,程序会将b的值(16位)拓展为long值(32位),拓展后会得到一个新值,这个值会被储存在a中,而b的值不变。
建一个值赋给取值范围更大的类型通常不会出现什么问题。但是,将一个很大的long值(例如123456789)赋给float变量就会降低精度,因为float只有6位有效数字,因此这个值将被四舍五入位1.23456E8。由此可见,有些赋值是安全的,有些则会带来麻烦。
转换 | 潜在的问题 |
---|---|
将较大的浮点类型转换为较小的浮点类型,例如将double转换为float | 精度(有效数位) 降低,值可能超出目标类型的取值范围,在这种情况下,结果将是不确定的 |
将浮点类型转换为整型 | 小数部分丢失,原来的值可能超出目标类型的取值范围,在这种情况下,结果将是不确定的 |
将较大的整型转换为较小的整型,例如将long转换为short | 原来的值可能超出目标类型的范围,通常只复制右边的字节 |
将0赋给bool变量时,将被转换为false,而非零值将被转换为true。
C++11将使用{}的初始化称为列表初始化,因为这种初始化常用于给复杂的数据类型提供值列表。前面所介绍的转换方式不同,它对类型转换的要求更严格。具体地说,列表初始化不允许缩窄,即变量的类型可能无法表示赋给它的值。例如,不允许将浮点型转换成整型。在不同整型之间转换或将整型转换为浮点型可能被允许,前提是编译器知道目标变量能够正确地储存赋给它的值,例如,可以将long变量初始化为int值,因为long总是至少与int一样长;相反方向的转换也可能被允许,只要int变量能储存赋给它的long变量,例如:
int a = 56;
char b = {56};//允许
char c = {a};//不允许
在上面的代码初始化c时,我们知道a的值为56,但在编译器看来,a是一个变量,其值可能很大。编译器不会跟踪下述阶段可能发生的情况:从a被初始化到他被用来初始化c。
当同一个表达式中包含两种不同的算术类型时,将会出现什么情况呢?在这种情况下,C++将执行两种自动转换:首先,一些类型出现是便会自动转换;其次,有些类型在于其他类型同时出现在表达式中时将被转换。
先来看看自动转换。在计算表达式时,C++将bool、char、unsigned char、signed char和short值转换为int。具体地说,true将被转换为1,false将被转换为0。这种转换被称为整型提示。例如:
short a = 20;
short b = 35;
short c = a + b;
为执行第三行语句,C++取得a和b的值,并将它们转化为int,然后程序将结果转换为short类型,因为结果将被赋给一个short变量,这种说法可能有点拗口,但是情况确实如此,通常将int类型选择为计算机最自然的类型,这意味着计算机使用这种类型时运算速度可能最快。
还有其他一些整型提升,如果shot比int短,则unsigned short类型将被转换为int;如果两种类型的长度相同,则unsigned short类型将被转换为unsigned int。这种规则确保了在对unsigned short进行提升时不会损失数据。
同样,wchar_t将被提升为下列类型中第一个宽度足够储存wchar_t取值范围的类型:int、unsigned int、long或unsigned long。
将不同类型进行算术运算时也会进行一些转换,例如将int和float相加时,当运算涉及两种类型时,较小的类型将被转换为较大的类型。例如用9.0除以5,因为9.0的类型是double,因此程序在用五除之前将5转换为double类型。总之,编译器通过校验表来确定在算式表达式中执行的转换。C++11对这个校验表稍做了修改,下面是C++11版本的校验表。编译器将依次查阅此表:
(1)如果有一个操作类型是long double,则将另一个操作数转换为long double。
(2)否则,如果有一个操作数的类型是double,要将另一个操作数转换为double。
(3)否则,如果有一个操作数的类型是float,将另一个操作数转换为float。
(4)否则,说明操作数都是整型,因此执行整型提升。
(5)在这种情况下,如果两个操作数都是有符号或无符号的,且其中一个操作数的级别比另一个低,则转换为级别高的类型。
(6)如果一个操作数为有符号的,另一个操作数为无符号的。且无符号操作数的级别比有符号操作数的级别高,则将有符号操作数转换为无符号操作数所属的类型。
(7)否则,如果有符号类型可表示为无符号类型的所有可能取值,则将无符号操作数转换为有符号操作数所属的类型。
(8)否则,将两个操作数都转换为有符号类型的无符号版本。
前面的列表谈到了整型级别的概念。简单的说,有符号整形按级别从高到低依次为long long、long、 short和char;无符号整型的排列顺序与有符号整型相同类型。signed char和unsigned char的级别相同;类型bool的级别最低。wchar_t、char16_t和char32_t的级别和其底层类型相同。
传递参数时的类型转换通常由C++函数原型控制。然而,也可以取消原型对参数转换的控制,尽管这样做并不明智。在这种情况下,C++将对char和short类型(signed和unsigned)应用整型提升。此外,为保持与传统C语言中大量代码的兼容性,在将参数传递给取消原型对参数传递控制的函数时,C++将float参数提升为double。
C++还允许通过强制类型转换机制显式地进行类型转换(C++认识到,必须有类型规则,而有时又需要推翻这些规则)。强制类型转换的格式有两种。例如,为将储存在变量a中的int值转换为long类型,可以使用下述表达式中的一种:
(long) a 或 long (a)
强制类型修改不会修改a变量本身,而是创建一个新的、指定类型的值,可以在表达式中使用这个值。
cout << int('Q');//输出Q的ASCLL码值
强制类型转换的通用格式如下:
(typeName) value//来自C语言
typtName value//纯粹的C++
static_cast用于将一个表达式转换为指定的类型。它可以用于以下情况:
将一个较大的整数类型转换为较小的整数类型
将一个浮点数类型转换为整数类型
将指针从一个类型转换为另一个类型
下面是一些使用static_cast的示例:
int a = 10;
double b = static_cast<double>(a); // 将整数类型转换为浮点数类型
dynamic_cast用于在运行时将一个指针或引用转换为另一个派生类的指针或引用。如果转换成功,则返回非空指针或引用。
下面是一个使用dynamic_cast的示例:
class Base {
public:
virtual void foo() {}
};
class Derived : public Base {};
int main() {
Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base);
if (derived != nullptr) {
// 转换成功
} else {
// 转换失败
}
return 0;
}
const_cast用于删除变量的常量性。它可以将一个指向常量对象的指针或引用转换为指向非常量对象的指针或引用。
下面是一个使用const_cast的示例:
void foo(const int& a) {
int& b = const_cast<int&>(a);
b = 10;
}
int main() {
int a = 5;
foo(a);
return 0;
}
reinterpret_cast用于将一个指针或引用转换为另一种类型的指针或引用。它可以将任何类型的指针或引用转换为任何其他类型的指针或引用。
下面是一个使用reinterpret_cast的示例:
int main() {
int a = 10;
char* b = reinterpret_cast<char*>(&a);
return 0;
}
C++11新增了一个工具,让编译器能够根据初始值的类型推断变量的类型,为此,它重新定义了auto的含义。auto原本是一个c语言关键字,但很少使用,它原来的含义为:
auto是一个C/C++语言存储类型,仅在语句块内部使用,初始化可为任何表达式,其特点是当执行流程进入该语句块的时候初始化可为任何表达式。C语言中提供了存储说明符auto、register、extern、static说明的四种存储类别。四种存储类别说明符有两种存储期:自动存储期和静态存储期。其中auto和register对应自动存储期。具有自动存储期的变量在进入声明该变量的程序块时被建立,它在该程序块活动时存在,退出该程序块时撤销。在函数内部定义的变量成为局部变量。在某些C语言教材中,局部变量称为自动变量,这就与使用可选关键字auto定义局部变量这一作法保持一致。
在初始化声明中,如果使用关键字auto而不指定变量的类型,编译器将把变量的类型设置与与初始值相同:
auto a = 100;//a是int
auto b = 1.5;//b是double
auto c = 1.3e12L;//c是long double
然而,自动推断类型并非是为这种简单情况而设计的。事实上,如果将其用于这种简单情形,甚至可以让我们误入歧途。例如,假设要将x、y和z都指定为double类型,并编写了以下代码:
auto x = 0.0;//可以,因为0.0的类型是double
double y = 0;//可以,因为0被自动变为了0.0
auto z = 0;//不可以,因为0是int
在C++中,数据类型转换是一个非常重要的概念。我们可以使用隐式类型转换或显式类型转换来将一个数据类型转换为另一种数据类型。虽然隐式类型转换在某些情况下很方便,但也可能会导致错误。因此,我们应该尽可能避免隐式类型转换,并在必要时使用显式类型转换。