C現代方法(第27章)筆記——C99對數學計算的新增支持

文章目錄

  • 第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>的動機即源于這一認識。例如,如果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}} 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_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} 1?3i ? 3 ? 2 i {-3-2i} ?3?2i ? 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} (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的共軛。

復數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} 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 _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)=(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 開關

其中開關可以是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=2a?b±b2?4ac ??

一般來說,x的值是復數,因為當 b 2 ? 4 a c {b^2-4ac} b2?4ac(稱為判別式)小于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>中的函數泛型宏
acoscacosacos
asincasinasin
atancatanatan
acoshcacoshacosh
asinhcasinhasinh
atanhcatanhatanh
cosccoscos
sincsinsin
tanctantan
coshccoshcosh
sinhcsinhsinh
tanhctanhtanh
expcexpexp
logcloglog
powcpowpow
sqrtcsqrtsqrt
fabscabsfabs

第二組宏僅對應于<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_ENVconst 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!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/212092.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/212092.shtml
英文地址,請注明出處:http://en.pswp.cn/news/212092.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

人工智能與自然語言處理

人工智能&#xff08;AI&#xff09;與自然語言處理&#xff08;NLP&#xff09;是當前科技領域的兩大熱門話題。人工智能通過模擬人類的思維過程和智能行為&#xff0c;使計算機具備了一定的智能和自學能力。而自然語言處理則是指計算機對人類語言進行理解、處理和生成的技術。…

PCIe MPS參數介紹及如何更改

目錄 1.簡介 2.主要功能作用 3.MPS控制策略 4.如何更改 1.簡介 MPS 該參數含義是一個TLP包里攜帶的有效凈荷的最大值是多少字節&#xff08;該限制條件同時適用于寫操作和讀操作&#xff09;。 MRRS 該參數含義是一個TLP讀請求包&#xff0c;一次最多能向接收端請求讀出…

計算機畢業設計JAVA+SSM+springboot養老院管理系統

設計了養老院管理系統&#xff0c;該系統包括管理員&#xff0c;醫護人員和老人三部分。同時還能為用戶提供一個方便實用的養老院管理系統&#xff0c;管理員在使用本系統時&#xff0c;可以通過系統管理員界面管理用戶的信息&#xff0c;也可以進行個人中心&#xff0c;醫護等…

LeetCode 108. 將有序數組轉換為二叉搜索樹

對于算法題&#xff0c;按題型類別刷題才會更有成效&#xff0c;因此我這里在網上搜索并參考了下 “&#x1f525; LeetCode 熱題 HOT 100” 的題型歸類&#xff0c;并在其基礎上做了一定的完善&#xff0c;希望能夠記錄自己的刷題歷程&#xff0c;有所收獲&#xff01;點擊下發…

點滴生活記錄2

我從小跟著我爺爺奶奶&#xff0c;小學六年級轉到縣城上小學&#xff0c;就沒跟我奶奶他們住一起了。十一回家&#xff0c;把奶奶接到我這住&#xff0c;細想&#xff0c;自六年級之后&#xff0c;就很少跟奶奶住一起了。 奶奶&#xff08;間歇性&#xff09;耳聾&#xff0c;為…

104. 二叉樹的最大深度

給定一個二叉樹 root &#xff0c;返回其最大深度。二叉樹的最大深度 是指從根節點到最遠葉子節點的最長路徑上的節點數。 深度就是在層序遍歷的基礎上&#xff0c;每層遍歷一次&#xff0c;就增加一次深度&#xff01; import java.util.ArrayList; import java.util.LinkedL…

軟件測試相關

軟件測試是什么&#xff1f; 使用人工和自動手段來運行或測試某個系統的過程&#xff0c;其目的在于驗證它是否滿足規定的需求或弄清預期結果與實際結果的差別。 為什么做軟件測試&#xff1f;目的是什么&#xff1f; 發現軟件存在的代碼或業務邏輯錯誤 檢驗產品是否符合用戶需…

堅鵬:中國郵政儲蓄銀行數字化轉型戰略、方法與案例培訓

中國郵政儲蓄銀行擁有優良的資產質量和顯著的成長潛力&#xff0c;是中國領先的大型零售銀行。2016年9月在香港聯交所掛牌上市&#xff0c;2019年12月在上交所掛牌上市。中國郵政儲蓄銀行擁有近4萬個營業網點&#xff0c;服務個人客戶超6.5億戶。2022年&#xff0c;在《銀行家》…

算法Day24 不專心開車

不專心開車 Description 小碩開車經過一條公路&#xff0c;這條路線總共由n 1個不同海拔的點組成。小碩從海拔為0的點0開始騎行。 給小碩一個長度為n的整數數組arr&#xff0c;其中arr[i]是點i和點i 1的凈海拔高度差&#xff08;0≤i < n&#xff09;。請你返回最高點的海…

【LeetCode刷題-二叉樹】--110.平衡二叉樹

110.平衡二叉樹 方法一&#xff1a;自頂向下遞歸 對于當前遍歷到的節點&#xff0c;首先計算左右子樹的高度&#xff0c;如果左右子樹的高度差是否不超過 111&#xff0c;再分別遞歸地遍歷左右子節點&#xff0c;并判斷左子樹和右子樹是否平衡。這是一個自頂向下的遞歸的過程。…

力扣:197. 上升的溫度(Python3)

題目&#xff1a; 表&#xff1a; Weather ------------------------ | Column Name | Type | ------------------------ | id | int | | recordDate | date | | temperature | int | ------------------------ id 是該表具有唯一值的列。 該表…

[NAND Flash] 1.1 閃存(NAND Flash) 學習指南

依公知及經驗整理&#xff0c;原創保護&#xff0c;禁止轉載。 專欄 《深入理解NAND Flash》 ? 回首 漠然回首&#xff0c;從事存儲芯片行業已多年&#xff0c;這些年寶貴的青春都獻給了閃存。 我剛入行的時候&#xff0c;也是萌新一個&#xff0c;彷佛大學學的都沒有和這相…

Kubernetes簡介與部署

一、Kubernetes 簡介 1、概念&#xff1a; Kubernetes 又稱 k8s&#xff0c;是一個可移植、可擴展的開源平臺&#xff0c;用于管理容器化應用和服務&#xff0c;通過 Kubernetes 能夠進行應用的自動化部署和擴縮容。(k8s不是容器&#xff0c;而是一套容器編排系統) 官網&…

RC522(RFID射頻模塊)讀卡ID的簡單應用

文章目錄 一、RFID是什么&#xff1f;二、RC522模塊三、使用步驟1.硬件1.硬件連接2.引腳定義 2.軟件1.初始化配置代碼如下&#xff08;示例&#xff09;&#xff1a;2.引腳配置代碼如下&#xff08;示例&#xff09;&#xff1a;3.模塊復位代碼如下&#xff08;示例&#xff09…

11、虛函數、多態、純虛函數

11、虛函數、多態、純虛函數 虛函數覆蓋調用 多態實現多態的兩個必要條件多態 和 this指針多態的實現&#xff1a;虛函數表虛函數表與動態綁定動態綁定動態綁定對性能的影響 純虛函數抽象類純抽象類 虛函數 形如class 類名{ virtual 返回值 函數名(形參表) { … } }; 的成員函…

C++筆記之Delegate和委托構造(Delegating constructor)

C筆記之Delegate和委托構造辨析 code review! —— 杭州 2023-12-10 文章目錄 C筆記之Delegate和委托構造辨析0.有道詞典&#xff1a;英語發音1.ChatGPT&#xff1a;delegate概念詳解2.Delegate和“將可調用對象作為函數參數”是不是一回事&#xff1f;3.C的Delegate示例4.…

Numpy矩陣(第16講)

Numpy矩陣(第16講) ??????? ??博主 侯小啾 感謝您的支持與信賴。?? ??????????????????????????????????????????????????????????????????????????????????????????…

認識計算機的設備管理

在計算機系統中&#xff0c;除了處理器和內存之外&#xff0c;其他的大部分硬設備稱為外部設備。它包括輸入/輸出設備&#xff0c;輔存設備及終端設備等。這些設備種類繁多&#xff0c;特性各異&#xff0c;操作方式的差異很大&#xff0c;從而使操作系統的設備管理變得十分繁雜…

【數據結構】哈希表算法總結

知識概覽&#xff08;哈希表&#xff09; 哈希表可以將一些值域較大的數映射到較小的空間內&#xff0c;通常用x mod 質數的方式進行映射。為什么用質數呢&#xff1f;這樣的質數還要離2的整數冪盡量遠。這可以從數學上證明&#xff0c;這樣沖突最小。取余還是會出現沖突情況。…

《三十一》開發模式構建工具 Vite

20的40分鐘之前還沒看。 20的1小時15分 基于 Vite2。 在實際開發中&#xff0c;編寫的代碼往往是不能被瀏覽器直接識別的&#xff0c;例如 ES6、React、Vue、TypeScript 等&#xff0c;必須通過構建工具來對代碼進行轉換、編譯&#xff0c;例如 Webpack、Rolluop、Vite 等。 V…