【深度C++】之“类型转换”

0. 类型转换的定义

C++中,每个变量(或者对象)都有一个特定的类型,如int,bool,char等(参考【深度C++】之“类型与变量”)。

对象的类型定义了对象能包含的数据能参与的运算,然而C++规定,参与运算的两个对象,必须是同一种类型。

在众多的运算中,有一种运算被大多数类型支持,就是将一种给定的类型转换(convert) 为另一种相关类型(所以说C++将类型转换定义为一种运算)。

当在程序的某处,我们使用了一种类型而其实对象应该取另一种类型时,程序会自动进行类型转换

举例来梳理一下上面的定义:

bool b = true;
int i = b;

第二条语句处,我们使用了bool类型的b,但是此处需要一个int类型(因为程序员定义的类型int是要被初始化的类型,不能改变),因此程序先将b转换成int,在使用转换后的结果赋值给ii的值为1。

若两种类型可以相互转换(conversion),则两种类型是关联的

关于类型转换,我们需要了解:

  1. 隐式类型转换
    1. 算术转换
    2. 指针相关的转换
    3. 类类型定义的转换
  2. 显示类型转换

1. 隐式类型转换

隐式类型转换(implicit conversion) 是指无须程序员的介入、甚至不需要程序员了解的一种转换。

int ival = 3.541 + 3;
// 先将3转换成double,然后执行加法得到double类型临时量6.541
// 再将6.541小数部分忽略,得到6,初始化变量ival

编译器只会警告该运算损失了精度。

1.1 发生条件

