C现代方法(第27章)笔记——C99对数学计算的新增支持

第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>的动机即源于这一认识。例如,如果iint型的变量,那么赋值语句

i = 100000;

int32位的类型时是没问题的,但如果int16位的类型就会出错。问题在于C标准没有精确地说明int值有多少位。标准可以保证int型的值一定包括-32767~32767范围内的所有整数(要求至少16位),但没有进一步的规定。示例中的变量i需要存储100000,传统的解决方案是把i的类型声明为某种由typedef创建的类型T,然后在特定的实现中根据整数的大小调整T的声明。(T16位的机器上应该是long int类型,但在32位的机器上可以是int类型。)这是7.5节中提到的策略。

如果编译器支持C99,还有一种更好的方法。<stdint.h>基于类型的宽度(存储该类型的值所需的位数,包括可能出现的符号位)声明类型的名字<stdint.h>中声明的typedef名字可以涉及基本类型(如intunsigned intlong int),也可以涉及特定实现所支持的扩展整数类型。


27.1.1 <stdint.h>类型

<stdint.h>中声明的类型可分为以下5组:

  • 精确宽度整数类型。每个形如intN_t的名字表示一种N位的有符号整数类型,存储为2的补码形式。(2的补码是一种用二进制表示有符号整数的方法,在现代计算机中非常普遍。)例如,int16_t型的值可以是16位的有符号整数。形如uintN_t的名字表示一种N位的无符号整数类型。如果某个具体的实现支持宽度N等于8163264的整数,它需要同时提供intN_tuintN_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_tuint24_t的类型。


27.1.2 对指定宽度整数类型的限制

<stdint.h>为其中的每一个有符号整数类型定义了两个宏,用于指明该类型的最小值和最大值,并为其中的每一个无符号整数类型定义了一个宏,用于指明该类型的最大值。表27-1中的前三行给出了精确宽度整数类型对应的宏的值,其他的行给出了C99<stdint.h>中其他类型的最小值和最大值的约束。(这些宏的精确值由实现定义。)表中所有的宏都是常量表达式。

表27-1 <stdint.h>对指定宽度整数类型进行限制的宏

名称 含义
INTN_MIN -( 2 N − 1 {2^{N-1}} 2N1) 最小的intN_t值
INTN_MAX 2 N − 1 {2^{N-1}} 2N1-1 最大的intN_t值
UINTN_MAX 2 N {2^N} 2N-1 最大的uintN_t值
INT_LEASTN_MIN ≤-( 2 N − 1 {2^{N-1}} 2N1-1) 最小的int_leastN_t值
INT_LEASTN_MAX 2 N − 1 {2^{N-1}} 2N1-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}} 2N1-1) 最小的int_fastN_t值
INT_FASTN_MAX 2 N − 1 {2^{N-1}} 2N1-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_tsize_twchar_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。这些宏对于变量初始化非常有用(当然,还有别的作用)。例如,如果iint_least32_t型的变量,这样的写法

i = 100000;

会有问题,因为常量100000可能会因为太大而不能用int型表示(如果int16位的类型)。但是如果写成

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_tlong int型的别名,那么正确的转换说明应为%ld而不是%d。为了按可移植的方式使用...printf...scanf函数,我们需要使所书写的转换说明能对应于<stdint.h>中声明的每一种类型。这就是<inttypes.h>的由来。对于<stdint.h>中的每一种类型,<inttypes.h>都提供了一个宏,该宏可以扩展为一个包含该类型对应的转换指定符的字面串。

每个宏名由以下三个部分组成:

  • 名字以PRISCN开始,具体以哪个开始取决于宏是用于...printf函数调用还是用于...scanf函数调用。
  • 接下来是一个单字母的转换指定符(有符号类型用di,无符号类型用ouxX)。
  • 名字的最后一个部分用于指明该宏对应于<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_tuintmax_t可能是128位宽。

