第10课|浮点与定点类型

21 阅读9分钟

本课目标

完成本课后,你将能够:

  • 深入理解 IEEE 754 浮点数的表示与精度限制
  • 掌握 Ada 中浮点类型的定义、属性与使用
  • 熟练使用定点类型实现精确小数运算
  • 理解 digits 与 delta 的物理意义及选择策略
  • 应用数值稳定性分析方法编写高可靠数值计算代码

一、浮点类型基础

1.1 IEEE 754 浮点表示

浮点数在计算机中采用科学记数法存储,由符号、指数和尾数三部分组成:

精度总位数符号位指数位尾数位十进制精度
单精度321823约 6~7 位
双精度6411152约 15~16 位
扩展精度8011564约 18~19 位

重要特性

  • 浮点运算是近似的,多数十进制小数无法精确表示(如 0.1)
  • 存在舍入误差、下溢、上溢和特殊值(NaN、无穷大)

1.2 Ada 中的预定义浮点类型

Ada 提供了三个标准的浮点类型族:

type Float          is digits implementation_defined;  -- 通常单精度
type Long_Float     is digits implementation_defined;  -- 通常双精度
type Long_Long_Float is digits implementation_defined; -- 扩展精度

实际精度由编译器决定,可通过属性查询:

with Ada.Text_IO;
procedure Float_Info is
begin
   Ada.Text_IO.Put_Line ("Float digits: " & Integer'Image (Float'Digits));
   Ada.Text_IO.Put_Line ("Float mantissa: " & Integer'Image (Float'Mantissa));
   Ada.Text_IO.Put_Line ("Float machine emax: " & Integer'Image (Float'Machine_Emax));
   Ada.Text_IO.Put_Line ("Float machine emin: " & Integer'Image (Float'Machine_Emin));
end Float_Info;

1.3 浮点字面量与赋值

-- 浮点字面量必须包含小数点或指数
X : Float := 3.14159;
Y : Float := 2.0E-10;          -- 科学记数法
Z : Float := 16#1.FFFF#E+2;    -- 基于16的浮点字面量

-- 显式类型转换(限定表达式)
A : Float := Float'(100);       -- 整数字面量转浮点

二、用户定义浮点类型

2.1 指定十进制精度

使用 digits 定义具有指定十进制精度的浮点类型:

-- 至少保证6位十进制有效数字
type Real is digits 6;

-- 同时指定范围
type Probability is digits 6 range 0.0 .. 1.0;

-- 高精度类型
type Double is digits 15;
type Quad is digits 18;  -- 若硬件支持

编译器会自动选择满足精度要求的最小硬件浮点格式(通常是 IEEE 单精度或双精度)。

2.2 浮点类型属性

属性含义示例
'Digits十进制精度位数Real'Digits = 6
'Mantissa二进制尾数位数(包括隐藏位)Float'Mantissa
'Machine_Emax最大指数Float'Machine_Emax
'Machine_Emin最小指数Float'Machine_Emin
'Epsilon1.0 与下一个可表示数的差值Float'Epsilon
'Safe_First算术运算安全范围的最小值Float'Safe_First
'Safe_Last算术运算安全范围的最大值Float'Safe_Last
'Model_Emin模型指数最小值Real'Model_Emin
'Model_Epsilon模型相对精度Real'Model_Epsilon

2.3 浮点类型派生

type Meters is new Float;          -- 派生新类型,单位语义
type Seconds is new Float;

-- 重载运算符确保单位安全
function "*" (M : Meters; T : Seconds) return Meters_Second is
begin
   return Meters_Second (Float (M) * Float (T));
end "*";

三、浮点运算与精度问题

3.1 舍入误差示例

with Ada.Text_IO;
procedure Rounding_Demo is
   X : Float := 0.1;
   Sum : Float := 0.0;