在以下五种情况中,编译器自动执行隐式类型转换:

  1. 大多数表达式中,比int类型小的整形值提升为较大的整数类型(整型提升
  2. 在条件语句中,非布尔值转换成布尔值(布尔转换
  3. 在初始化过程中,初始值转换成变量的类型;赋值语句中,右侧运算对象转换成左侧运算对象的类型(赋值转换
  4. 算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型(严格匹配
  5. 函数调用

1.2-1.5介绍了C++中常见的可能出现转换的场景,每个场景可能涵盖上述多种发生条件。

1.2 算术转换

算数转换是C++中最常见的转换。顾名思义,转换发生在C++中基本内置类型算数类型内部(参考【深度C++】之“类型与变量”),包括有符号和无符号的类型。

算数转换遵循以下原则:

  1. 整形提升(类型提升)
  2. 布尔转换
  3. 赋值转换
1.2.1 整型提升(类型提升)

算术转换遵循的一个原则,整型提升:小整型提升到较大整型(发生条件1)。

因为C++标准只规定了各个数据类型的最小尺寸以及几种类型的大小关系,因此提升的结果根据编译器不同。

  • 原则1:小整形bool、char、signed char、unsigned char、short、unsigned short将被提升成int。若int也装不下,就提升为unsigned int。
  • 原则2:较大的char类型wchar_t、char16_t、char32_t提升成int、unsigned int、long unsigned long、long long和unsigned long long中最小的。

看下表:(假设int在某编译器上为32bit)

类型含义尺寸提升
bool布尔类型8位int
char字符8位int
wchar_t宽字符32位int
char16_tUnicode字符16位int
char32_tUnicode字符32位int
short短整型16位int
int整型32位long
long长整型64位long long
long long长整型64位

几点说明:

  1. bool类型提升为int见本文2.2.2。
  2. 当int与浮点型运算时,通常将int转换为浮点型发生条件4)。
  3. 对于浮点型,也存在类似的提升,float提升到double,double提升至long double。

虽然名称叫做整型提升,但是从整型到浮点型的转换,我们也可以理解为从精度较低整型的提升到精度较高的浮点型,因此有时将这两种转换统称为类型提升

注意:提升不能一步到底

char c = 'a';
float f = 3.14;
c + f;
// c首先提升成int
// int再转换成float
1.2.2 布尔转换

条件语句如if、while等,需要输入一个布尔类型,因此从其他算术类型到布尔类型也存在一种定义了的隐式转换机制:

  • 0 -> false
  • 非0 -> true

若使用了布尔值参与了运算,则需要定义一个从布尔类型转换至其他类型的机制:

  • false -> 0
  • true -> 1
1.2.3 赋值转换

当我们使用=进行初始化或者赋值的时候,等号右边的数值往往转换为左边的数值。这时不能单纯的遵循整型提升布尔转换,因为左侧定义的类型是程序员不希望被改变的。

int i = 1.2;
// 1.2是字面值常量,是double类型
// 赋值给i将截断1.2的小数部分,i = 1
  • 浮点型到整型:截断小数部分,保留整数部分
  • 整型到浮点型:小数部分记为0
float f = 3.14;
bool b = f;
// 此时b是true

f = b;
// 此时f是1.0

原则:

  1. 精度高到精度低的转换,通常被截断
  2. 精度低到精度高的转换,可以参考整形提升和布尔转换相关的规范。

1.3 指针相关的转换

转换到指针 -> pointer

  • 数组转换成指针:转换为指向首元素的指针
  • 0或者nullptr:转换为任意指针类型
  • void *:指向任意非常量的指针可转换为void *
  • const void *:指向任意对象的指针可转换const void *

指针转换为:

  • 常量指针:指向非常量类型的指针转换成指向常量类型的指针,引用类似
  • 布尔类型:若指针是0或nullptr,转换为false;否则为true

【深度C++】之“指针”里,我们提到过,指针的值一共有四种情况:

  1. 指向一个有效的对象
  2. 指向紧邻对象所占空间的下一个位置(尾后指针
  3. 空指针,没有指向任何对象(0或者nullptr
  4. 无效指针,上述情况之外

除了第3条是false,其余都是true(真的很容易记错)。

1.4 类类型定义的转换

在C++中,我们可以通过重载类型转换运算符自定义自己的类类型的类型转换结果。

1.4.1 转换为类类型

【深度C++】之“构造函数”中我们提到过一种构造函数:转换构造函数,它实际上定义了转换为此类类型的隐式转换机制。

1.4.2 类类型转换为其他类型

关于重载类型转换运算符,请参考【深度C++】之“运算符重载”

1.5 函数调用

函数调用传递实参与函数调用返回结果的方式与初始化一个变量的方式一样,因此进行类型转换的方式和赋值转换类似。

不同的是,在函数调用时需要考虑函数的重载(参考【深度C++】之“函数重载”),以及函数匹配是否可以确定出一个最佳匹配函数供调用点使用。

2. 显示类型转换

有时我们希望将对象强制转换成另外一种类型,便产生了强制类型转换,也称作显示类型转换

无论是新方法还是老方法,C++标准都不推荐强制类型转换。

2.1 老方法

旧式C++类型换换有两种

type (expr)  // 函数形式的强制类型转换
(type) expr  // C语言风格的强制类型转换

例如

float f = 3.14;
int i = (int) f;
int p = int(f);

此处我们告诉编译器,我们就希望利用对象f的整数部分。

2.2 命名的强制类型转换

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

cast_name<type>(expression);
  • cast-name:执行转换的类别
  • type:转换的目标类型
  • expression:要转换的值

共有四种cast-name运算符

  1. static_cast
  2. dynamic_cast
  3. const_cast
  4. reinterpret_cast
2.2.1 static_cast

具有明确定义的类型转换,除了底层const,都可以使用static_cast运算符。

int i, j;
double s = static_cast<double>(j) / i;
// 使用static_cast将整型j转换为double
// i隐式转换为double
// 二者除法,得到double类型的结果,初始化s

隐式类型转换已经足够程序员使用了,但是有些时候我们就是不care精度的丢失,就是只关心浮点数的整数部分,此时使用static_cast,编译器的警告信息就会关闭。

2.2.2 dynamic_cast

dynamic_cast运算符是C++运行时类型识别的功能之一,参考【深度C++】之“运行时类型识别RTTI”

2.2.3 const_cast

static_cast不能改变底层const,可以使用const_cast。

int a = 20;
const int *p_ca = &a;
int *p_a = const_cast<int *>(p_ca);

参考【深度C++】之“const” 我们得知,底层const的要求十分严格:

  • 可以把非底层const的变量赋值给一个底层const
int a = 20;
int *p_a = &a;
// 非底层const赋值给底层const
const int *p_ca = p_a;
  • 但是不可以使用底层const赋值给非底层const
int a = 20;
const int *p_ca = &a;
// 编译出错
// 底层const赋值给非底层const
int *p_a = p_ca;

使用const_cast

int a = 20;
const int *p_ca = &a;
// 强制转换
int *p_a = const_cast<int *>(p_ca);

虽然解决了我们的问题,但是使用p_a进行写操作,是一种未定义的行为:

  • a不是常量,可以写
  • a是常量,未定义,通常是改变不了
const int a = 20;
const int *p_ca = &a;
int *p_a = const_cast<int *>(p_ca);
*p_a = 40;

// 20还是40未定义,通常是20
cout << a << endl;

const_cast技术通常用于一个类同时定义了某函数的常量版本和非常量版本,通常非常量版本委托常量版的情况。例如:

class MyClass {
public:
    const string &shorter(const string &s1, const string &s2) {
        return s1.size() <= s2.size() ? s1 : s2;
    }
    string &shorter(stirng &s1, string &s2) {
        auto &r = shorter(const_cast<const string &>(s1),
                          const_cast<const string &>(s2));
        return const_cast<string &>(r);
    }
};

定义了一个const MyClass cmc后,自然调用的是shorter的常量版本。

但是定义了一个MyClass mc后,调用的是非常量版本,非常量版本通过const_cast调用了常量版本。

我们思考,既然类定义了shorter的常量版本,那么说明它的工作不需要修改类内任何成员变量即可完成。提供了非常量版本仅仅是为了MyClass mc使用方便,因此两个重载函数的功能一模一样,为什么需要两处代码?所以在非常量shorter中,将功能委托给了常量shorter,让代码更好维护。

2.2.4 reinterpret_cast

reinterpret_cast为运算对象的位模式提供较低层次上的重新解释。

// 假设int32bit,16进制定义
// 0x41 = 65 = 'A'
// 0x42 = 66 = 'B'
int a = 0x41424344;
int *pa = &a;

// 将一个32bit的int转换为4个8bit的char
// 注意*pc的末尾没有\0,不要直接使用
char *pc = reinterpret_cast<char *>(pa);
string str(pc);

// 输出DCBA(大小端相关)
cout << str << endl;

通过上面的例子就应该明白reinterpret_cast是如何工作的了。

但是这种转换也是危险的,因为编译器认为pc指向了一个字符串,也就是C风格字符串,通常这样的字符串末尾必须有一个'\0'。但是我们使用了强制转换,编译器在使用pc的时候不做检查,实际上的值是一个int类型。

而且C++并没有规定其基本内置类型的固定长度,在各个编译器上很可能转换的结果很可能不一样,甚至和物理机器存储数据的形式都相关。

我们不应该写出这样高度依赖某种机器的代码。

3. 总结

C++中的类型转换是C++中的数据类型都普遍支持的一种运算。它包括隐式类型转换显示类型转换。隐式类型转换又可以分为算数转换指针相关的转换类类型定义的转换

隐式类型转换,基于运算的对象类型严格匹配的原则,可以有类型提升布尔转换赋值转换,在函数调用的时候,因为函数的重载,也可能发生类型转换。

显式类型转换也是一种强制类型转换,他告诉编译器我们需要这样的转换,即使丢失了精度,不要帮我检查。这样的转换是危险的且不推荐。可以使用命名的强制类型转换来实现。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值