imaxabsimaxdiv函数是<stdlib.h>(26.2节)中声明的整数算术运算函数的最大宽度版本。imaxabs函数返回参数的绝对值。参数和返回值的类型都是intmax_timaxdiv函数用第一个参数除以第二个参数,返回imaxdiv_t型的值。imaxdiv_t是一个包含商(quot)成员和余数(rem)成员的结构,这两个成员的类型都是intmax_t


strtoimaxstrtoumax函数是<stdlib.h>中的数值转换函数的最大宽度版本。strtoimax函数与strtolstrtoll类似,但返回值的类型是intmax_tstrtoumax函数与strtoulstrtoull类似,但返回值的类型是uintmax_t。如果没有执行转换,strtoimaxstrtoumax都返回零。如果转换产生的值超出函数返回类型的表示范围,两个函数都将ERANGE存于errno中。另外,strtoimax返回最小或最大的intmax_t型值(INTMAX_MININTMAX_MAX),strtoumax返回最大的uintmax_t型值(UINTMAX_MAX)。


wcstoimaxwcstoumax函数是<wchar.h>中的宽字符串数值转换函数的最大宽度版本。wcstoimax函数与wcstolwcstoll类似,但返回值的类型是intmax_twcstoumax函数与wcstoulwcstoull类似,但返回值的类型是uintmax_t。如果没有执行转换,wcstoimaxwcstoumax都返回零。如果转换产生的值超出函数返回类型的表示范围,两个函数都将ERANGE存于errno中。另外,wcstoimax返回最小或最大的intmax_t型值(INTMAX_MININTMAX_MAX),strtoumax返回最大的uintmax_t型值(UINTMAX_MAX)。另外,wcstoimax返回最小或最大的intmax_t型值(INTMAX_MININTMAX_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,其中ab是实数。我们称a为该数的实部b虚部。注意!实数是复数的特例(b=0的情况)。

复数有什么用呢?首先,它可以解决之前不能解决的问题。考虑方程 x 2 + 1 = 0 {x^2+1=0} x2+1=0,如果限定x为实数则无解,如果允许复数,这个方程有两个解:x=ix=-i

可以把复数想象为二维空间中的点,该二维空间称为复平面(complex plane)。每个复数(复平面中的点)用笛卡儿坐标表示,其中复数的实部对应于点的x轴坐标,虚部对应于y轴坐标。例如,复数 2 + 2.5 i {2+2.5i} 2+2.5i 1 − 3 i {1-3i} 13i − 3 − 2 i {-3-2i} 32i − 3.5 + 1.5 i {-3.5+1.5i} 3.5+1.5i可以作图为

在这里插入图片描述

另一种称为极坐标(polarcoordinates)的系统也可以用于描述复平面中的点。在极坐标系中,复数zrθ表示,其中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} (32i)+(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} (32i)(1.5+3i)=(31.5)+(23)i=1.55i

两个复数相乘,需要把第一个复数的每一项乘以第二个复数的每一项,然后把乘积相加:

( 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} (32i)×(1.5+3i)=(3×1.5)+(3×3i)+(2i×1.5)+(2i×3i)=4.5+9i3i6i2=10.5+6i

注意,这里用恒等式 i 2 = − 1 {i^2=-1} i2=1来简化计算结果。

复数的除法相对难一些。首先需要了解一下复共轭的概念,一个数的复共轭通过变换其虚部的符号得到。例如, 7 − 4 i {7-4i} 74i 7 + 4 i {7+4i} 7+4i的共轭, 7 + 4 i {7+4i} 7+4i也是 7 − 4 i {7-4i} 74i的共轭。我们用 z ∗ {z^*} z来表示复数z的共轭。

复数yz的商由下面的公式给出:

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} 32i的计算过程:

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} 32i10.5+6i=(32i)(3+2i)(10.5+6i)(3+2i)=1319.5+39i=1.5+3i


27.3.3 C99中的复数类型

C99内建了许多对复数的支持。我们不需要包含任何头就可以声明表示复数的变量,然后对这些变量进行算术和其他运算。

