Skip to content

⭐ 1.2 浮点数(上)

哦!看这个小标题👇,居然要一口气讲3个东西!看来有了上一节的基础,本章的介绍可以进入快车道了。

浮点数的类型、声明与赋值

1=1×1对吗?什么?太简单了?那1=0.1×10=10×0.1对吗?你问我是不是小学生?

好吧,请观察下面的等式:

\[ \begin{align} 1 &= 0.001 \times 1000 \\ &= 0.01 \times 100 \\ &= 0.1 \times 10 \\ &= 1 \times 1 \\ &= 10 \times 0.1 \\ &= 100 \times 0.01 \\ &= 1000 \times 0.001 \end{align} \]

如果写成科学计数法,那就是

\[ \begin{align} 1 &= 0.001 \times 10^3 \\ &= 0.01 \times 10^2 \\ &= 0.1 \times 10^1 \\ &= 1.0 \times 10^0 \\ &= 10.0 \times 10^{-1} \\ &= 100.0 \times 10^{-2} \\ &= 1000.0 \times 10^{-3} \end{align} \]

注意符号

在数学公式中,我们有时用 a^b 来表示ab。但是在C#编程中的正确写法应该是 Math.Pow(a, b),这个符号 ^ 起别的作用,一定不要混淆哦。

从上面这个数字游戏可以看出,科学计数法通过改变乘号右边10的指数,可以让乘号左边的数字的小数点动来动去。与普通写法相比,小数点的位置好像在随意浮动一般,因此叫它“浮点数”(Floating-point Number)。

相对的,普通写法的数字就叫做“定点数”了。整数可以写成定点数也可以写成浮点数(比如刚刚展示的数字1),小数也是如此。不要因为我们通常把小数写成浮点数,就把浮点数等同于小数

C#提供了两种二进制浮点数 floatdouble。所谓“二进制浮点数”就是在刚刚的案例中,把科学计数法由以10为底改为以2为底。那以10为底的呢,就顺理成章的叫做“十进制浮点数”了咯。在C#中,decimal类型就是一种十进制浮点数。看看它们的声明方法:

float a;
double b;
decimal c;

乖乖,和上一节的整数类型变量声明方法一模一样!事实上,这也适用于本章要讲的其他类型数据的声明。浮点数常数的声明和初始化也可以通过上一节的知识举一反三!但是浮点数在赋值方面有一些不同的特点。在你的代码文件中随便输入一个小数,就像这样:

0.1

Note

还记得吗?这样直接写在代码里的值叫做字面量。

此时可能有错误提示,但无需理会。请把鼠标指针移动到0.1上停住,等待一会,你会看到下面这样的提示:

图片里显示System.Double

也许其他说明我们看不懂,但“double”可是写的明明白白!也就是说,我们普通输入的小数,C#会默认它是double类型。上一节我们说到过,不能随便给某个类型的实例赋一个不相符类型的值对不对?所以可以预测,把普通输入的小数赋给double变量没事,但是赋给floatdecimal,甚至int类型的变量就会有问题了。动手测试一下吧:

float a = 0.1;
double b = 0.1;
decimal c = 0.000_1;//小数的数位也可以用下划线分隔
int d = 0.1;

果然——除了变量b以外都报错了。等等,刚刚说“普通输入的小数”,那怎么样的是不普通的小数呢?请看下面:

float a = 0.1f;
double b = 0.1d;
decimal c = 0.000_1m;

改成这样以后,错误都消失了!没错,为了区分这3种浮点数类型,需要在它们的字面量后面加上特定的字母:fF表示float类型,dD表示double类型,mM表示decimal类型。如果不加字母就会被认为是double

为什么decimal是字母m?

呃,据说首先是d被double占用了,于是设计者选择了比较独特的字母m作为后缀。

上一节我们知道了C#提供的不同整数类型有不同的取值范围、占用的空间大小也不一样。那么,这3种浮点数是不是也有这样的区别呢?正是!

类型 范围 占用空间 精度
float ±1.5x10−45 到 ±3.4x1038 32 bit 6~9位数字
double ±5.0×10−324 到 ±1.7×10308 64 bit 15~17位数字
decimal ±1.0x10-28 到 ±7.9228x1028 16 byte 28~29位数字

科学计数法可以方便的表示一串很长的数字,这也是浮点数相对于定点数的优势所在。与整数类型相比,浮点数的表示范围显然要大很多,占用的空间也比较大,但……精度?这是什么?

