|
在下述三种情况下,C/C++会进行隐式类型转换(implicit type cast):①变量初始化或者赋值时,值与变量的类型不同;②表达式中不同类型的变量/值进行运算时;③函数参数传递▲时。
本文引用自作者编写的下述图书; 本文允许以个人学习、教学等目的引用、讲授或转载,但需要注明原作者"海洋饼干叔叔";本文不允许以纸质及电子出版为目的进行抄摘或改编。
1.《Python编程基础及应用》,陈波,刘慧君,高等教育出版社。免费授课视频
2.《Python编程基础及应用实验教程》, 陈波,熊心志,张全和,刘慧君,赵恒军, 高等教育出版社
3. 《简明C及C++语言教程》,陈波,待出版书稿。免费授课视频 3.5.1 赋值/变量初始化
我们通过对下述C++程序及其执行结果的分析来解释赋值/变量初始化时的隐式类型转换及其影响:
//Project - AssignType
#include <iostream>
using namespace std;
int main(){
bool a = 3.0; //转换为bool类型:0为false,非0为true
float b = -99999.2301; //double转float,精度降低,可能超出储值范围
int c = b; //float转int, 小数部分丢失,可能超出储值范围
unsigned int d = c; //int转unsigned int,负值被错误解释
short e = d; //unsigned int转short,可能超出储值范围
double f = b; //float转double,安全
cout.setf(ios_base::fixed, ios_base::floatfield);
cout << &#34;a = &#34; << a << &#34;\nb = &#34; << b << &#34;\nc = &#34; << c
<< &#34;\nd = &#34; << d << &#34;\ne = &#34; << e << &#34;\nf = &#34; << f;
return 0;
}
上述代码的执行结果为:
a = 1
b = -99999.226562
c = -99999
d = 4294867297
e = 31073
f = -99999.226562
第6行:任意值赋值给布尔型变量,按非零即真原则,0值转换为false, 非零转换为true。double类型的字面量3.0赋值给布尔类型的变量a,被转换成true。true输出给cout,结果为1。
第7行:double转换为float时,可能发生精度损失。因为double类型对象由64个比特组成,而float类型只有32个比特。如果double类型的值超过float的储值范围,转换结果将是不确定的。本行进行了此类转换,可以看到转换后的float类型变量b丢失了精度(值不等于-99999.2301)。如果把b的类型改为double,可以保持更高的精度。
第8行:float转换为int时,小数部分丢失。当储值范围超出时,结果不确定。输出结果证实,这种转换只保留了整数部分-99999。
第9行:int(有符号)转换为unsigned int时,如果原值为负数,则结果不确定,因为无符号整数只能存储非负值。输出结果中,可以看到c值-99999被错误转换成了4294867297。
第10行:unsigned int转成short(有符号),是从32位无符号整数到16位有符号整数的转换,如果超出储值范围,则结果不确定。输出结果中,可以看到无符号整数d值4294867297被错误转换成了有符号短整数31073。
第11行:float转换为double则是安全的,因为double的储值范围更大,精度也更高。输出结果证实,double类型变量f完美复制了float类型变量b的值。同理,short到int,int到long long也是安全的。
第13行:对cout输出浮点数的格式进行了设置:按定点小数(相对于科学计算法)输出,保留6位小数。具体细节请参考本书2.3.4节中的扩展阅读内容。
将储值范围和精度较低的对象赋值给储值范围更大,精度更高的对象是安全的,反之则不然。程序员应尽量避免将储值范围大/精度高的对象赋值给储值范围小/精度低的对象。如果因为某些原因不得不这么做,则需要反复确认两件事:①精度的损失在可接受范围内;②源对象的值不会超过目标对象的储值范围。
使用列表初始化(list initialization)【C++ 11】可以避免变量初始化过程中不恰当的隐式类型转换所带来的差错。如下述C++代码所示:
//Project - ListInit
#include <iostream>
using namespace std;
int main(){
char c = 712; //允许,但会溢出
char d {66}; //允许,66在char的储值范围内
char e {712}; //不允许,712超过char的储值范围
unsigned int f {-1}; //不允许,unsigned int只能存非负整数
int g {3.12}; //不允许,收窄会导致精度丢失
return 0;
}
与传统的赋值初始化不同,列表初始化不允许收窄(narrowing),当被初始化的变量无法准确表达{ }内的字面量或者常量▲时,编译器会报错,拒绝编译。
第6行:char类型对象c只有8个比特的存储空间,由于是有符号字符,其储值范围为-128 ~ +127。显然,字面量712超过了c的储值范围。但由于这是传统的赋值初始化,编译器将放任这种情况的发生,至多给出一个警告。
第7行:字面量66在d的储值范围内,该行代码会被编译器所接受。
第8 ~ 10行的变量不能准确表达{ }内的字面量,编译器将报错拒绝。
3.5.2 表达式
在表达式a/b中,a,b称为操作数(operand),/称为操作符(operator)。当算术运算发生时,如果参与运算的操作数具有不同的类型,则编译器会生成额外的代码将其转变成相同的类型后再进行运算。因为,绝大多数的CPU指令集均只支持同类型对象之间的算术运算,比如两个有符号32位整数之间的运算。显然,算术运算的结果类型也受上述操作数类型转换的影响。
下述C++代码以除法运算为例,研究算术运算过程中的类型转换:
//Project - DivisionOperator
#include <iostream>
using namespace std;
int main(){
int i = 3; //有符号整数
auto a = 10 / i; //整数 / 整数 = 整数
cout << &#34;10/3 = &#34; << a << &#34;, type = &#34; << typeid(a).name() << endl;
auto b = double(10.0) / i; //浮点数 / 整数 = 浮点数
cout << &#34;10.0/3 = &#34; << b << &#34;, type = &#34; << typeid(b).name() << endl;
auto c = i / double(10.0); //整数 / 浮点数 = 浮点数
cout << &#34;3/10.0 = &#34; << c << &#34;, type = &#34; << typeid(c).name() << endl;
auto d = double(10.0) / 3.0f; //双精度浮点数 / 单精度浮点数 = 双精度浮点数
cout << &#34;10.0/3.0f = &#34; << d << &#34;, type = &#34; << typeid(d).name() << endl;
auto e = (unsigned int)10 / i; //无符号整数 / 有符号整数 = 无符号整数
cout << &#34;unsigned 10/3 = &#34; << e << &#34;, type = &#34; << typeid(e).name() << endl;
auto f = 10 / (unsigned int)3; //有符号整数 / 无符号整数 = 无符号整数
cout << &#34;10/unsigned 3 = &#34; << f << &#34;, type = &#34; << typeid(f).name() << endl;
return 0;
}
上述代码的执行结果为:
10/3 = 3, type = i
10.0/3 = 3.33333, type = d
3/10.0 = 0.3, type = d
10.0/3.0f = 3.33333, type = d
unsigned 10/3 = 3, type = j
10/unsigned 3 = 3, type = j
第6行:为了便于解释,此处显式定义了一个值为3的整数i,根据前述章节的描述,它事实上是一个有符号整数(signed int)。接下来的代码里,我们使用auto【C++ 11】来推断商的结果类型,并使用typeid()操作符及其name成员函数▲将商的类型打印出来。
第8 ~ 9行:输出结果证实,整数/整数的结果类型为整数,商的小数部分被舍弃。请学习过Python语言的读者注意,这与Python语言不同。
第11 ~ 12行:输出结果证实,浮点数/整数的结果类型为浮点数,此处的浮点数类型为double。请读者注意,字面量10.0的类型也是double。作者在这里有意写成double(10.0),通过一个显式的double类型构造函数▲将10.0“转换”成一个double,是因为担心读者无法正确识别10.0字面量的类型而产生疑惑。
第14 ~ 15行:输出结果证实,整数/浮点数的结果类型为浮点数。
第17 ~ 18行:输出结果证实,双精度浮点数/单精度浮点数的结果类型为双精度浮点数。3.0f中的后缀f表明该字面量为float类型。
第20 ~ 24行:输出结果证实,有符号整数与无符号整数进行除法运算的结果为无符号整数。输出结果type = j中的j指无符号整数。
当表达式中两种不同类型的对象进行算术运算时,编译器总是将较小的类型转换为较大的类型再进行计算。第11行中,一个double除以一个int,编译器会先将整数i转换成double,再进行除法运算。两个double相除,其结果自然是double。
需要注意的是,这种形式的隐式类型转换只是创建一个被转换对象的副本,不会改变被转换对象自身。比如第11行的i被转成double,编译器只是创建了一个double类型的用完即弃的临时对象,其值与i相同。第11行代码执行前后,对象i不会有任何变化。
为了帮助更多的年轻朋友们学好编程,作者在B站上开了两门免费的网课,一门零基础讲Python,一门零基础C和C++一起学,拿走不谢!
如果你觉得纸质书看起来更顺手,目前Python有两本,C和C++在出版过程中。
普通读者如果期望学习好编程(C、C++或者Python),欢迎加入QQ频道:海洋饼干学编程的朋友们, 与众多志同道合的读者一起努力。
方法:在手机QQ频道(需要安装较新的版本)中搜索下述关键词,找到对应频道,然后选择加入。频道号:w395n39434。
 |
|