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 allowed31325 超过了
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种 强制转换运算符 ,介绍了它们的使用方法以及要点,对于强制类型转换的工作,使用它们是最为妥当的选择。
认识以及清楚在编写代码的过程中产生的 隐式的 类型转换,以及 安全的 进行强制类型转换,是我们理解 函数重载 的规则,以及 模板类型匹配 规则的重要基础。
认识这些转换并在编写代码时尽可能地少使用 显式的 以及 隐式的 ,或者说 各种类型的 类型转换,非必要不使用,会让我们的程序更加健壮。