C99提供了3种复数类型(7.2节曾提到过):float _Complexdouble _Complexlong 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)。对于实浮点类型(floatdoublelong double)来说,对应实数类型与原始类型一样。对于复数类型而言,对应实数类型是原始类型去掉_Complex。(例如,float _Complex的对应实数类型为float。)

现在可以讨论有关复数类型的转换规则了。这些规则分为3类。

  • 复数转换为复数。第一条规则考虑从一种复数类型到另一种复数类型的转换,例如把float _Complex转换为double _Complex。在这种情况下,实部和虚部分别使用对应实数类型的转换规则(见7.4节)进行转换。在这个例子中,float _Complex值的实部转换为double型,得到double _Complex值的实部,虚部用类似的方式转换为double型。
  • 实数转换为复数。把实数类型的值转换为复数类型时,使用实数类型之间的转换规则生成复数的实部,虚部设置为正的零或者无符号的零。
  • 复数转换为实数。把复数类型的值转换为实数类型时,丢弃虚部并使用实数类型之间的转换规则生成实部。

常规算术转换指的是一组特定的类型转换,它们可以自动作用于大多数二元运算符的操作数。当两个操作数中至少有一个为复数类型的情况下,执行常规算术转换还有一些特殊的规则:

  1. 如果任一操作数的对应实数类型为long double,那么对另一个操作数进行转换,使它的对应实数类型为long double
  2. 否则,如果任一操作数的对应实数类型为double型,那么对另一个操作数进行转换,使它的对应实数类型为double
  3. 否则,必然有一个操作数的对应实数类型为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_II都表示虚数单位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)=(acbd)+(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)+(bcad)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 开关

其中开关可以是ONOFF或者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函数返回xy次幂,分支切割在负的实轴方向上对第一个参数进行。
  • 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=2ab±b24ac

一般来说,x的值是复数,因为当 b 2 − 4 a c {b^2-4ac} b24ac(称为判别式)小于0时其平方根为虚数。

例如,假设a=5b=2c=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种复数版本(csqrtcsqrtfcsqrtl),还有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,而不是acosfacosl)相对应。

表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);

xy的类型根据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>中的函数,规则是类似的),那么依次使用下面的规则:

  1. 如果与泛型参数对应的实参为long double型,那么调用函数的long double版本。
  2. 如果与泛型参数对应的实参为double型或整数类型,那么调用函数的double版本。
  3. 其他情况下调用函数的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_EXCEPTFE_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 开关

其中开关可以是ONOFFDEFAULT。如果值为ON,该编译提示告诉编译器程序可能会测试浮点状态标志或者修改浮点控制模式;如果值为OFF,那么不会对标志进行测试,且使用默认的控制模式;DEFAULT的含义由实现定义,它可能表示ON也可能表示OFF

FENV_ACCESS编译提示的有效期限与它在程序中出现的位置有关。如果它出现在源文件的最顶层,也就是说在任何外部声明之外,那么它将持续有效直到遇到下一个FENV_ACCESS编译提示或者到达文件结尾。除此之外,FENV_ACCESS编译提示只可能出现在复合语句(可能是函数体)的开始处;这种情况下,该编译提示将持续有效,直到遇到下一个FENV_ACCESS编译提示(甚至可能出现在内嵌的复合语句中)或者到达复合语句的结尾。在复合语句的结尾处,开关的状态会恢复为进入复合语句之前的值。

程序员应使用FENV_ACCESS编译提示来指明程序的哪些部分需要对浮点硬件进行底层访问。在编译提示的开关值为OFF的程序区域,测试浮点状态标志或者以非默认的控制模式运行都会导致未定义的行为。

