25年9月团队招新,自己带了一些小家伙,帮助他们进行C语言的学习,同时对他们的疑惑进行解答。
其中,有一个小家伙的问题很有意思,是初学者不尝思考、老家伙(我)也不一定清晰知道的点,涉及到数据的储存和转换方式,比较有趣,于是查阅资料学习后写下此文
关键词
-
浮点数的数据储存方式
-
变量的自动转化
前言
为了探究变量自动转换的问题,我们首先需要知道为什么程序中需要有变量类型的存在?
我们知道,人类发明的目的是为了更好得改善生活体验,其创造出的每个东西都有其存在的目的和价值。发明文字,是为了更好去进行个体交流;发明纸笔,是为了更好去传承经验和文明;而对于编程,其诞生的目的就是描述现实中的问题并丢给计算机快速解决。比如说,小明想要尽快搭车去学校,对于这个问题,由于选取的路线、车辆不同,转场时机不同,最后的结果可能有多种。对于人而言,一个个去模拟计算这个过程耗时极大的。而编程就是用计算机语言去描述这个问题,再利用计算机高效的处理效率快速解决问题。
为了描述这个问题,我们需要先创建问题的主体(小明)以及主体作用的对象(学校)和行为(搭校车)。这里的主体和作用对象就是程序中需要声明的变量,而行为就是程序另外一个重要组成部分------函数。
由于现实中对象的特征不同,因此转化为计算机变量对应的类型也不一样,常见的种类大致可以划分为:整型变量类型、字符串变量类型、浮点变量类型。本文主要围绕的,即是整型变量和浮点变量间的关系
变量的储存方式
我们都知道,计算机处理器的核心组件为二极管,本质上只能表示和处理二进制数据。而我们现在接触的编程语言之所以能够表示更加复杂的内容,是因为经过了计算机操作系统等软硬件的层层封装,把底层的二进制实现给隐藏起来。知道变量的二进制储存形式,将会对我们的代码理解、算法设计带来巨大的好处。
计算机上的所有文件本质都是一串01二进制码,变量也不例外。
在学习字符串部分,我们不难知道,在相同编码规范下的字符,其实际储存的数值和真实数据之间是一个一一映射的关系。拿ASCLL编码举例,字符a对应数值97,二进制数值01100001(8bit,1Byte),于是我们可以通过这样的映射规则去利用二进制编码储存我们想要的数据。

整型变量类型和浮点变量类型都是数的类型,本身组成部分就可以用二进制来表示,因此不需要实际数据和储存数据之间的转化关系表,只需要处理其中的正负值、小数值即可。
整型变量的储存方式
按照需求,整型变量需要能表示一定范围内的整数,包括正数以及负数。正数可以直接用二进制码进行表示,而为了解决负数表示的问题,编程语言开发者引入了一个补码的形式
补码
补码的目的是实现正负数的储存、将减法转变为加法以及统一正负数的储存与运算,转换规则如下:
正数补码:与原码相同
例如:十进制 5
(假设用 8 位存储)
原码 = 补码 = 00000101
负数补码:先取其绝对值原码,再按位取反,最后加1
例如:十进制 -5
(8 位存储)
绝对值原码:00000101
反码:(按位取反):11111010
补码(反码 + 1):11111011
按位取反后的反码与源码相加能溢出归零,因此这里的取反相当于取了一个可消去原码数值的"负数,实现了负数的表示(类比自然数中的相反数定义:两个数相加等于零,则一个数就是另一个数的负值)
同时,取反前后的二进制码普遍的特点是第一个二进制位正数为0,负数为1。为了统一规范,定第一个二进制位为符号位,因此,n字节的整型变量可表示范围为
-2\^{(n-1)} \\sim 2\^{(n-1)}-1
Range : -32768 \\sim 32767
int 变量类型对应4字节空间,可以储存的整数数范围为:
Boundary = 2\^{(n-1)} =2\^{ (4\*4-1)} = 2\^{15} = 32768
浮点数储存方式
因为本人也没有系统性了解浮点数储存方式,因此暂贴一篇文章,留待后面进行补充
参考资料:zhuanlan.zhihu.com
变量的自动转化
在计算机编程中,时常会遇到不同类型的数值变量计算问题。对于这个问题,理论上应该是先把所有参与运算的变量统一为标准类型,然后再进行计算,确保计算方式以及存储精度的统一。
不过,刚刚学习C语言的小家伙往往意识不到变量不统一带来的问题,经常出现计算表达式中int、float、double互用的场景,但又能正常运行,这是什么原因呢?
万能的编译器
现代编译器设计十分完备,在代码进行编译前会进行错误检查。对于一些常见的错误,编译器会自动进行纠正,例如这里的变量类型转换。
从编写程序代码到在机器上运行,其中不可或缺的步骤就是利用编译器进行编译(链接)。编译器编译过程大致分为以下几个阶段:
-
词法分析
-
语法分析
-
语义分析
-
中间代码生成
-
代码优化目标代码生成

