文章目錄
- 第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!