通常把指定开关值为ONFENV_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个函数都有一个名为exceptsint型形式参数,它是一个或多个浮点异常宏(表27-9列出的第一组宏)的按位或。例如,传递给这些函数的参数可能是FE_INVALID|FE_OVERFLOW|FE_UNDERFLOW,表示3种状态标志的组合;这些参数也可能是0,表示没有选择任何标志。

  • feclearexcept函数试图清除excepts所表示的浮点异常。如果excepts0或者所有指定的异常都成功清除,feclearexcept函数返回0;否则返回非零值。
  • fegetexceptflag函数试图获取excepts所表示的浮点状态标志。该数据存储在flagp指向的fexcept_t型对象中。如果状态标志成功存储,fegetexceptflag函数返回0;否则返回非零值。
  • feraiseexcept函数试图产生excepts所表示的浮点异常。产生上溢出或下溢出异常时,feraiseexcept是否还会同时产生不精确浮点异常由实现定义。(符合IEEE标准的实现会这样做。)如果excepts0或者所有指定的异常都成功产生,feraiseexcept函数返回0;否则返回非零值。
  • fesetexceptflag函数试图设置excepts所表示的浮点状态标志。这些数据存储在flagp指向的fexcept_t型对象中,且该对象必须已经由前面的fegetexceptflag函数调用设置过了。此外,前面的fegetexceptflag函数调用的第二个参数必须包含了excepts所表示的所有浮点异常。如果excepts0或者所有指定的异常都成功设置,fesetexceptflag函数返回0;否则返回非零值。
  • fetestexcept函数只测试excepts所表示的浮点状态标志,它返回与当前设置的标志相对应的浮点异常宏的按位或。例如,如果excepts的值是FE_INVALID|FE_OVERFLOW|FE_UNDERFLOWfetestexcept函数可能会返回FE_INVALID|FE_UNDERFLOW;这表明在FE_INVALIDFE_OVERFLOWFE_UNDERFLOW所表示的异常中,只有FE_INVALIDFE_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既可以指向由之前的fegetenvfeholdexcept函数调用所存储的浮点环境,也可以等于FE_DFL_ENV之类的浮点环境宏。与feupdateenv函数不同,fesetenv函数不会产生任何异常。如果用fegetenv函数调用来保存当前的浮点环境,那么以后可以调用fesetenv函数来恢复之前的浮点环境。

  • feupdateenv函数试图完成3个操作:(1)保存当前产生的浮点异常;(2)安装envp指向的浮点环境;(3)产生所保存的异常。envp既可以指向由之前的fegetenvfeholdexcept函数调用所存储的浮点环境,也可以等于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个版本之间还不一样。(frexpremquo也有指针参数,但类型总是int*。)如果为modf给出一个泛型宏,会引起一些难题。例如,modf(d, &f)(其中d的类型为doublef的类型为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!

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2023-12-15 08:02:05       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2023-12-15 08:02:05       106 阅读
  3. 在Django里面运行非项目文件

    2023-12-15 08:02:05       87 阅读
  4. Python语言-面向对象

    2023-12-15 08:02:05       96 阅读

热门阅读

  1. cfa一级考生复习经验分享系列(二)

    2023-12-15 08:02:05       56 阅读
  2. 在Go中理解栈和先进先出原则

    2023-12-15 08:02:05       59 阅读
  3. 【C语言】clock_gettime函数的使用

    2023-12-15 08:02:05       70 阅读
  4. 盈科数智视频管理平台简介

    2023-12-15 08:02:05       55 阅读
  5. 代码随想录494.目标和

    2023-12-15 08:02:05       45 阅读
  6. 模拟I2C通信

    2023-12-15 08:02:05       52 阅读
  7. npm 和 pip 、cnpm、Yum分别是什么

    2023-12-15 08:02:05       70 阅读
  8. Crow:基于req.rul查找路由Rule对象及匹配参数

    2023-12-15 08:02:05       62 阅读
  9. Android Studio(Flutter)常用快捷键

    2023-12-15 08:02:05       50 阅读
  10. GitHub 深度解析:高级功能和最佳实践

    2023-12-15 08:02:05       56 阅读
  11. uniapp使用u-search以及相关api

    2023-12-15 08:02:05       56 阅读