请问你知道1/3写成小数是多少吗?0.333333333……无限循环下去对不对?那你知道1/3在3进制里面是多少吗?

(1/3)10 = (1 × 3-1)10 = (10-1)3 = (0.1)3

在10进制里面的无限循环小数在3进制里面居然只是0.1?!难道有限小数转换进制之后有可能变成无限循环小数?是的!十进制里的0.1转换成二进制以后,就会变成0.0001100110011……这样的无限循环小数。虽然它们的位数是无限的,可是你的计算机的内存是有限的!于是计算机说,差不多得了吧,就在某个位置截断了这些小数。

可想而知,这样一定会造成计算误差!请尝试运行下面的代码:

Console.WriteLine(0.1 + 0.2);

真是笑掉大牙了,计算机连这么简单的算术都做不对。多出来的0.00000000000000004就来源于进制转换造成的计算误差。

为什么 0.1f + 0.2f 能算“对”?

其实结果也是不准确的,只是float类型的精度没达到能显示误差的数位,因此巧合地产生了0.3的假象。

使用十进制浮点数decimal类型就可以避免进制转换带来的误差。decimal被广泛用于计算精度要求高的场合,比如银行、金融业等(难道m后缀是money?)。有得必有失,decimal数值范围较小、占用空间较大、运算较慢就是计算机模拟十进制的代价。

为什么decimal采用十进制计算还是有误差(虽然很小)?

虽然不用转换进制,但十进制本身也有无限小数(比如1/3)。计算机处理过长的位数时依然会残忍地把它截断~

floatdoubledecimal之间到底如何选择?和上一节说的一样:根据你的需求。通常double就足够了(这也说明了为什么输入无后缀小数默认是double),除非你开发的游戏因为攻击判定不准而差评如潮,或者在研究登陆火星的项目,这时就要考虑使用精度更高的类型了。

如何选用二进制浮点类型

double 就好了。没事别想着用 float ,精度太垃圾了。除非节省内存的重要性已经超过了保持结果精度,比如一些游戏涉及图形的大规模计算场景,可以考虑用 float

类型转换

你一定还记得,浮点数不等于小数。所以,当我们给浮点类型的实例赋一个整数值时,也应该加上相应的后缀,否则就会引发错误对吧?像下面这样:

float a = 100;
double b = 666;
decimal c = 20;

什——么——居然没有报错!?究竟是怎么回事?为什么小数需要加后缀区分,而整数无论是在上一节的定点整数类型,还是本节介绍的3种浮点类型都可以直接赋值?

同样的,我们先在代码文件中随便找个空行输入一个整数字面量:

666

像刚刚那样,把鼠标移动到数字上停留,直到显示提示信息。请看下图红框处显示了它的类型:

System.Int32

System.Int32,好像没听说过?其实它就是你的老朋友——int的大名,而我们之前说的int其实是System.Int32的昵称,二者指的其实是同一种类型。

Tip

所以,int类型的变量也可以这样声明:

System.Int32 a = 1;

显然要比直接写int麻烦。所以别这么做。

莫非前面提到的其他类型也有全名?我要怎么查看一种类型的全名是什么?以double为例,把鼠标指针移到double关键字上:

System.Double

原来是System.Double。好吧。我们好像已经碰见了太多的.了。如果在数字里面,这显然是个小数点,但被两个单词夹在中间时呢?首先肯定不是句号的意思,因为我们用;表示句子的结束。

初识“类”与“成员”

可以笼统地把A.B理解为B是A的一个“成员”(member)。比如作者作为一个人类,有手,就用人.手来表达(“手”是“人”的成员)。 (好奇怪的发言) 手上还长了手指,就可以写成人.手.手指。手可以用来拿东西,“拿”这个动作也可以是手的成员 (更奇怪了) ,写成人.手.拿()这样。一个人用手拿着一个苹果——人.手.拿(苹果)。什么也不拿——人.手.拿()

嗯!所以System.Double就是说“System”的成员“Double”的意思。Console.WriteLine()就是“控制台”的成员“写一行”的意思咯,只不过这个“写一行”还是个动作,可以把你放进括号里的东西写出来罢了。(太棒了,我逐渐理解一切.jpg)

我们要说什么来着?哦对了,说到像666这样输入的整数会被默认识别为int类型,就像不加后缀的小数默认是double一样。也就是说,下面这几种情况:

float a = 666;
double b = 888;
short c = 100;