其中,语义分析到目标代码生成过程,编译器会自动检查并进行必要的类型转换。在语义分析阶段,编译器会检查抽象语法树,检查表达式中的操作数的数据类型是否匹配,如果不匹配,则触发转换逻辑。
触发转换逻辑后,编译器首先会检查是否符合语言的隐式转换规则 (如"拓宽转换"允许int → float,但是"缩窄转换可能被禁止或者警告,如flota → int),然后在生成中间代码时插入专门的转换指令
编写代码时可以注意制定正确的变量类型,避免不必要的类型转换指令开销

允许的转换规则
编译器支持显式转换和隐式转换两种方式。显式转换即程序员在代码中使用("指定转换类型")
将变量转换为指定类型。
C
char ch = 'a';
int temp;
temp = (int)ch;
// temp 接收'a'对应转换为int型变量的值
// 字符 --> 整型的值对应字符对应的ASCLL码值
// 'a'对应ASCLL码表值为97,因此这里temp值对应更新为97
由于这种类型转换是程序员手动进行的,程序员知悉其转换对应带来的信息损失,评估并解决其潜在问题,因此编译器相信程序员的选择,不会出现问题报错。
隐式转换即程序员没有指定变量数据类型转换,但在程序计算部分需要进行数据转换的地方,编译器会根据一定规则进行安全的自动转换。
隐式数据类型转换一般遵循"大范围类型" --> "大范围类型"的转换,避免数据丢失。
这里的"范围"可以看做变量能够蕴含的信息量。例如,整型变量只能表达整数,而浮点型变量可以表达所有实数,因此范围:浮点型 > 整型
当程序需要进行"大范围类型"-->"小范围类型"的隐式操作时,编译器不确定这样的转换操作带来的信息缺失/变换是否会对程序产生危害,因此输出错误报错,提示程序员可能存在的问题,有程序员定夺是否进行变量转换。
常见允许的转换规则如下:
拓宽转换:
bool → char/int/long
char → short/int/long
int → long/float/double
float → double
缩窄转换
int → char
注意,待转换变量值需要在目标类型变量的取值范围内。例如,char接受(-128~127)范围内的值,则待转换的int类型变量值不能超过这个范围,否则会报错
总结
总而言之,编译器在编译程序的过程中需要确保生成的二进制文件在运行时不会出现重大错误,因此编译器对于隐式转换错误部分会直接产生报错退出,而对于程序员手动操作的显式转换部分,则相信程序员的设计不进行报错。理解这个point也就不难理解显式/隐式类型转换之间的关系。
在计算机编程中我们经常会遇到类似难以理解区分的概念,此时我们不妨去了解一下概念底层相关的计算机软硬件设计。在了解这些设计过程中你一定会对原有的问题和概念有深刻而清晰的认知。毕竟,现在计算机理论应用都是基于计算机体系机构一层层的封装与继承,了解底层,就掌握了程序运行的心脏。
参考文献
如有问题,欢迎在评论区进行斧正