begin
   for I in 1 .. 10 loop
      Sum := Sum + X;
   end loop;
   Ada.Text_IO.Put_Line ("10 * 0.1 = " & Float'Image (Sum));
   -- 可能输出 0.999999... 而非 1.0
end Rounding_Demo;

3.2 比较浮点数

错误做法

if A = B then  -- 危险!因舍入误差很可能不相等

正确做法:使用容差比较

function Approx_Equal (A, B : Float; Eps : Float := 1.0E-6) return Boolean is
begin
   return abs (A - B) <= Eps * (abs A + abs B) / 2.0;  -- 相对误差
end Approx_Equal;

Ada 提供了通用函数 Ada.Numerics.Generic_Real_Arrays 等,也可用属性:

if abs (A - B) <= Float'Model_Epsilon * (abs A + abs B) then ...

3.3 避免精度丢失的技巧

  • 避免加减大数和小数:可能丢失小数部分
  • 使用 Kahan 求和算法:补偿舍入误差
  • 选择合适的数据类型:高精度类型如 Long_Long_Float
  • 避免对非常接近的数相减(灾难性抵消)
-- 灾难性抵消示例
A := 1.2345678;
B := 1.2345679;
C := B - A;  -- 有效数字严重丢失

3.4 特殊值与异常

-- 检测无效操作
if X /= X then  -- 只有 NaN 满足
   Ada.Text_IO.Put_Line ("X is NaN");
end if;

-- 检测无穷大
if X > Float'Safe_Last then
   Ada.Text_IO.Put_Line ("X is infinite");
end if;

四、定点类型基础

定点类型用整数运算模拟小数,精度固定,无舍入误差(但可能有溢出风险),常用于嵌入式、控制、金融等领域。

4.1 普通定点类型

使用 delta 指定最小步长(绝对精度),必须对称于0的范围:

-- 精度 0.01,范围 -100.0 .. 100.0
type Fixed is delta 0.01 range -100.0 .. 100.0;

-- 自动确定存储大小(通常为16位或32位整数)
F : Fixed := 12.34;

注意delta 值必须是2的负幂(或编译时能转换为2的负幂),如 0.125 是允许的,但 0.1 可能不行(除非编译器支持十进制定点)。

4.2 小数定点类型

使用 delta 和 digits 共同定义,指定小数位数和总十进制位数,适合货币计算:

-- 总共10位十进制,小数点后2位(由 delta 0.01 隐含决定)
type Money is delta 0.01 digits 10;

-- 指定总位数,delta 决定小数部分位数
type Price is delta 0.001 digits 7;  -- 范围 -9999.999 .. 9999.999

-- 十进制定点类型的范围自动计算

digits 指定总十进制位数(包括整数部分和小数部分),delta 指定小数部分的步长,二者必须兼容(delta <= 10.0**(-D) 其中 D 是小数位数)。

4.3 定点类型属性

属性含义示例
'Delta指定的 delta 值Fixed'Delta = 0.01
'Small实际存储的步长(通常等于 delta,除非使用 small 指定)Fixed'Small
'Fore整数部分所需的最小字符数(包括负号)Fixed'Fore
'Aft小数部分所需的最小字符数Fixed'Aft = 2
'Range类型的范围Fixed'Range
'Size存储位数Fixed'Size

五、定点类型的运算与控制

5.1 定点算术运算

with Ada.Text_IO;
procedure Fixed_Demo is
   type Fixed is delta 0.01 range -100.0 .. 100.0;
   A, B, C : Fixed := 10.0;
begin
   A := 12.34;
   B := 5.67;
   C := A + B;                 -- 18.01
   C := A - B;                 -- 6.67
   C := A * B;                 -- 乘法结果可能需要转换
   -- C := A * B;              -- 可能编译错误:定点乘法的结果类型不确定
   C := Fixed (A * B);         -- 需要显式转换
end Fixed_Demo;

定点乘法返回通用定点类型(universal_fixed),需要显式转换为具体类型。

5.2 溢出与范围检查

Ada 默认启用范围检查,防止溢出:

procedure Overflow_Demo is
   type Small is delta 0.1 range 0.0 .. 10.0;
   S : Small := 9.9;
begin
   S := S + 0.2;  -- 触发 Constraint_Error!9.9+0.2=10.1 > 10.0
exception
   when Constraint_Error =>
      Ada.Text_IO.Put_Line ("Overflow prevented");
end Overflow_Demo;

5.3 表示子句控制存储

可通过 small 和 size 精确控制定点类型的内存布局:

-- 强制步长为 2^{-8},用8位整数存储
type Angle is delta 2.0**(-8) range -Pi .. Pi;
for Angle'Small use 2.0**(-8);
for Angle'Size use 8;

5.4 定点与浮点转换

F : Float := 1.23;
D : Fixed := Fixed (F);          -- 浮点转定点(可能溢出)
F2 : Float := Float (D);         -- 定点转浮点

六、数值稳定性与误差控制

6.1 误差来源对比

类型误差来源特点
浮点舍入误差(相对误差固定)范围大,精度随数值大小变化
定点截断误差(绝对误差固定)范围受限,精度均匀

6.2 选择准则

  • 科学计算、图形、范围极大 → 浮点
  • 嵌入式控制、货币、需要均匀精度 → 定点
  • 对性能要求极高且无FPU → 定点
  • 需要避免舍入误差累积 → 定点可能更合适

6.3 误差控制策略

  • 浮点:使用高精度类型,采用稳定算法(如 Kahan 求和)
  • 定点:合理选择 delta,确保不溢出,必要时使用饱和运算
-- 饱和加法(超过范围时 clamp)
function Sat_Add (A, B : Fixed) return Fixed is
   Result : constant Fixed'Base := Fixed'Base (A) + Fixed'Base (B);
begin
   if Result > Fixed'Last then
      return Fixed'Last;
   elsif Result < Fixed'First then
      return Fixed'First;
   else
      return Fixed (Result);
   end if;
end Sat_Add;

七、完整示例:数值积分(梯形法则)

-- 文件名: numerical_integration.adb
-- 演示浮点与定点两种方式计算定积分

with Ada.Text_IO;
with Ada.Float_Text_IO;

procedure Numerical_Integration is

   -- 浮点版本
   function F_Float (X : Float) return Float is
   begin
      return 4.0 / (1.0 + X * X);
   end F_Float;

   function Integral_Float (N : Integer) return Float is
      H : constant Float := 1.0 / Float (N);
      Sum : Float := 0.0;
   begin
      for I in 1 .. N - 1 loop
         Sum := Sum + F_Float (Float (I) * H);
      end loop;
      return H * ((F_Float (0.0) + F_Float (1.0)) / 2.0 + Sum);
   end Integral_Float;

   -- 定点版本(精度 0.0001,范围 0..4)
   type Fixed is delta 0.0001 range 0.0 .. 4.0;

   function F_Fixed (X : Fixed) return Fixed is
   begin
      return Fixed (4.0 / (1.0 + Float (X) * Float (X)));
   end F_Fixed;

   function Integral_Fixed (N : Integer) return Fixed is
      H : constant Float := 1.0 / Float (N);
      Sum : Fixed := 0.0;
   begin
      for I in 1 .. N - 1 loop
         Sum := Sum + F_Fixed (Fixed (Float (I) * H));
      end loop;
      return Fixed (H) * ((F_Fixed (0.0) + F_Fixed (1.0)) / 2.0 + Sum);
   end Integral_Fixed;

   N : constant Integer := 1000;

begin
   Ada.Text_IO.Put_Line ("Float 积分值: " & Float'Image (Integral_Float (N)));
   Ada.Text_IO.Put_Line ("Fixed 积分值: " & Fixed'Image (Integral_Fixed (N)));
   Ada.Text_IO.Put_Line ("真实值 π:     3.1415926535...");
end Numerical_Integration;

编译运行:

gnatmake numerical_integration.adb
./numerical_integration

输出示例:

Float 积分值:  3.14159
Fixed 积分值:  3.1415
真实值 π:     3.1415926535...

八、本课总结

  • 浮点类型用 digits 指定精度,适用于大范围、相对误差要求场景
  • 定点类型用 delta 指定步长,适用于固定绝对精度、无FPU的场合
  • 浮点运算存在舍入误差,比较时应使用容差
  • 定点类型需注意范围溢出,Ada的范围检查提供保护
  • 'Small 控制实际存储精度,表示子句可精确控制内存布局
  • 数值稳定性取决于算法选择,必要时需进行误差分析

九、课后练习

1.精度探查:编写程序输出你所用编译器下 FloatLong_Float 和自定义 digits 18 类型的 'Digits'Mantissa'Epsilon

2.二次方程求解:用浮点实现二次方程求根公式,测试 a=1, b=-1000000, c=1 时的稳定性,并尝试改进算法(避免灾难性抵消)。

3.货币计算:定义type Salary is delta 0.01 digits 8;,计算年薪、月薪,测试极端值溢出行为。

4.物理单位:扩展前课的物理单位包,增加加速度类型Meters_Per_Second_Squared,并用定点实现匀加速运动位移公式。

5.比较浮点与定点:编写程序计算 1000 个 0.1 相加的结果,分别使用 Floatdelta 0.01 的定点类型,对比结果与精确值 100.0 的误差。


十、下节预告

第11课|枚举与布尔类型

我们将:

  • 深入枚举类型的定义与属性
  • 掌握布尔类型的逻辑运算与短路控制
  • 学习字符类型的分类与转换
  • 应用枚举类型实现状态机与选择控制

关键术语表

IEEE 754:浮点数国际标准,定义单精度、双精度等格式。

digits:指定浮点类型十进制精度的关键字。

delta:指定定点类型最小步长的关键字。

'Small:定点类型实际存储步长的属性。

舍入误差:浮点运算因精度有限产生的近似误差。

灾难性抵消:两个相近数相减导致有效数字严重丢失的现象。

Kahan 求和:补偿舍入误差的求和算法。

饱和运算:超出范围时截断到边界值的运算。