C++ 归纳:类型转换
在我们平时的使用中,很少主动去变换一个量的类型,在不知不觉中(尤其在使用函数的过程中)会出现很多错综复杂的 隐式的 类型转换,导致函数获得的结果并非在我们意料之中,我认为 C++ 中的一切麻烦皆来于此。
这篇文章会探讨 C++ 中,何时发生了转换以及发生了何种类型的转换。
我们的目的是了解这些 隐式的 转换,并且尽可能的使用 类型安全 的转换。
本文讨论的基本类型
bool | char | short | int | long | float |
---|---|---|---|---|---|
引用:& | 指针:* | 数组:[] | |||
signed | unsigned | ||||
const | volatile |
第一行是基本类型,按照所占字节大小(从1Byte开始)进行排列;第二行是可加在基本类型后面的,具有特殊意义的操作符;第三行和第四行是修饰符。行与行之间可以任意组合,可以涵盖大多数变量以及形参的定义。
何种类型的转换
整型提升(自动提升)
上表中,位于 int
左边的任意 signed
unsigned
类型(unsigned short
除外)被自动转换为 int
。
unsigned short
可能会被转换为 unsigned int
,这取决于系统或编译器定义的 类型宽度 ,当然这也被称为 整型提升 。
当然还有
wchar_t
,为了保证类型安全也会进行整型提升,将wchar_t
转换为一个int
之后的、类型安全的、最小宽度类型。可能仍然是int
,也可能是unsigend int
或者宽度更宽的类型。
这些转换是安全的,因为编译器的实现保证了 int
比这些类型能容纳更大的数据(宽度更宽),在进行整型提升时不会损失精度。
这个过程是自动的,即如果满足要求,编译器会自动执行整型提升,同时也是编译器首选的转换模式,如果这个模式不满足类型安全则编译器会使用其它策略。
整型转换
整形转换是指整型在 signed
以及 unsigned
之间的转换。
这个转换在进行表达式计算时是有规则的:
- 两个操作数 都 是
signed
或unsigned
的,则向优先级高的一侧转换。 - 两个操作数一个是
signed
一个是unsigned
,unsigned
的级别高于signed
,则向unsigned
一侧转换。 - 若
signed
级别高于unsigned
,且signed
能够容纳unsigned
所能表示的所有数字,则向signed
一侧转换。 - 否则 将两个类型转换为 有符号类型(默认有符号的类型)的无符号版本。
这个转换不总是安全的,因为两种形式可表示的数据范围不同。能发生这种情况的场景也十分有限,只会发生在 signed
和 unsigned
互相赋值的场景下(平时很少使用)。
浮点转换
指 float
和 double
相关类型的转换。
从宽度上看,只有 float
转换为 double
的行为是安全的。
如果非需要,不要做从 浮点类型 向 整型 的转换,这种转换通常有 截断 和 溢出 两种结果。
截断 的结果经常被用于浮点数取整;溢出 很可能是我们小看了浮点类型能够表示的数的范围,而溢出的结果往往是我们所讨厌的。
用户定义的转换
在 class
中可以进行类型转换的定义,称为 转换函数:
operator typename()
转换函数定义了将类转换为其它类型的行为。除非必要,否则不要使用转换函数。
何时发生类型转换
表达式计算时的转换
编译器遇到表达式计算时会按照下面的规则进行类型转换:
从最宽的 浮点类型 long double
开始检查,如果存在 long double
则将其它操作数转换为 long double
;
进行 double
的检查;进行 float
的检查;进行 整型提升 ;进行 整形转换 ;
初始化和赋值时的转换
进行宽度提升的转换总是安全的,在进行初始化和赋值时通常也遵循该原则。
-
以常量(字面量)进行的初始化
如果没有其它理由,编译器总会将 数字 作为
int
类型对待;如果有后缀(比如浮点类型的 f 、无符号长整形的 ul)则转换为后缀标识的类型;如果int
的能够表示的范围不够大,则向更宽的整型转换。 -
不允许缩窄的初始化
C++ 11 的新特性 列表初始化 ,使用列表初始化进行初始化操作时,不允许将列表中的数据类型 缩窄 使用。
char c1 {31325}; // narrowing, not allowed
31325 超过了
char
能够表示的数据范围,如果不使用 列表初始化 则c1
将进行溢出表示;如果使用了 列表初始化 则会出现编译错误。
我们在进行赋值时常常也会使用常量,有时也会用到表达式,这两种情况的规则已经在上文介绍过了,要注意的是:在表达式赋值时,转换或许不仅发生一次。
short res;
short a = 10;
short b = 20;
res = a + b;
在这个代码中,在定义 a
b
时会出现 int
向 short
转换的情况;在进行 res = a + b
时,a + b
会作为 int
进行计算,最终向 res
赋值时也会进行 int
向 short
转换的情况。
const
相关的转换
只允许从 non-const 向 const 的转换。
函数调用时的转换
若实参与形参类型不一致,则编译器会将 实参 类型向 形参 转换,存在一些无关紧要的转换,这在编译器匹配合适的函数时会十分有用。
实参 | 形参 |
---|---|
type |
type & |
type |
const type |
type |
volatile type |
type * |
const type |
type * |
volatile type |
type & |
type |
type [] |
* type |
type() |
type(*)() |
type *
指得是类型指针;* type
指得是数组的指针形式;type(*)()
指得是函数指针。
如果实参和形参满足上述列表的话,编译器无需(不会)进行转换,因为这些转换“无关紧要”。这也意味着如果存在两个函数:
type func(type);
type func(type &);
将会被视为重复的函数声明,产生二义性(ambiguous)。
或者你进行了如下的声明:
int func(int &a);
int func(const int a);
仍然会出现二义性,这是可能会出现的错误信息:
error: call of overloaded ‘func(int&)’ is ambiguous
使用函数时还会出现的一种转换:若函数返回非 void
类型,在计算 return
表达式时也会进行转换,表达式计算时的转换也适用于return
表达式。
强制类型转换
强制类型转换是显式的 explicit
。
我们熟知的语法是 (typename) value
,C++中可以使用新的方式 typename(value)
,就像是调用了 “类的构造函数” 或者说定义了一个“类的对象”,记住这种感觉,因为我们稍后会用到。
这种语法用来将 value
强制的转换为 typename
。
这种强制的类型转换是在告诉编译器“我知道自己在做什么,所以不需要警告或者错误信息”,这似乎没有问题,我却认为我们需要安全的,最好可以在编译期间检查出来的类型转换的错误。
于是我们有4个 强制类型转换运算符 :
-
static_cast<typename> (value)
,依然是将value
转换为typename
,可用于基本类型,也可以作用于指针和引用。编译器会对这种转换进行检查,当两者无法兼容时会出现编译期间的错误,相对于传统形式的强制转换要安全一些。 -
const_cast<typename> (value)
,用于取消const
以及volatile
特性。这不能用于转换基本类型,只可以作用于指针和引用。 -
dynamic_cast<typename> (value)
,作用于指向类的指针,是 RTTI 的重要组件,用于判断两个类的指针能否进行转换。比如基类的指针可以指向派生类,则通过基类的指针可以调用到派生类当中的成员,但这依赖于析构函数的定义。如果试图调用派生类中独自定义的成员,那是不允许也是不可能的。为达到此目的,我们或许还需要再将基类指针转换为派生类指针,这就需要
dynamic_cast<typename> (value)
。typename
是一个指向派生类的指针,value
是指向基类的指针。如果允许转换,那么转换返回的结果会是typename
,若不允许,则返回一个nullptr
。总之这个运算符会让你安全的进行转换。
-
reinterpret_cast<typename> (value)
,这个单词的含义是重新解释
。它允许将任何指针转换为任何指针,也可以将整型转换为指针所指向的地址,当然也可以反向进行转换。这也存在限制,比如不能将指针转换为浮点型,不能用于函数指针和数据指针之间的转换。这种转换依赖于底层的实现,不同环境下地址的表示方式不同,这是不可移植的。但是如果需要将指针固定到某一个位置时,应该可以考虑到这种方法。
类的自动转换
这一点可以说明在定义构造函数时 explicit
的意义。
类的自动转换指得是:基本类型自动转换为类类型,其原因源自于构造函数。
还记得C++中得强制类型转换 typename(value)
么,如果 typename
是一个类的名字的话,就相当于调用了类的构造函数。
class Stonewt
{
private:
enum {Lbs_per_stn = 14}; // pounds per stone
int stone; // whole stones
double pds_left; // fractional pounds
double pounds; // entire weight in pounds
public:
Stonewt(double lbs); // constructor for double pounds
Stonewt(int stn, double lbs); // constructor for stone, lbs
Stonewt(); // default constructor
~Stonewt();
void show_lbs() const; // show weight in pounds format
void show_stn() const; // show weight in stone format
};
我们来关注这个类的构造函数 Stonewt(double lbs)
。
如果我们这样使用 Stonewt s = 16.6
那么这个语句是成立的,编译器会调用 Stonewt(double)
将 16.6
构造为一个匿名的 Stonewt
对象,并将其赋值给 s
。
对于任意一个类来说,能否发生这样的转换取决于其构造函数的定义:是否允许将某个基本类型构造为该类对象。这种转换也意味着提供了一种从基本类型到类的映射,在函数调用以及表达式赋值时可以做这样的转换。但是这样的转换或许并非是我们想要的,从语义上会造成误解,于是将 explicit
标识在构造函数上以消除这种隐式的转换,从而明确构造函数的语义,而不是将构造函数理解为 “强制类型转换”。
在隐式的条件下,通过合适的构造函数可以实现 基本类型 向 类 的转换。同样的,也可以实现 类 向 基本类型 的转换,这需要在类的成员中定义 转换函数。
转换函数通过 operator typename()
定义为类成员,拥有函数体,没有返回值 声明,不能拥有参数,可以被 const
修饰。
定义之后就可以实现从类向基本类型的转换,可以将两个类对象进行四则运算,实际上是将类转换为相应的类型后进行计算。
在C++11中,转换函数也可被 explicit
修饰,表示不可通过该方式进行隐式转换,需要进行强制类型转换。
总之要谨慎的使用隐式的转换。
总结
本文讨论了何时发生类型转换,以及发生了何种类型的转换。讨论的重点在于何时发生了类型转换,同时说明了类型转换的基本规则:在基本类型中的隐式转换往往是进行“宽度提升”的转换,这类转换是安全的,即不会丢失精度。同时也描述了稍微复杂的规则,例如表达式计算时的转换。
在“何时发生的类型转换”的介绍当中,初始化、赋值以及表达式计算时的隐式类型转换,是我们较容易分辨和处理的。在介绍完基本规则后,将最容易出现问题的 函数调用时的转换 以及 类的自动转换 进行说明,能够掌握 无关紧要的转换 以防止在进行函数重载时出现 二义性 ;能够清楚的认识 explicit
在构造函数以及转换函数中的意义。
介绍了C++中的4种 强制转换运算符 ,介绍了它们的使用方法以及要点,对于强制类型转换的工作,使用它们是最为妥当的选择。
认识以及清楚在编写代码的过程中产生的 隐式的 类型转换,以及 安全的 进行强制类型转换,是我们理解 函数重载 的规则,以及 模板类型匹配 规则的重要基础。
认识这些转换并在编写代码时尽可能地少使用 显式的 以及 隐式的 ,或者说 各种类型的 类型转换,非必要不使用,会让我们的程序更加健壮。