明明等号右边的数是int类型,却可以赋值给等号左边不是int类型的几种变量?!这岂不是违背了“不能随便给某个类型的实例赋一个不相符类型的值”的规则吗?什么时候会报错?什么时候又会平安无事?

隐式转换

还记得吗?上一节里我们聊到了,声明某个类型的变量后,运行时会为它分配一个为该类型定制的房型。所以不同类型的变量的确不能混着住。像float a = 666;这种安排int客人去住float房间的操作是不行的。

但是,善解人意(?)的编译器呢,猜测到了你的本意是想让float类型的变量a等于666f,所以悄悄地帮你把原本是int类型的666转换为了float类型的666f,然后再赋值到变量a中(常量的初始化也是这样)。因为担心打扰你专心工作,它甚至没有通知你一声(它真的,我哭死)。这就是所谓的“隐式类型转换”。

隐式类型转换也不是每次都能丝滑进行。在某些情况下,编译器也拿捏不准你的主意,或者它觉得光靠它可能干不好,这时它就会撂挑子不干,直接报错。可以总结出这些规则:

从A类型隐式转换到B类型,B类型能表达的范围应能覆盖A类型的范围。反过来就有超过表达范围的风险,编译器才不会傻到替你承担风险,所以直接生成一个错误扔给你。比如byte的范围是[0, 255],ushort的范围是[0, 65535],所以byte可以转换为ushort,反过来则不行。这就是数值提升(Numeric Promotion)规则。

记忆助手

如果你有一桶水,你不会担心把它倒进空的泳池里会产生什么后果;但当你尝试把它倒进空杯子里时,就会有溢出的风险了。

  • 特例1:像byte a = 100;这样的写法,是把类型范围更大的int转换为类型范围更小的byte,但是不报错。在这个过程中,你写的整数100首先会被默认识别为int,然后编译器会检查你写的数字有没有超过byte的范围,如果没有超过就帮你转换。如果不这样会怎么样?那就要为每个类型的整数指定一个后缀,然后逼你赋值时必须写成byte a = 100后缀这样,多麻烦啊。我们找不到足够多的后缀字母了。

Warning

这只对你写的字面量有效,如果不是直接赋一个数字字面量,而是赋一个int类型的变量,像:

int a = 100;
byte b = a;//a作为int变量赋值给b
那么对不起,编译器才不会帮你做范围检查呢,直接报错。所以特例1的存在多少有照顾编程习惯的意思。

Note

特例1只对整数有效!在float a = 10.0;这个例子中,你写的无后缀的小数会被默认识别为double类型。然后呢,编译器根本不管10.0有没有超出float能表示的范围,直接报错!

浮点数的转化可能导致精度损失,编译器不会帮你承担这个风险。

  • 特例2:decimal类型作为高精度的十进制浮点数,不能隐式转换为其他类型。类型范围比decimal覆盖范围小的整数类型可以转换为decimal;二进制浮点数floatdouble不能转换为decimal

显式转换

当编译器认为转换行为有风险时,就不会执行自动的隐式转换。如果你认为转换有必要且能掌控风险,可以选择手动强制转换(显式转换)。

本来double是不能转换为int的,但可以用这种方式:

//声明两种变量
int a;
double b = 10.9;

//强制转换类型
a = (int)b;
Console.WriteLine(a);

在被转换的变量前面加上用括号()括起来的准备转换成的类型。也可以写得紧凑些:

int a = (int)10.9;
Console.WriteLine(a);

在运行代码之前,请先预测一下结果。会是11吗?还是10?

结果是10,也就是直接把小数部分截断而不是四舍五入。因此,涉及到显式类型转换时,请确保你了解转换的机制,避免得到与你的预期不符的结果。猜猜下面这段代码会输出什么?

byte a;
int b = 1000;

a = (byte)b;
Console.WriteLine(a);

是255?还是100?结果是232!为什么?!

因为1000是以二进制11 1110 1000储存的,转换为byte时只留下了最后8位1110 1000,也就是十进制的232。通过这个案例再次告诉我们使用显式转换时一定要谨慎。

二进制和十六进制的字面量

在C#中,二进制数字需要在开头加上0b0B作为标志,比如0b1000表示二进制的数字1000。

十六进制数字需要在开头加上0x或者0X,比如0xE4表示十六进制数字E4。

页面过长

警报!警报!本节教程过长,请先休息一下,喝一杯水后继续阅读。🚰