文章目录
- 第27章 C99对数学计算的新增支持
- 27.1 <stdint.h>: 整数类型(C99)
- 27.1.1 <stdint.h>类型
- 27.1.2 对指定宽度整数类型的限制
- 27.1.3 对其他整数类型的限制
- 27.1.4 用于整型常量的宏
- 27.2 <inttype.h>: 整数类型的格式转换(C99)
- 27.2.1 用于格式指定符的宏
- 27.2.2 用于最大宽度整数类型的函数
- 27.3 复数(C99)
- 27.3.1 复数的定义
- 27.3.2 复数的算术运算
- 27.3.3 C99中的复数类型
- 27.3.4 复数的运算
- 27.3.5 复数类型的转换规则
- 27.4 <complex.h>: 复数算术运算(C99)
- 27.4.1 <complex.h>宏
- 27.4.2 CX_LIMITED_RANGE编译提示
- 27.4.3 <complex.h>中的函数
- 27.4.4 三角函数
- 27.4.5 双曲函数
- 27.4.6 指数函数和对数函数
- 27.4.7 幂函数和绝对值函数
- 27.4.8 操作函数
- 27.5 <tgmath.h>: 泛型数学(C99)
- 27.5.1 泛型宏
- 27.5.2 调用泛型宏
- 27.6 <fenv.h>: 浮点环境(C99)
- 27.6.1 浮点状态标志和控制模式
- 27.6.2 <fenv.h>宏
- 27.6.3 FENV_ACCESS编译提示
- 27.6.4 浮点异常函数
- 27.6.5 舍入函数
- 27.6.6 环境函数
- 问与答
- 写在最后
第27章 C99对数学计算的新增支持
——先繁后简,而非先简后繁。
本章介绍
C99
新增的5
个标准头,对标准库的介绍至此将全部结束。这些头与其他头一样,也提供了处理数的方法,但更有针对性。其中一些只对工程师、科研人员和数学工作者有用,他们可能需要在数的表示和浮点运算的执行方式上进行更多的控制,还可能需要用到复数。
前两节讨论与整数
类型相关的头。<stdint.h>头(27.1节)
声明了具有指定位数的整数类型。<inttypes.h>头(27.2节)
提供了可读写<stdint.h>
型值的宏。
之后的两节描述了
C99
对复数
的支持。27.3节
回顾了复数的概念,并讨论了C99
中的复数类型。随后27.4节
介绍了<complex.h>
头,它提供了对复数进行数学运算的函数。
最后两节讨论的头与浮点类型
有关。<tgmath.h>头(27.5节)
提供了泛型宏,这使得调用<complex.h>
和<math.h>
中的函数更方便。<fenv.h>头(27.6节)
中的函数允许程序访问浮点状态标志和控制模式。
27.1 <stdint.h>: 整数类型(C99)
<stdint.h>
声明了包含指定位数
的整数类型。另外,它还定义了表示其他头中声明的整数类型和自己声明的整数类型的最小值和最大值的宏[这些宏是对<limits.h>头(23.2节)
中的宏的补充]。<stdint.h>
还定义了构建具体类型的整型常量的带参数的宏。<stdint.h>
中没有函数。
7.5节
讨论了类型定义对程序可移植性的作用,C99
增加<stdint.h>
的动机即源于这一认识。例如,如果i
是int
型的变量,那么赋值语句
i = 100000;
在int
是32
位的类型时是没问题的,但如果int
是16
位的类型就会出错。问题在于C
标准没有精确地说明int
值有多少位。标准可以保证int
型的值一定包括-32767~32767
范围内的所有整数(要求至少16
位),但没有进一步的规定。示例中的变量i
需要存储100000
,传统的解决方案是把i
的类型声明为某种由typedef
创建的类型T
,然后在特定的实现中根据整数的大小调整T
的声明。(T
在16
位的机器上应该是long int
类型,但在32
位的机器上可以是int
类型。)这是7.5节
中提到的策略。
如果编译器支持
C99
,还有一种更好的方法。<stdint.h>
基于类型的宽度(存储该类型的值所需的位数,包括可能出现的符号位)声明类型的名字。<stdint.h>
中声明的typedef
名字可以涉及基本类型(如int
、unsigned int
和long int
),也可以涉及特定实现所支持的扩展整数类型。
27.1.1 <stdint.h>类型
<stdint.h>
中声明的类型可分为以下5
组:
-
精确宽度整数类型
。每个形如intN_t
的名字表示一种N
位的有符号整数类型,存储为2
的补码形式。(2
的补码是一种用二进制表示有符号整数的方法,在现代计算机中非常普遍。)例如,int16_t
型的值可以是16
位的有符号整数。形如uintN_t
的名字表示一种N
位的无符号整数类型。如果某个具体的实现支持宽度N
等于8
、16
、32
和64
的整数,它需要同时提供intN_t
和uintN_t
。 -
最小宽度整数类型
。每个形如int_leastN_t
的名字表示一种至少N
位的有符号整数类型。形如uint_leastN_t
的名字表示一种至少N
位的无符号整型。<stdint.h>
至少应提供下列最小宽度类型:int_least8_t uint_least8_t int_least16_t uint_least16_t int_least32_t uint_least32_t int_least64_t uint_least64_t
-
最快的最小宽度整数类型
。每个形如int_fastN_t
的名字表示一种至少N
位的最快的有符号整型。(“最快”的含义因实现的不同而不同。如果没有办法分辨一种特定的类型是否为最快的,则可以选择任何一种至少N
位的有符号整型。)每个形如uint_fastN_t
的名字表示一种至少N
位的最快的无符号整型。<stdint.h>
至少应提供下列最快的最小宽度类型:int_fast8_t uint_fast8_t int_fast16_t uint_fast16_t int_fast32_t uint_fast32_t int_fast64_t uint_fast64_t
-
可以保存对象指针的整数类型
。intptr_t
类型表示可以安全存储任何void*
型值的有符号整型。更准确地说,如果把void*
型指针转换为intptr_t
类型然后再转换回void*
类型,所得的指针应该和原始指针相等。uintptr_t
类型是一种无符号整型,其性质和intptr_t
相同。<stdint.h>
不一定要提供这两种类型。 -
最大宽度整数类型
。intmax_t
是一种有符号整型,包括任意有符号整型的值。uintmax_t
是一种无符号整型,包括任意无符号整型的值。<stdint.h>
应提供这两种类型,它们的宽度可能超过long long int
。
前3
组中的名字使用typedef
声明。
除了上面列出的类型外,实现中还可以提供值为
N
的精确宽度整数类型、最小宽度整数类型以及最快的最小宽度整数类型。此外,N
可以不是2
的幂(不过一般为8
的倍数)。例如,实现可以提供名为int24_t
和uint24_t
的类型。
27.1.2 对指定宽度整数类型的限制
<stdint.h>
为其中的每一个有符号整数类型定义了两个宏,用于指明该类型的最小值和最大值,并为其中的每一个无符号整数类型定义了一个宏,用于指明该类型的最大值。表27-1
中的前三行给出了精确宽度整数类型对应的宏的值,其他的行给出了C99
对<stdint.h>
中其他类型的最小值和最大值的约束。(这些宏的精确值由实现定义。)表中所有的宏都是常量表达式。
表27-1 <stdint.h>
对指定宽度整数类型进行限制的宏
名称 | 值 | 含义 |
---|---|---|
INTN_MIN | -( 2 N − 1 {2^{N-1}} 2N−1) | 最小的intN_t值 |
INTN_MAX | 2 N − 1 {2^{N-1}} 2N−1-1 | 最大的intN_t值 |
UINTN_MAX | 2 N {2^N} 2N-1 | 最大的uintN_t值 |
INT_LEASTN_MIN | ≤-( 2 N − 1 {2^{N-1}} 2N−1-1) | 最小的int_leastN_t值 |
INT_LEASTN_MAX | ≥ 2 N − 1 {2^{N-1}} 2N−1-1 | 最大的int_leastN_t值 |
UINT_LEASTN_MAX | ≥ 2 N {2^N} 2N-1 | 最大的uint_leastN_t值 |
INT_FASTN_MIN | ≤-( 2 N − 1 {2^{N-1}} 2N−1-1) | 最小的int_fastN_t值 |
INT_FASTN_MAX | ≥ 2 N − 1 {2^{N-1}} 2N−1-1 | 最大的int_fastN_t值 |
UINT_FASTN_MAX | ≥ 2 N {2^N} 2N-1 | 最大的uint_fastN_t值 |
INTPTR_MIN | ≤-( 2 15 {2^{15}} 215-1) | 最小的intptr_t值 |
INTPTR_MAX | ≥ 2 15 {2^{15}} 215-1 | 最大的intptr_t值 |
UINTPTR_MAX | ≥ 2 16 {2^{16}} 216-1 | 最大的uintptr_t值 |
INTMAX_MIN | ≤-( 2 63 {2^{63}} 263-1) | 最小的intmax_t值 |
INTMAX_MAX | ≥ 2 63 {2^{63}} 263-1 | 最大的intmax_t值 |
UINTMAX_MAX | ≥ 2 64 {2^{64}} 264-1 | 最大的uintmax_t值 |
27.1.3 对其他整数类型的限制
C99
委员会在创建<stdint.h>
时认为,这个地方也应该存放对不在其中声明的整数类型进行限制的宏。这些类型有ptrdiff_t
、size_t
、wchar_t
[这三个属于<stddef.h>(21.4节)
]、sig_atomic_t
[在<signal.h>(24.3节)
中声明]和wint_t
[在<wchar.h>(25.5节)
中声明]。表27-2
列出了这些宏以及它们的值(或者C99
标准中的约束)。在一些情况下,对类型的最小值和最大值限制与该类型是有符号型还是无符号型有关。与表27-1
相似,表27-2
中的宏都是常量表达式。
表27-2 <stdint.h>
对其他整数类型进行限制的宏
名称 | 值 | 含义 |
---|---|---|
PTRDIFF_MIN | ≤-65535 | 最小的ptrdiff_t值 |
PTRDIFF_MAX | ≥+65535 | 最大的ptrdiff_t值 |
SIG_ATOMIC_MIN | ≤-127(如果有符号),0(如果无符号) | 最小的sig_atomic_t值 |
SIG_ATOMIC_MAX | ≥+127(如果有符号),≥255(如果无符号) | 最大的sig_atomic_t值 |
SIZE_MAX | ≥65535 | 最大的size_t值 |
WCHAR_MIN | ≤-127(如果有符号),0(如果无符号) | 最小的wchar_t值 |
WCHAR_MAX | ≥+127(如果有符号),≥255(如果无符号) | 最大的wchar_t值 |
WINT_MIN | ≤-32767(如果有符号),0(如果无符号) | 最小的wint_t值 |
WINT_MAX | ≥+32767(如果有符号),≥65535(如果无符号) | 最大的wint_t值 |
27.1.4 用于整型常量的宏
<stdint.h>
还提供了类似函数的宏,这些宏能够将(用十进制、八进制或十六进制表示,
但是不带后缀U
或者L
的)整型常量(7.1节
)转换为属于最小宽度整数类型或最大宽度整数类型的常量表达式。
<stdint.h>
为其中声明的每一个int_leastN_t
类型定义了一个名为INTN_C
的带参数的宏,用于将整型常量转换为这个类型(可能会用整数提升,7.4节
)。对于每一个uint_leastN_t
类型,也有一个类似的带参数的宏UINTN_C
。这些宏对于变量初始化非常有用(当然,还有别的作用)。例如,如果i
是int_least32_t
型的变量,这样的写法
i = 100000;
会有问题,因为常量100000
可能会因为太大而不能用int
型表示(如果int
是16
位的类型)。但是如果写成
i = INT32_C(100000);
则是安全的。如果int_least32_t
表示int
类型,那么INT32_C(100000)
是int
型。但如果int_least32_t
表示long int
类型,那么INT32_C(100000)
是long int
型。
<stdint.h>
还有另外两个带参数的宏:INTMAX_C
将整型常量转换为intmax_t
类型,
UINTMAX_C
将整型常量转换为uintmax_t
类型。
27.2 <inttype.h>: 整数类型的格式转换(C99)
<inttypes.h>
与上一节讨论的<stdint.h>
紧密相关。事实上,<inttypes.h>
包含了<stdint.h>
,所以包含了<inttypes.h>
的程序就不需要再包含<stdint.h>
了。<inttypes.h>
从两方面对<stdint.h>
进行了扩展。首先,它定义了可用于...printf
和...scanf
格式串的宏,这些宏可以对<stdint.h>
中声明的整数类型进行输入/输出操作。其次,它提供了可以处理最大宽度整数的函数。
27.2.1 用于格式指定符的宏
<stdint.h>
中声明的类型可以使程序更易于移植,但也给程序员带来了新的麻烦。考虑这个问题:显示int_least32_t
型变量i
的值。语句
printf("i = %d\n", i);
有可能不会工作,因为i
不一定是int
型的。如果int_least32_t
是long int
型的别名,那么正确的转换说明应为%ld
而不是%d
。为了按可移植的方式使用...printf
和...scanf
函数,我们需要使所书写的转换说明能对应于<stdint.h>
中声明的每一种类型。这就是<inttypes.h>
的由来。对于<stdint.h>
中的每一种类型,<inttypes.h>
都提供了一个宏,该宏可以扩展为一个包含该类型对应的转换指定符的字面串。
每个宏名由以下三个部分组成:
- 名字以
PRI
或SCN
开始,具体以哪个开始取决于宏是用于...printf
函数调用还是用于...scanf
函数调用。 - 接下来是一个单字母的转换指定符(有符号类型用
d
或i
,无符号类型用o
、u
、x
或X
)。 - 名字的最后一个部分用于指明该宏对应于
<stdint.h>
中的哪种类型。例如,与
int_leastN_t
类型对应的宏的名字应该以LEASTN
结尾。
回到前面那个显示
int_least32_t
型整数的例子。我们把转换指定符从d
改成了PRIDLEAST32
宏。为了使用这个宏,我们将printf
格式串分为三个部分,并把%d
中的d
替换为PRIDLEAST32
:
printf("i = %" PRIdLEAST32 "\n", i);
PRIDLEAST32
的值可能是"d"
(如果int_least32_t
等同于int
类型)或"ld"
(如果int_least32_t
等同于long int
类型)。为了讨论方便,我们假定其为"ld"
。宏替换之后,语句变为
printf("i = %" "ld" "\n", i);
一旦编译器将这三个字面串连成一个(自动完成),语句将变成如下形式:
printf("i = %ld\n", i);
注意,转换说明中仍然可以包含标志、栏宽和其他选项。PRIDLEAST32
只提供转换指定符,可能还有一个长度指定符,比如字母l
。
表27-3
列出了<inttypes.h>
中的宏:
表27-3 <inttypes.h>
中用于格式说明的宏
用处 | 宏名 |
---|---|
用于有符号整数的…printf宏 | PRIdN、PRIdLEASTN、PRIdFASTN、PRIdMAX、PRIdPTR、PRIiN、PRIiLEASTN、PRIiFASTN、PRIiMAX、PRIiPTR |
用于无符号整数的…printf宏 | PRIoN、PRIoLEASTN、PRIoFASTN、PRIoMAX、PRIoPTR、PRIuN、PRIuLEASTN、PRIuFASTN、PRIuMAX、PRIuPTR、PRIxN、PRIxLEASTN、PRIxFASTN、PRIxMAX、PRIxPTR、PRIXN、PRIXLEASTN、PRIXFASTN、PRIXMAX、PRIXPTR |
用于有符号整数的…scanf宏 | SCNdN、SCNdLEASTN、SCNdFASTN、SCNdMAX、SCNdPTR、SCNiN、SCNiLEASTN、SCNiFASTN、SCNiMAX、SCNiPTR |
用于无符号整数的…scanf宏 | SCNoN、SCNoLEASTN、SCNoFASTN、SCNoMAX、SCNoPTR、SCNuN、SCNuLEASTN、SCNuFASTN、SCNuMAX、SCNuPTR、SCNxN、SCNxLEASTN、SCNxFASTN、SCNxMAX、SCNxPTR |
27.2.2 用于最大宽度整数类型的函数
intmax_t imaxabs(intmax_t j);
imaxdiv_t imaxdiv(intmax_t numer, intmax_t denom);
intmax_t strtoimax(const char * restrict nptr,
char ** restrict endptr, int base);
uintmax_t strtoumax(const char * restrict nptr,
char ** restrict endptr, int base);
intmax_t wcstoimax(const wchar_t * restrict nptr,
wchar_t ** restrict endptr, int base);
uintmax_t wcstoumax(const wchar_t * restrict nptr,
wchar_t ** restrict endptr, int base);
除了定义宏之外,<inttypes.h>
还提供了用于最大宽度整数类型(在27.1节
介绍过)的函数。最大宽度整数的类型为intmax_t
(实现所支持的最宽的有符号整数类型)或uintmax_t
(最宽的无符号整数类型)。这些类型可能与long long int
型具有相同的宽度,也可以更宽。例如,long long int
型可能是64
位宽,而intmax_t
和uintmax_t
可能是128
位宽。
imaxabs
和imaxdiv
函数是<stdlib.h>(26.2节)
中声明的整数算术运算函数的最大宽度版本。imaxabs
函数返回参数的绝对值。参数和返回值的类型都是intmax_t
。imaxdiv
函数用第一个参数除以第二个参数,返回imaxdiv_t
型的值。imaxdiv_t
是一个包含商(quot)
成员和余数(rem)
成员的结构,这两个成员的类型都是intmax_t
。
strtoimax
和strtoumax
函数是<stdlib.h>
中的数值转换函数的最大宽度版本。strtoimax
函数与strtol
和strtoll
类似,但返回值的类型是intmax_t
。strtoumax
函数与strtoul
和strtoull
类似,但返回值的类型是uintmax_t
。如果没有执行转换,strtoimax
和strtoumax
都返回零。如果转换产生的值超出函数返回类型的表示范围,两个函数都将ERANGE
存于errno
中。另外,strtoimax
返回最小或最大的intmax_t
型值(INTMAX_MIN
或INTMAX_MAX
),strtoumax
返回最大的uintmax_t
型值(UINTMAX_MAX
)。
wcstoimax
和wcstoumax
函数是<wchar.h>
中的宽字符串数值转换函数的最大宽度版本。wcstoimax
函数与wcstol
和wcstoll
类似,但返回值的类型是intmax_t
。wcstoumax
函数与wcstoul
和wcstoull
类似,但返回值的类型是uintmax_t
。如果没有执行转换,wcstoimax
和wcstoumax
都返回零。如果转换产生的值超出函数返回类型的表示范围,两个函数都将ERANGE
存于errno
中。另外,wcstoimax
返回最小或最大的intmax_t
型值(INTMAX_MIN
或INTMAX_MAX
),strtoumax
返回最大的uintmax_t
型值(UINTMAX_MAX
)。另外,wcstoimax
返回最小或最大的intmax_t
型值(INTMAX_MIN
或INTMAX_MAX
),wcstoumax
返回最大的uintmax_t
型值(UINTMAX_MAX
)。
27.3 复数(C99)
除了数学领域之外,复数还用于科学和工程应用领域。
C99
提供了几种复数类型,允许操作符的操作数为复数,同时将<complex.h>
加入了标准函数库。不过,并非所有的C99
实现都支持复数。14.3节
中讨论过托管式C99
实现和独立式实现之间的区别。托管式实现必须能够接受符合C99
标准的程序,而独立式实现不需要能够编译使用复数类型或除<float.h>
、<iso646.h>
、<limits.h>
、<stdarg.h>
、<stdbool.h>
、<stddef.h>
和<stdint.h>
之外的头的程序。所以,独立式实现有可能同时缺少复数类型和<complex.h>
。
我们先回顾一下复数的数学定义和复数运算,然后再看看C99
的复数类型以及对这些类型的值可以进行哪些运算。27.4节
会继续讨论复数,那里主要描述<complex.h>
。
27.3.1 复数的定义
设
i
是-1
的平方根(满足条件 i 2 = − 1 {i^2=-1} i2=−1)。i
称为虚数单位(imaginary unit)
——工程师通常用符号j
而不是i
来表示虚数单位。复数的形式为 a + b i {a+bi} a+bi,其中a
和b
是实数。我们称a
为该数的实部
,b
为虚部
。注意!实数是复数的特例(b=0
的情况)。
复数有什么用呢?首先,它可以解决之前不能解决的问题。考虑方程
x
2
+
1
=
0
{x^2+1=0}
x2+1=0,如果限定x
为实数则无解,如果允许复数,这个方程有两个解:x=i
和x=-i
。
可以把复数想象为二维空间中的点,该二维空间称为复平面(complex plane)
。每个复数(复平面中的点)用笛卡儿坐标表示,其中复数的实部对应于点的x
轴坐标,虚部对应于y
轴坐标。例如,复数
2
+
2.5
i
{2+2.5i}
2+2.5i、
1
−
3
i
{1-3i}
1−3i、
−
3
−
2
i
{-3-2i}
−3−2i和
−
3.5
+
1.5
i
{-3.5+1.5i}
−3.5+1.5i可以作图为
另一种称为极坐标(polarcoordinates)
的系统也可以用于描述复平面中的点。在极坐标系中,复数z
用r
和θ
表示,其中r
是原点到z
的线段长度,θ
是该线段和实轴之间的夹角:
r
称作z
的绝对值(绝对值也称为范数、模或幅值),θ
称为z
的辐角(或相角)。
a
+
b
i
{a+bi}
a+bi的绝对值由下式给出:
∣ a + b i ∣ = a 2 + b 2 {|a+bi|=\sqrt{a^2+b^2}} ∣a+bi∣=a2+b2
27.3.2 复数的算术运算
两个复数相加等价于把它们的实部和虚部分别相加。例如:
( 3 − 2 i ) + ( 1.5 + 3 i ) = ( 3 + 1.5 ) + ( − 2 + 3 ) i = 4.5 + i {(3-2i)+(1.5+3i)=(3+1.5)+(-2+3)i=4.5+i} (3−2i)+(1.5+3i)=(3+1.5)+(−2+3)i=4.5+i
两个复数相减的计算也是类似的,把它们的实部和虚部分别相减即可。例如:
( 3 − 2 i ) − ( 1.5 + 3 i ) = ( 3 − 1.5 ) + ( − 2 − 3 ) i = 1.5 − 5 i {(3-2i)-(1.5+3i)=(3-1.5)+(-2-3)i=1.5-5i} (3−2i)−(1.5+3i)=(3−1.5)+(−2−3)i=1.5−5i
两个复数相乘,需要把第一个复数的每一项乘以第二个复数的每一项,然后把乘积相加:
( 3 − 2 i ) × ( 1.5 + 3 i ) = ( 3 × 1.5 ) + ( 3 × 3 i ) + ( − 2 i × 1.5 ) + ( − 2 i × 3 i ) = 4.5 + 9 i − 3 i − 6 i 2 = 10.5 + 6 i {(3-2i)×(1.5+3i)=(3×1.5)+(3×3i)+(-2i×1.5)+(-2i×3i)=4.5+9i-3i-6i^2=10.5+6i} (3−2i)×(1.5+3i)=(3×1.5)+(3×3i)+(−2i×1.5)+(−2i×3i)=4.5+9i−3i−6i2=10.5+6i
注意,这里用恒等式 i 2 = − 1 {i^2=-1} i2=−1来简化计算结果。
复数的除法相对难一些。首先需要了解一下复共轭的概念,一个数的复共轭通过变换其虚部的符号得到。例如, 7 − 4 i {7-4i} 7−4i是 7 + 4 i {7+4i} 7+4i的共轭, 7 + 4 i {7+4i} 7+4i也是 7 − 4 i {7-4i} 7−4i的共轭。我们用 z ∗ {z^*} z∗来表示复数
z
的共轭。
复数y
和z
的商由下面的公式给出:
y / z = y z ∗ / z z ∗ {y/z=yz^*/zz^*} y/z=yz∗/zz∗
z z ∗ {zz^*} zz∗总是实数,所以用 y z ∗ {yz^*} yz∗除以 z z ∗ {zz^*} zz∗非常容易(只要将 y z ∗ {yz^*} yz∗的实部和虚部分别除以 z z ∗ {zz^*} zz∗即可)。下面的示例展示了 10.5 + 6 i {10.5+6i} 10.5+6i 除以 3 − 2 i {3-2i} 3−2i的计算过程:
10.5 + 6 i 3 − 2 i = ( 10.5 + 6 i ) ( 3 + 2 i ) ( 3 − 2 i ) ( 3 + 2 i ) = 19.5 + 39 i 13 = 1.5 + 3 i {\frac{10.5+6i}{3-2i}=\frac{(10.5+6i)(3+2i)}{(3-2i)(3+2i)}=\frac{19.5+39i}{13}=1.5+3i} 3−2i10.5+6i=(3−2i)(3+2i)(10.5+6i)(3+2i)=1319.5+39i=1.5+3i
27.3.3 C99中的复数类型
C99
内建了许多对复数的支持。我们不需要包含任何头就可以声明表示复数的变量,然后对这些变量进行算术和其他运算。
C99
提供了3
种复数类型(7.2节
曾提到过):float _Complex
、double _Complex
和long double _Complex
。这些类型的使用方法与C
中其他类型的使用方法一样,可以用于声明变量、参数、返回类型、数组元素以及结构和联合的成员等。例如,我们可以这样声明3
个变量:
float _Complex x;
double _Complex y;
long double _Complex z
上面每个变量的存储与包含两个普通浮点数的数组的存储一样。所以,y
存储为两个相邻的double
型值,其中第一个值包含y
的实部,第二个值包含y
的虚部。
C99
还允许实现提供虚数类型(关键字_Imaginary
就是为这个目的保留的),但并不做强制要求。
27.3.4 复数的运算
复数可以用在表达式中,但只有以下这些运算符允许操作数为复数:
- 一元的
+
和-
; - 逻辑非(
!
); sizeof
;- 强制类型转型;
- 乘法类运算(仅
*
和/
); - 加法类运算(
+
和-
); - 判等(
==
和!=
); - 逻辑与(
&&
); - 逻辑或(
||
); - 条件(
?:
); - 简单赋值(
=
); - 复合赋值(仅
*=
、/=
、+=
和-=
); - 逗号(
,
)。
不在此列的主要运算符包括关系运算符(<
、<=
、>
和>=
),以及自增运算符(++
)和自减运算符(--
)等。
27.3.5 复数类型的转换规则
7.4节
描述了C99
的类型转换规则,但没有涉及复数类型,本节就来补上相应内容。不过,在介绍转换规则之前,我们需要知道一些新的术语。对于每一种浮点类型,都有一种对应实数类型(corresponding real type)
。对于实浮点类型(float
、double
和long double
)来说,对应实数类型与原始类型一样。对于复数类型而言,对应实数类型是原始类型去掉_Complex
。(例如,float _Complex
的对应实数类型为float
。)
现在可以讨论有关复数类型的转换规则了。这些规则分为3
类。
复数转换为复数
。第一条规则考虑从一种复数类型到另一种复数类型的转换,例如把float _Complex
转换为double _Complex
。在这种情况下,实部和虚部分别使用对应实数类型的转换规则(见7.4节
)进行转换。在这个例子中,float _Complex
值的实部转换为double
型,得到double _Complex
值的实部,虚部用类似的方式转换为double
型。实数转换为复数
。把实数类型的值转换为复数类型时,使用实数类型之间的转换规则生成复数的实部,虚部设置为正的零或者无符号的零。复数转换为实数
。把复数类型的值转换为实数类型时,丢弃虚部并使用实数类型之间的转换规则生成实部。
常规算术转换指的是一组特定的类型转换,它们可以自动作用于大多数二元运算符的操作数。当两个操作数中至少有一个为复数类型的情况下,执行常规算术转换还有一些特殊的规则:
- 如果任一操作数的对应实数类型为
long double
,那么对另一个操作数进行转换,使它的对应实数类型为long double
; - 否则,如果任一操作数的对应实数类型为
double
型,那么对另一个操作数进行转换,使它的对应实数类型为double
; - 否则,必然有一个操作数的对应实数类型为
float
。对另一个操作数进行转换,使它的对应实数类型也为float
。
转换之后,实操作数仍然属于实数类型,复操作数仍然属于复数类型。
通常,常规算术转换的目的是使两个操作数具有共同的类型。但是,当同时使用实操作数和复操作数时,常规算术转换会使两个操作数具有共同的实数类型,但并不一定是同一种类型。例如,如果把
float
型的操作数和double _Complex
型的操作数相加,float
型的操作数将转换为double
型而不是double _Complex
型。结果的类型是一个复数类型,其对应实数类型与共同的实数类型相匹配。在这个例子中,结果的类型是double _Complex
。
27.4 <complex.h>: 复数算术运算(C99)
从27.3节
可以看到,C99
内建了许多支持复数的特性。<complex.h>
不仅提供了一些有用的宏和一条#pragma
指令,还以数学函数的形式提供了一些额外的支持。我们先来看看宏。
27.4.1 <complex.h>宏
<complex.h>
定义了表27-4
所示的宏。
表27-4 <complex.h>
宏
名称 | 值 |
---|---|
complex | _Complex |
_Complex_I | 虚数单位,类型为const float _Complex |
I | _Complex_I |
complex
是关键字_Complex
的别名。之前在讨论布尔类型时遇到过类似的情况:在不破坏已有程序的前提下,C99
委员会选择了一个新的关键字_Bool
,但是在<stdbool.h>(21.5节)
中以宏的方式提供了一个更好的名字bool
。包含<complex.h>
的程序可以用complex
来代替_Complex
,就像包含<stdbool.h>
的程序可以用bool
来代替_Bool
一样。
I
宏在C99
中扮演着重要的角色。没有专门的语言特性可以用于从实部和虚部创建复数,因此可以把虚部乘以I
再和实部相加:
double complex dc = 2.0 + 3.5 * I;
//变量dc的值为2+3.5i。
注意,
_Complex_I
和I
都表示虚数单位i
。大多数程序员可能会使用I
而不是_Complex_I
。不过,如果已有的代码已经把I
用于其他目的,则可以使用备选的_Complex_I
。如果I
的名字引发了冲突,可以删除其定义:
#include <complex.h>
#undef I
接下来程序员可以为i
定义一个新的名字(不过仍然很短),比如J
:
#define J _Complex_I
需要注意的是,_Complex_I
的类型(即I
的类型)是float _Complex
而不是double _Complex
。用于表达式时,I
可以根据需要自动扩展为double _Complex
或者long double _Complex
类型。
27.4.2 CX_LIMITED_RANGE编译提示
<complex.h>
提供了一个名为CX_LIMITED_RANGE
的编译提示,允许编译器使用如下标准公式进行乘、除和绝对值运算:
( a + b i ) × ( c + d i ) = ( a c − b d ) + ( b c + a d ) i {(a+bi)×(c+di)=(ac-bd)+(bc+ad)i} (a+bi)×(c+di)=(ac−bd)+(bc+ad)i
( a + b i ) / ( c + d i ) = [ ( a c + b d ) + ( b c − a d ) i ] / ( c 2 + d 2 ) {(a+bi)/(c+di)=[(ac+bd)+(bc-ad)i]/(c^2+d^2)} (a+bi)/(c+di)=[(ac+bd)+(bc−ad)i]/(c2+d2)
∣ a + b i ∣ = a 2 + b 2 {|a+bi|=\sqrt{a^2+b^2}} ∣a+bi∣=a2+b2
使用这些公式有时会因为上溢出或下溢出而导致反常的结果;此外,这些公式不能非常好地处理无穷数。由于以上问题的存在,C99
仅在程序员允许时才会使用这些公式。
CX_LIMITED_RANGE
编译提示的形式如下:
#pragma STDC CX_LIMITED_RANGE 开关
其中开关
可以是ON
、OFF
或者DEFAULT
。如果值为ON
,该编译提示允许编译器使用上面列出的公式;如果值为OFF
,编译器会以一种更加安全的方式进行计算,但速度也可能要慢一些;DEFAULT
是默认设置,效果等同于OFF
。
CX_LIMITED_RANGE
编译提示的有效期限与它在程序中出现的位置有关。如果它出现在源文件的最顶层,也就是说在任何外部声明之外,那么它将持续有效,直到遇到下一个CX_LIMITED_RANGE
编译提示或者到达文件结尾。除此之外,CX_LIMITED_RANGE
编译提示只可能出现在复合语句(可能是函数体)的开始处;这种情况下,该编译提示将持续有效直到遇到下一个CX_LIMITED_RANGE
编译提示(甚至可能出现在内嵌的复合语句中)或者到达复合语句的结尾。在复合语句的结尾处,开关的状态会恢复为进入复合语句之前的值。
27.4.3 <complex.h>中的函数
<complex.h>
所提供的函数与C99
版本的<math.h>
所提供的函数类似。与<math.h>
中的函数一样,<complex.h>
中的函数也可以分成几组:三角函数
、双曲函数
、指数
和对数函数
以及幂和绝对值函数
。复数所独有的一组函数是操作函数,将在本节的最后加以讨论。
<complex.h>
中的每一个函数都有3
种版本:float complex
版本、double complex
版本和long double complex
版本。float complex
版本的名字以f
结尾,long double complex
版本的名字以l
结尾。
在讨论<complex.h>
中的函数之前,需要说明几点。首先,与<math.h>
中的函数一样,<complex.h>
中的函数以弧度而不是角度对角进行度量。其次,当发生错误时,<complex.h>
中的函数可能会在errno变量(24.2节)
中存储值,但不强制要求这么做。
最后还要提一点:描述有多个可能的返回值的函数时,经常会提到术语
分支切割(branch cut)
。在复数领域,选择返回值会导致一种分支切割:复平面中的一条曲线(通常是直线),函数在其周围是不连续的。分支切割通常不是唯一的,但一般按习惯确定。分支切割的精确定义涉及复分析的知识,超出了本书的范围,因此这里只介绍一下C99
标准的相关约束条件,不做进一步的解释。
27.4.4 三角函数
double complex cacos(double complex z);
float complex cacosf(float complex z);
long double complex cacosl(long double complex z);
double complex casin(double complex z);
float complex casinf(float complex z);
long double complex casinl(long double complex z);
double complex catan(double complex z);
float complex catanf(float complex z);
long double complex catanl(long double complex z);
double complex ccos(double complex z);
float complex ccosf(float complex z);
long double complex ccosl(long double complex z);
double complex csin(double complex z);
float complex csinf(float complex z);
long double complex csinl(long double complex z);
double complex ctan(double complex z);
float complex ctanf(float complex z);
long double complex ctanl(long double complex z);
cacos
函数计算复数的反余弦,分支切割在实轴区间[-1,+1]
之外进行。返回值位于一个条状区域中,该条状区域在虚轴方向可以无限延伸,在实轴方向上位于区间[0,π]
。casin
函数计算复数的反正弦,分支切割在实轴区间[-1,+1]
之外进行。返回值位于一个条状区域中,该条状区域在虚轴方向可以无限延伸,在实轴方向上位于区间[-π/2,+π/2]
。catan
函数计算复数的反正切,分支切割在虚轴区间[-i,+i]
之外进行。返回值位于一个条状区域中,该条状区域在虚轴方向可以无限延伸,在实轴方向上位于区间[-π/2,+π/2]
。ccos
函数计算复数的余弦,csin
函数计算复数的正弦,ctan
函数计算复数的正切。
27.4.5 双曲函数
double complex cacosh(double complex z);
float complex cacoshf(float complex z);
long double complex cacoshl(long double complex z);
double complex casinh(double complex z);
float complex casinhf(float complex z);
long double complex casinhl(long double complex z);
double complex catanh(double complex z);
float complex catanhf(float complex z);
long double complex catanhl(long double complex z);
double complex ccosh(double complex z);
float complex ccoshf(float complex z);
long double complex ccoshl(long double complex z);
double complex csinh(double complex z);
float complex csinhf(float complex z);
long double complex csinhl(long double complex z);
double complex ctanh(double complex z);
float complex ctanhf(float complex z);
long double complex ctanhl(long double complex z)
cacosh
函数计算复数的反双曲余弦,分支切割在实轴上小于1
的值上进行。返回值位于一个半条状区域中,该区域在实轴方向取非负值,在虚轴方向上位于区间[-iπ, +iπ]
。casinh
函数计算复数的反双曲正弦,分支切割在虚轴区间[-i, +i]
之外进行。返回值位于一个条状区域中,该条状区域在实轴方向可以无限延伸,在虚轴方向上位于区间[-iπ/2, +iπ/2]
。catanh
函数计算复数的反双曲正切,分支切割在实轴区间[-1, +1]
之外进行。返回值位于一个条状区域中,该条状区域在实轴方向可以无限延伸,在虚轴方向上位于区间[-iπ/2, +iπ/2]
。ccosh
函数计算复数的双曲余弦,csinh
函数计算复数的双曲正弦,ctanh
函数计算复数的双曲正切。
27.4.6 指数函数和对数函数
double complex cexp(double complex z);
float complex cexpf(float complex z);
long double complex cexpl(long double complex z);
double complex clog(double complex z);
float complex clogf(float complex z);
long double complex clogl(long double complex z);
cexp
函数计算复数基于e
的指数值。clog
函数计算复数的自然对数(以e
为底数)值,分支切割在负的实轴方向上进行。返回值位于一个条状区域中,该条状区域在实轴方向可以无限延伸,在虚轴方向上位于区间[-iπ, +iπ]
。
27.4.7 幂函数和绝对值函数
double cabs(double complex z);
float cabsf(float complex z);
long double cabsl(long double complex z);
double complex cpow(double complex x, double complex y);
float complex cpowf(float complex x, float complex y);
long double complex cpowl(long double complex x, long double complex y);
double complex csqrt(double complex z);
float complex csqrtf(float complex z);
long double complex csqrtl(long double complex z);
cabs
函数计算复数的绝对值。cpow
函数返回x
的y
次幂,分支切割在负的实轴方向上对第一个参数进行。csqrt
函数计算复数的平方根,分支切割在负的实轴方向上进行。返回值位于右边的半平面(包括虚轴)。
27.4.8 操作函数
double carg(double complex z);
float cargf(float complex z);
long double cargl(long double complex z);
double cimag(double complex z);
float cimagf(float complex z);
long double cimagl(long double complex z);
double complex conj(double complex z);
float complex conjf(float complex z);
long double complex conjl(long double complex z);
double complex cproj(double complex z);
float complex cprojf(float complex z);
long double complex cprojl(long double complex z);
double creal(double complex z);
float crealf(float complex z);
long double creall(long double complex z);
carg
函数返回z
的辐角(相角),分支切割在负的实轴方向上进行。返回值位于区间[-π, +π]
。cimag
函数返回z
的虚部。conj
函数返回z
的复共轭。cproj
函数计算z
在黎曼球面上的投影。返回值一般等于z
;但是当实部和虚部中存在无穷数时,返回值为INFINITY + I * copysign(0.0, cimag(z))
。creal
函数返回z
的实部。
求二次方程的根:二次方程 a x 2 + b x + c = 0 {ax^2+bx+c=0} ax2+bx+c=0的根由下面的
二次公式(quadratic formula)
给出:
x = − b ± b 2 − 4 a c 2 a {x=\frac{-b±\sqrt{b^2-4ac}}{2a}} x=2a−b±b2−4ac
一般来说,x
的值是复数,因为当
b
2
−
4
a
c
{b^2-4ac}
b2−4ac(称为判别式)小于0
时其平方根为虚数。
例如,假设a=5
,b=2
,c=1
,于是得到二次方程
5 x 2 + 2 x + 1 = 0 {5x^2+2x+1=0} 5x2+2x+1=0
判别式的值为4-20 = -16
,所以这个方程的根是复数。下面的程序使用了<complex.h>
中的一些函数来计算并显示该方程的根。
/*
quadratic.c
--Finds the roots of the equation 5x**2 + 2x + 1 = 0
*/
#include <complex.h>
#include <stdio.h>
int main(void)
{
double a = 5, b = 2, c = 1;
double complex discriminant_sqrt = csqrt(b * b - 4 * a * c);
double complex root1 = (-b + discriminant_sqrt) / (2 * a);
double complex root2 = (-b - discriminant_sqrt) / (2 * a);
printf("root1 = %g + %gi\n", creal(root1), cimag(root1));
printf("root2 = %g + %gi\n", creal(root2), cimag(root2));
return 0;
}
/*输出如下:
root1 = -0.2 + 0.4i
root2 = -0.2 + -0.4i
*/
程序quadratic.c
说明了如何显示复数:提取实部和虚部,把它们分别当作浮点数输出。printf
没有用于复数的转换指定符,因此没有更简单的方法。读取复数也没有捷径可走,程序需要分别获取实部和虚部,然后将它们合并为一个复数。
27.5 <tgmath.h>: 泛型数学(C99)
<tgmath.h>
提供了带参数的宏,宏的名字与<math.h>
和<complex.h>
中的函数名相匹配。这些泛型宏(type-generic macro)
可以检测参数的类型,然后调用<math.h>
或<complex.h>
中相应的函数。
从23.3节
、23.4节
和27.4节
可以看出,C99
中的许多数学函数有多个版本。例如,sqrt
函数不仅有3
种复数版本(csqrt
、csqrtf
和csqrtl
),还有double(sqrt)
、float(sqrtf)
以及long double版本(sqrtl)
。使用<tgmath.h>
之后,程序员可以直接使用sqrt
,而不用担心需要的到底是哪个版本:根据x
类型的不同,函数调用sqrt(x)
有可能是6
个版本的sqrt
中的任何一个。
使用<tgmath.h>
的好处之一是数学函数的调用更容易书写(也更易读懂)。更重要的是,将来参数类型改变时,不需要修改泛型宏的调用。
顺便提一下,
<tgmath.h>
包含了<math.h>
和<complex.h>
。因此只要在程序中包含了<tgmath.h>
,就可以访问<math.h>
和<complex.h>
中的函数。
27.5.1 泛型宏
根据泛型宏是对应于
<math.h>
中的函数、<complex.h>
中的函数,还是对应于同时存在于<math.h>
和<complex.h>
中的函数,可以把<tgmath.h>
中定义的泛型宏分为3
组。
表27-5
列出了与同时存在于<math.h>
和<complex.h>
中的函数相对应的泛型宏。注意,每个泛型宏的名字与<math.h>
中“不带后缀”的函数的名字(例如acos
,而不是acosf
或acosl
)相对应。
表27-5 <tgmath.h>
中的泛型宏(第一组)
<math.h>中的函数 | <complex.h>中的函数 | 泛型宏 |
---|---|---|
acos | cacos | acos |
asin | casin | asin |
atan | catan | atan |
acosh | cacosh | acosh |
asinh | casinh | asinh |
atanh | catanh | atanh |
cos | ccos | cos |
sin | csin | sin |
tan | ctan | tan |
cosh | ccosh | cosh |
sinh | csinh | sinh |
tanh | ctanh | tanh |
exp | cexp | exp |
log | clog | log |
pow | cpow | pow |
sqrt | csqrt | sqrt |
fabs | cabs | fabs |
第二组
宏仅对应于<math.h>
中的函数。每个宏的名字与<math.h>
中不带后缀的函数的名字一样。用复数作为这些宏的参数会导致未定义的行为。
- atan2
- fma
- llround
- remainder
- cbrt
- fmax
- log10
- remquo
- ceil
- fmin
- log1p
- rint
- copysign
- fmod
- log2
- round
- erf
- frexp
- logb
- scalbn
- erfc
- hypot
- lrint
- scalbln
- exp2
- ilogb
- lround
- tgamma
- expm1
- ldexp
- nearbyint
- trunc
- fdim
- lgamma
- nextafter
- floor
- llrint
- nexttoward
最后一组
宏仅对应于<complex.h>
中的函数:
- carg
- conj
- creal
- cimag
- cproj
除modf
函数外,上面3
组覆盖了<math.h>
和<complex.h>
中所有有多个版本的函数。
27.5.2 调用泛型宏
为了解泛型宏的调用过程,首先需要了解
泛型参数(generic parameter)
的概念。考虑nextafter
函数(来自<math.h>
)的3
个版本的原型:
double nextafter(double x, double y);
float nextafterf(float x, float y);
long double nextafterl(long double x, long double y);
x
和y
的类型根据nextafter
函数的版本变化,所以这两个参数都是泛型参数。现在再来看看nexttoward
函数3
个版本的原型:
double nexttoward(double x, long double y);
float nexttowardf(float x, long double y);
long double nexttowardl(long double x, long double y);
第一个参数是泛型参数,但第二个参数不是(其类型总是long double
)。在不带后缀的函数版本中,泛型参数的类型总是double
(或者double complex
)。
调用泛型宏时,首先需要确定应该用
<math.h>
中的函数还是<complex.h>
中的函数来替换它。(对于第2组
和第3组
中的宏,不需要这一步,因为第2组
中的宏总会被替换为<math.h>
中的函数,而第3组中的宏总会被替换为<complex.h>
中的函数。)判断的规则很简单:如果泛型参数对应的参数是复数,那么选择<complex.h>
中的函数,否则选择<math.h>
中的函数。
接下来需要分析应调用<math.h>
中的函数或<complex.h>
中的函数的哪个版本。假定需要调用的函数在<math.h>
中(对于<complex.h>
中的函数,规则是类似的),那么依次使用下面的规则:
- 如果与泛型参数对应的实参为
long double
型,那么调用函数的long double
版本。 - 如果与泛型参数对应的实参为
double
型或整数类型,那么调用函数的double
版本。 - 其他情况下调用函数的
float
版本。
第(2)
条规则有一些特别,它说整数类型的实参会导致调用函数的double
版本,而不是我们预料中的float
版本。
举个例子,假设声明了如下变量:
int i;
float f;
double d;
long double ld;
float complex fc;
double complex dc;
long double complex ldc;
对于表27-8
左列的每个宏调用,相应的函数调用在右列给出。
表27-8 宏调用所对应的等价函数调用
宏调用 | 等价的函数调用 |
---|---|
sqrt(i) | sqrt(i) |
sqrt(f) | sqrtf(f) |
sqrt(d) | sqrt(d) |
sqrt(ld) | sqrtl(ld) |
sqrt(fc) | csqrtf(fc) |
sqrt(dc) | csqrt(dc) |
sqrt(ldc) | csqrtl(ldc) |
注意
!!宏调用sqrt(i)
会调用sqrt
函数的double
版本,而不是float
版本。
这些规则同样适用于带有多个参数的宏。例如,宏调用
pow(ld, f)
将被替换为powl(ld, f)
。pow
的两个参数都是泛型参数。由于有一个参数是long double
型,根据规则1
,将调用pow
函数的long double
版本。
27.6 <fenv.h>: 浮点环境(C99)
IEEE 754
标准在表示浮点数时使用最广泛。(C99
标准把IEEE 754
称为IEC 60559
。)<fenv.h>
的目的是使程序可以访问IEEE
标准指定的浮点状态标志
和控制模式
。虽然对<fenv.h>
的设计具有一般性,也考虑到了用于其他浮点表示法的情况,但创建<fenv.h>
的目的是支持IEEE
标准。
27.6.1 浮点状态标志和控制模式
7.2节
讨论了IEEE 754
标准的一些基本性质,23.4节
给出了进一步的细节,讨论了C99
在<math.h>
中新增的内容。其中一些讨论是与<fenv.h>
直接相关的,尤其是有关异常和舍入方向的讨论。在继续介绍之前,首先回顾一下23.4节
的一些内容并定义几个新的术语。
浮点状态标志
是一个系统变量,在发生浮点异常时设置。在IEEE
标准中,有5
种类型的浮点异常:上溢出
、下溢出
、除零
、无效运算
(算术运算的结果是NaN
)和不精确
(需要对算术运算的结果舍入)。每种异常都有一种相对应的状态标志。
<fenv.h>
声明了一种名为fexcept_t
的类型,用于浮点状态标志。fexcept_t
型的对象表示这些标志的整体值。可以简单地把fexcept_t
设成整数类型,其中每个位表示一个标志,不过C99
标准没有做这样的要求。因此其他方案也存在,比如可以把fexcept_t
设成结构类型,其中每个成员表示一种异常。成员中还可以存储有关异常的其他信息,比如导致该异常的浮点指令的地址。
浮点控制模式
是一个系统变量,程序可以通过设置该变量来改变浮点运算的未来行为。当不能用浮点表示方法精确地表示一个数时,IEEE
标准要求用“定向舍入”模式来控制其舍入方向。舍入方向有4
种:(1)
向最近的数舍入,向最接近的可表示的值舍入,如果一个数正好在两个数值的中间,就向“偶”值(最低有效位为0
)舍入;(2)
趋零截尾;(3)
向正无穷方向舍入;(4)
向负无穷方向舍入。默认的舍入方向是向最近的数舍入
。IEEE
标准的有些实现还提供了另外两种控制模式:一种
是用于控制舍入精度的模式,另一种
是“陷阱”模式,它用于在发生异常时判断浮点处理器是否掉入陷阱(或停止)。
术语浮点环境(floating-point environment)
是指特定实现所支持的浮点状态标志和控制模式的结合。fenv_t
类型的值表示整个浮点环境。fenv_t
类型与fexcept_t
类型一样,都声明在<fenv.h>
中。
27.6.2 <fenv.h>宏
表27-9
列出了<fenv.h>
中可能会定义的宏,但这些宏中只有两个宏(FE_ALL_EXCEPT
和FE_DEL_ENV
)是必须有的。实现中也可以定义表中没有列出的宏,宏的名字必须以FE_
后跟一个大写字母开头。
表27-9 <fenv.h>
中的宏
名称 | 值 | 说明 |
---|---|---|
FE_DIVBYZERO、FE_INEXACT、FE_INVALID、FE_OVERFLOW、FE_UNDERFLOW | 整型常量表达式,位不重叠 | 仅当实现支持相应的浮点异常时才定义。实现可以定义其他表示浮点异常的宏 |
FE_ALL_EXCEPT | 见说明 | 实现所定义的所有浮点异常宏的按位或。如果没有定义这样的宏,则值为0 |
FE_DOWNWARD、FE_TONEAREST、FE_TOWARDZERO、FE_UPWARD | 整型常量表达式,值是非负离散的 | 仅当相应的浮点异常可以通过fegetround和fesetround函数来获得和设置时才定义。实现可以定义其他表示舍入方向的宏 |
FE_DFL_ENV | const fenv_t *类型的值 | 表示(程序启动时的)默认浮点环境。实现可以定义其他表示浮点环境的宏 |
27.6.3 FENV_ACCESS编译提示
<fenv.h>
提供了一个名为FENV_ACCESS
的编译提示,用于通知编译器:程序想使用该头提供的函数。知道程序中的哪些部分会使用<fenv.h>
对编译器来说很重要,因为如果控制模式不是按习惯设置的,或者在程序执行过程中控制模式可能改变,那么有些常见的优化方法将不能使用。
FENV_ACCESS
编译提示的形式如下:
#pragma STDC FENV_ACCESS 开关
其中开关
可以是ON
、OFF
或DEFAULT
。如果值为ON
,该编译提示告诉编译器程序可能会测试浮点状态标志或者修改浮点控制模式;如果值为OFF
,那么不会对标志进行测试,且使用默认的控制模式;DEFAULT
的含义由实现定义,它可能表示ON
也可能表示OFF
。
FENV_ACCESS
编译提示的有效期限与它在程序中出现的位置有关。如果它出现在源文件的最顶层,也就是说在任何外部声明之外,那么它将持续有效直到遇到下一个FENV_ACCESS
编译提示或者到达文件结尾。除此之外,FENV_ACCESS
编译提示只可能出现在复合语句(可能是函数体)的开始处;这种情况下,该编译提示将持续有效,直到遇到下一个FENV_ACCESS
编译提示(甚至可能出现在内嵌的复合语句中)或者到达复合语句的结尾。在复合语句的结尾处,开关的状态会恢复为进入复合语句之前的值。
程序员应使用FENV_ACCESS
编译提示来指明程序的哪些部分需要对浮点硬件进行底层访问。在编译提示的开关值为OFF
的程序区域,测试浮点状态标志或者以非默认的控制模式运行都会导致未定义的行为。
通常把指定开关值为
ON
的FENV_ACCESS
编译提示置于函数体的开始位置:
void f(double x, double y)
{
#pragma STDC FENV_ACCESS ON
...
}
函数f
可以根据需要测试浮点状态标志或改变控制模式。在f
函数体的末尾,编译提示的开关将恢复以前的状态。
程序执行过程中,从FENV_ACCESS
编译提示的开关值为OFF
的区域进入开关值为ON
的区域时,浮点状态标志没有指定的值,控制模式采用默认设置。
27.6.4 浮点异常函数
int feclearexcept(int excepts);
int fegetexceptflag(fexcept_t *flagp, int excepts);
int feraiseexcept(int excepts);
int fesetexceptflag(const fexcept_t *flagp, int excepts);
int fetestexcept(int excepts);
<fenv.h>
中的函数分为3
组。第一组函数用于处理浮点状态标志
。这5
个函数都有一个名为excepts
的int
型形式参数,它是一个或多个浮点异常宏(表27-9
列出的第一组宏)的按位或。例如,传递给这些函数的参数可能是FE_INVALID|FE_OVERFLOW|FE_UNDERFLOW
,表示3
种状态标志的组合;这些参数也可能是0
,表示没有选择任何标志。
feclearexcept
函数试图清除excepts
所表示的浮点异常。如果excepts
为0
或者所有指定的异常都成功清除,feclearexcept
函数返回0
;否则返回非零值。fegetexceptflag
函数试图获取excepts
所表示的浮点状态标志。该数据存储在flagp
指向的fexcept_t
型对象中。如果状态标志成功存储,fegetexceptflag
函数返回0
;否则返回非零值。feraiseexcept
函数试图产生excepts
所表示的浮点异常。产生上溢出或下溢出异常时,feraiseexcept
是否还会同时产生不精确浮点异常由实现定义。(符合IEEE
标准的实现会这样做。)如果excepts
为0
或者所有指定的异常都成功产生,feraiseexcept
函数返回0
;否则返回非零值。fesetexceptflag
函数试图设置excepts
所表示的浮点状态标志。这些数据存储在flagp
指向的fexcept_t
型对象中,且该对象必须已经由前面的fegetexceptflag
函数调用设置过了。此外,前面的fegetexceptflag
函数调用的第二个参数必须包含了excepts
所表示的所有浮点异常。如果excepts
为0
或者所有指定的异常都成功设置,fesetexceptflag
函数返回0
;否则返回非零值。fetestexcept
函数只测试excepts
所表示的浮点状态标志,它返回与当前设置的标志相对应的浮点异常宏的按位或。例如,如果excepts
的值是FE_INVALID|FE_OVERFLOW|FE_UNDERFLOW
,fetestexcept
函数可能会返回FE_INVALID|FE_UNDERFLOW
;这表明在FE_INVALID
、FE_OVERFLOW
和FE_UNDERFLOW
所表示的异常中,只有FE_INVALID
和FE_UNDERFLOW
的标志是当前设置的。
27.6.5 舍入函数
int fegetround(void);
int fesetround(int round);
fegetround
函数和fesetround
函数用于确定和修改舍入方向。这两个函数都依赖于舍入方向宏(见表27-9
中的第三组)。
fegetround
函数返回与当前舍入方向相匹配的舍入方向宏的值。如果不能确定当前舍入方向或者当前舍入方向不能和任何舍入方向宏相匹配,fegetround
函数返回负数。
以舍入方向宏的值作为参数时,
fesetround
函数会试图确立相应的舍入方向。如果调用成功,fesetround
函数返回0
;否则返回非零值。
27.6.6 环境函数
int fegetenv(fenv_t *envp);
int feholdexcept(fenv_t *envp);
int fesetenv(const fenv_t *envp);
int feupdateenv(const fenv_t *envp);
<fenv.h>
中的最后4
个函数是针对整个浮点环境的,而不仅仅针对状态标志或控制模式。如果成功完成了所需进行的操作,每个函数都会返回0
;否则返回非零值。
-
fegetenv
函数试图从处理器获取当前的浮点环境,并将其存储在envp
指向的对象中。 -
feholdexcept
函数需完成3
个操作:(1)
把当前浮点环境存入envp
指向的对象中;(2)
消除浮点状态标志;(3)
尝试为所有的浮点异常安装不阻塞模式(从而以后发生的异常不会导致陷阱或停止)。 -
fesetenv
函数试图建立envp
所表示的浮点环境。其中envp
既可以指向由之前的fegetenv
或feholdexcept
函数调用所存储的浮点环境,也可以等于FE_DFL_ENV
之类的浮点环境宏。与feupdateenv
函数不同,fesetenv
函数不会产生任何异常。如果用fegetenv
函数调用来保存当前的浮点环境,那么以后可以调用fesetenv
函数来恢复之前的浮点环境。 -
feupdateenv
函数试图完成3
个操作:(1)
保存当前产生的浮点异常;(2)
安装envp
指向的浮点环境;(3)
产生所保存的异常。envp
既可以指向由之前的fegetenv
或feholdexcept
函数调用所存储的浮点环境,也可以等于FE_DFL_ENV
之类的浮点环境宏。
问与答
问1:既然
<inttypes.h>
包含了<stdint.h>
,为什么还需要<stdint.h>
呢?
答:主要是为了让独立式实现(14.3节)
中的程序可以包含<stdint.h>
。(C99
要求托管式实现和独立式实现都提供<stdint.h>
,但只要求托管式实现提供<inttypes.h>
。)即便在托管式环境中,包含<stdint.h>
而不是<inttypes.h>
可能也是有益的,因为这样可以避免对属于后者的所有宏都进行定义。
问2:
<math.h>
中的modf
函数有3
个版本,为什么没有名为modf
的泛型宏呢?
答:我们来看看modf
函数的3
个版本的原型:
double modf(double value, double *iptr);
float modff(float value, float *iptr);
long double modfl(long double value, long double *iptr);
modf
的与众不同之处在于,它有一个指针类型的参数,而且指针的类型在函数的3
个版本之间还不一样。(frexp
和remquo
也有指针参数,但类型总是int*
。)如果为modf
给出一个泛型宏,会引起一些难题。例如,modf(d, &f)
(其中d
的类型为double
,f
的类型为float
)的含义不清楚:我们应该调用modf
函数还是应该调用modff
函数?C99
委员会认为,与其为某一个函数(可能还考虑到modf
不是很常用的函数)定义一组复杂的规则,还不如不为它提供泛型宏。
问3:当使用整数参数调用
<tgmath.h>
中的宏时,会调用相应函数的double
版本。根据常规算术转换(7.4节)
,应该调用float
版本吧?
答:我们处理的是宏,而不是函数,所以常规算术转换不适用。C99
标准委员会需要创建一条规则,以确定当传递给<tgmath.h>
中的宏的参数为整数时,应该调用函数的哪个版本。委员会曾经考虑过调用float
版本(与常规算术转换一致),但最终还是认为调用double
版本更合适。首先,这样更安全:把整数转换为float
型可能会导致精度的丢失,当整数类型的宽度为32
位或更大时尤其如此。其次,这样做给程序员带来的惊讶程度要小一些。假定i
是一个整数变量,如果不包含<tgmath.h>
,那么调用sin(i)
会调用sin
函数;如果包含了<tgmath.h>
,那么调用sin(i)
会调sin
宏,预处理器会把sin
宏替换为sin
函数,从而使最终的结果与上一种情况一致。
问4:当程序调用
<tgmath.h>
中的泛型宏时,实现如何确定应调用哪个函数呢?宏有没有办法测试参数的类型?
答:<tgmath.h>
与众不同的一个方面在于,其中的宏需要能够测试传递给它们的参数的类型。C
语言不具备测试类型的特性,所以通常无法写出这样的宏。<tgmath.h>
中的宏需要依靠特定编译器所提供的特殊工具来进行这样的测试。我们不清楚这些工具是什么,而且这些工具也不一定能够从一个编译器移植到另一个编译器。
写在最后
本文是博主阅读《C语言程序设计:现代方法(第2版·修订版)》时所作笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对各位有所帮助,Thank you very much!