文章目錄
- 第23章 庫對數值和字符數據的支持
- 23.1 <float.h>: 浮點類型的特性
- 23.2 <limits.h>: 整數類型的大小
- 23.3 <math.h>: 數學計算(C89)
- 23.3.1 錯誤
- 23.3.2 三角函數
- 23.3.3 雙曲函數
- 23.3.4 指數函數和對數函數
- 23.3.5 冪函數
- 23.3.6 就近舍入、絕對值函數和取余函數
- 23.4 <math.h>: 數學計算(C99)
- 23.4.1 IEEE浮點標準
- 23.4.2 類型
- 23.4.3 宏
- 23.4.4 錯誤
- 23.4.5 函數
- 23.4.6 分類宏
- 23.4.7 三角函數
- 23.4.8 雙曲函數
- 23.4.9 指數函數和對數函數
- 23.4.10 冪函數和絕對值函數
- 23.4.11 誤差函數和伽馬函數
- 23.4.12 就近舍入函數
- 23.4.13 取余函數
- 23.4.14 操作函數
- 23.4.15 最大值函數、最小值函數和正差函數
- 23.4.16 浮點乘加
- 23.4.17 比較宏
- 23.5 <ctype.h>: 字符處理
- 23.5.1 字符分類函數
- 23.5.2 字符大小寫映射函數
- 23.6 <string.h>: 字符串處理
- 23.6.1 復制函數
- 23.6.2 拼接函數
- 23.6.3 比較函數
- 23.6.4 搜索函數
- 23.6.5 其他函數
- 問與答
- 寫在最后
第23章 庫對數值和字符數據的支持
——與計算機過長時間的接觸會把數學家變成書記員,也會把書記員變成數學家。
本章會介紹
5
個函數庫的頭,這5
個頭提供了對數值、字符和字符串的支持。23.1節
和23.2節
分別介紹了<float.h>
和<limits.h>
頭,它們包含了用于描述數值和字符類型特性的宏。23.3節
和23.4節
描述<math.h>
頭,它提供了數學函數。23.3節
討論C89
版本的<math.h>
頭,而23.4節
則講述C99
中新增的內容,因為內容很多,所以將分別介紹。23.5節
和23.6節
分別討論<ctype.h>
和<string.h>
頭,這兩個頭分別提供了字符函數和字符串函數。
C99
增加了幾個也能處理數、字符和字符串的頭。<wchar.h>
和<wctype.h>
頭在第25章
中討論。第27章
討論<complex.h>
、<fenv.h>
、<inttypes.h>
、<stdint.h>
和<tgmath.h>
。
23.1 <float.h>: 浮點類型的特性
<float.h>
中提供了用來定義float
、double
和long double
類型的范圍及精度的宏。在<float.h>
中沒有類型和函數的定義。
有兩個宏對所有浮點類型適用。FLT_ROUNDS
表示當前浮點加法的舍入方向(23.4節)
。表23-1
列出了FLT_ROUNDS
的可能值。(對于表中沒有給出的值,舍入行為由實現定義。)
表23-1 舍入方向
取值 | 含義 |
---|---|
-1 | 不確定 |
-0 | 趨零截尾 |
-1 | 向最近的整數舍入 |
-2 | 向正無窮方向舍入 |
-3 | 向負無窮方向舍入 |
與<float.h>
中的其他宏(表示常量表達式)不同,FLT_ROUNDS
的值在執行期間可以改變。(fesetround函
數允許程序改變當前的舍入方向。)另一個宏FLT_RADIX
指定了指數表示中的基數,它的最小值為2
(表明二進制表示)。
其他宏用來描述具體類型的特性,這里會用一系列的表格來描述。根據宏是針對float
、double
還是long double
類型,每個宏都會以FLT
、DBL
或LDBL
開頭。C
標準對這些宏給出了相當詳細的定義,因此這里的介紹會更注重通俗易懂,不追求十分精確。依據C
標準,表中列出了部分宏的最大值和最小值。
表23-2
列出了定義每種浮點類型的有效數字個數的宏:
表23-2 <float.h>
中的有效數字宏
宏名 | 取值 | 宏的描述 |
---|---|---|
FLT_MANT_DIG | 有效數字的個數(基數FLT_RADIX ) | |
DBL_MANT_DIG | ||
LDBL_MANT_DIG | ||
FLT_DIG | ≥6 | 有效數字的個數(十進制) |
DBL_DIG | ≥10 | |
LDBL_DIG | ≥10 |
表23-3
列出了與指數相關的宏:
表23-3 <float.h>
中的指數宏
宏名 | 取值 | 宏的描述 |
---|---|---|
FLT_MIN_EXP | FLT_RADIX 的最小(負的)次冪 | |
DBL_MIN_EXP | ||
LDBL_MIN_EXP | ||
FLT_MIN_10_EXP | ≤-37 | 10 的最小(負的)次冪 |
DBL_MIN_10_EXP | ≤-37 | |
LDBL_MIN_10_EXP | ≤-37 | |
FLT_MAX_EXP | FLT_RADIX 的最大次冪 | |
DBL_MAX_EXP | ||
LDBL_MAX_EXP | ||
FLT_MAX_10_EXP | ≥+37 | 10 的最大次冪 |
DBL_MAX_10_EXP | ≥+37 | |
LDBL_MAX_10_EXP | ≥+37 |
表23-4
列出的宏描述了最大值、最接近0
的值以及兩個連續的數之間的最小差值:
表23-4 <float.h>
中的最大值、最小值和差值宏
宏名 | 取值 | 宏的描述 |
---|---|---|
FLT_MAX | ≥10^37 | 最大的有限值 |
DBL_MAX | ≥10^37 | |
LDBL_MAX | ≥10^37 | |
FLT_MIN | ≤10^-37 | 最小的正值 |
DBL_MIN | ≤10^-37 | |
LDBL_MIN | ≤10^-37 | |
FLT_EPSILON | ≤10^-5 | 兩個數之間可表示的最小差值 |
DBL_EPSILON | ≤10^-9 | |
LDBL_EPSILON | ≤10^-9 |
C99
提供了另外兩個宏:DECIMAL_DIG
和FLT_EVAL_METHOD
。DECIMAL_DIG
表示所支持的最大浮點類型的有效數字個數(以10
為基數)。FLT_EVAL_METHOD
的值說明具體的實現中是否用到了超出實際需要的范圍和精度的浮點運算。例如,如果該宏的值為0
,那么對兩個float
類型的值相加就按照正常的方法進行;但如果該宏的值為1
,在執行加法之前需要先把float
類型的值轉換為double
類型的值。表23-5
列出了FLT_EVAL_METHOD
可能的取值。(表中沒有給出的負值表示由實現定義的行為。)
表23-5 求值方法
取值 | 含義 |
---|---|
-1 | 不確定 |
0 | 根據類型的范圍和精度對所有運算和常量求值 |
1 | 根據double 類型的范圍和精度對所有float 類型和double類 型的運算和常量求值 |
2 | 根據long double 類型的范圍和精度對所有類型的運算和常量求值 |
<float.h>
中定義的大多數宏只有數值分析領域的專家才會感興趣,因此這可能是標準庫中最不常用的頭。
23.2 <limits.h>: 整數類型的大小
<limits.h>
中提供了用于定義每種整數類型(包括字符類型)取值范圍的宏。在<limits.h>
中沒有聲明類型或函數。
<limits.h>
中的一組宏用于字符類型:char
、signed char
和unsigned char
。表23-6
列舉了這些宏以及它們的最大值或最小值。
表23-6 <limits.h>
中的字符類型宏
宏名 | 取值 | 宏的描述 |
---|---|---|
CHAR_BIT | ≥8 | 每個字符包含位的位數 |
SCHAR_MIN | ≤-127 | 最小的signed char 類型值 |
SCHAR_MAX | ≥+127 | 最大的signed char 類型值 |
UCHAR_MAX | ≥255 | 最大的unsigned char 類型值 |
CHAR_MIN | ① | 最小的char 類型值 |
CHAR_MAX | ② | 最大的char 類型值 |
MB_LEN_MAX | ≥1 | 多字節字符最多包含的字節數 |
①如果char
類型被當作有符號類型,則CHAR_MIN
與SCHAR_MIN
相等,否則CHAR_MIN
為0
。
②根據char
類型被當作有符號類型還是無符號類型,CHAR_MAX
分別與SCHAR_MAX
或UCHAR_MAX
相等。
其他在
<limits.h>
中定義的宏針對整數類型:short int
、unsigned short int
、int
、unsigned int
、long int
以及unsigned long int
。表23-7
列舉了這些宏以及它們的最大值或最小值,并給出了計算各個值的公式。注意!!C99
及之后的標準提供了三個宏來描述long long int
類型的特性:
表23-7 <limits.h>
中整數類型的宏
宏名 | 取值 | 公式 | 宏的描述 |
---|---|---|---|
SHRT_MIN | ≤-32767 | -(2^15-1) | 最小的short int 類型值 |
SHRT_MAX | ≥+32767 | 2^15-1 | 最大的shor tint 類型值 |
USHRT_MAX | ≥65535 | 2^16-1 | 最大的unsigned short int 類型值 |
INT_MIN | ≤-32767 | -(2^15-1) | 最小的int 類型值 |
INT_MAX | ≥+32767 | 2^15-1 | 最大的int 類型值 |
UINT_MAX | ≥65535 | 2^16-1 | 最大的unsigned int 類型值 |
LONG_MIN | ≤-2147483647 | -(2^31-1) | 最小的long int 類型值 |
LONG_MAX | ≥+2147483647 | 2^31-1 | 最大的long int 類型值 |
ULONG_MAX | ≥4292967295 | 2^32-1 | 最大的unsigned long int 類型值 |
LLONG_MIN① | ≤-9223372036854775807 | -(2^63-1) | 最小的long long int 類型值 |
LLONG_MAX① | ≥+9223372036854775807 | 2^63-1 | 最大的long long int 類型值 |
ULLONG_MAX① | ≥18446744073709551615 | 2^64-1 | 最大的unsigned long long int 類型值 |
①僅C99
及之后的標準才有。
<limits.h>
中定義的宏在查看編譯器是否支持特定大小的整數時十分方便。例如,如果要判斷int
類型是否可以用來存儲像100000
一樣大的數,可以使用下面的預處理指令:
#if INT_MAX < 100000
#error int type is too small
#endif
如果int
類型不適用,#error指令(14.5節)
會導致預處理器顯示一條出錯消息。
進一步講,可以使用<limits.h>
中的宏來幫助程序選擇正確的類型定義。假設Quantity
類型的變量必須可以存儲像100000
一樣大的整數。如果INT_MAX
至少為100000
,就可以將Quantity
定義為int
;否則,要定義為long int
:
#if INT_MAX >= 100000
typedef int Quantity;
#else
typedef long int Quantity;
#endif
23.3 <math.h>: 數學計算(C89)
C89
的<math.h>
中定義的函數包含下面5
種類型:
- 三角函數;
- 雙曲函數;
- 指數和對數函數;
- 冪函數;
- 就近舍入函數、絕對值函數和取余函數。
C99
在這5
種類型中增加了許多函數,并且新增了一些其他類型的數學函數。C99
中對<math.h>
所做的改動很大,下一節將專門討論相關內容。
在深入討論<math.h>
提供的函數之前,先來簡單地了解一下這些函數是如何處理錯誤的。
23.3.1 錯誤
<math.h>
中的函數對錯誤的處理方式與其他庫函數不同。當發生錯誤時,<math.h>
中的大多數函數會將一個錯誤碼存儲到[在<errno.h>(24.2節)
中聲明的]一個名為errno
的特殊變量中。此外,一旦函數的返回值大于double
類型的最大取值,<math.h>
中的函數會返回一個特殊的值,這個值由HUGE_VAL
宏定義(這個宏在<math.h>
中定義)。HUGE_VAL
是double
類型的,但不一定是普通的數。[IEEE
浮點運算標準定義了一個值叫“無窮數”(23.4節)
,這個值是HUGE_VAL的
一個合理的選擇。]
<math.h>
中的函數檢查下面兩種錯誤:
- 定義域錯誤。函數的實參超出了函數的定義域。當定義域錯誤發生時,函數的返回值是由實現定義的,同時
EDOM(“定義域錯誤”)
會被存儲到errno
中。在<math.h>的
某些實現中,當定義域錯誤發生時,函數會返回一個特殊的值NaN(“非數”)
。 - 取值范圍錯誤。函數的返回值超出了
double
類型的取值范圍。如果返回值的絕對值過大(上溢出),函數會根據正確結果的符號返回正的或負的HUGE_VAL
。此外,值ERANGE
(“取值范圍錯誤”)會被存儲到errno
中。如果返回值的絕對值太小(下溢出),函數返回零;一些實現可能也會將ERANGE
存儲到errno
中。
本節不討論取余時可能發生的錯誤。附錄D
中的函數描述會解釋導致每種錯誤的情況。
23.3.2 三角函數
double acos(double x);
double asin(double x);
double atan(double x);
double atan2(double y, double x);
double cos(double x);
double sin(double x);
double tan(double x);
cos
、sin
和tan
函數分別用來計算余弦、正弦和正切。假定PI
被定義為3.14159265
,那么以PI/4
為參數調用cos
、sin
和tan
函數會產生如下的結果:
cos(PI/4) ? 0.707107
sin(PI/4) ? 0.707107
tan(PI/4) ? 1.0
//注意!!傳遞給 cos、sin和tan函數的實參是以弧度表示的,而不是以角度表示的。
acos
、asin
和atan
函數分別用來計算反余弦、反正弦和反正切:
acos(1.0) ? 0.0
asin(1.0) ? 1.5708
atan(1.0) ? 0.785398
對cos
函數的計算結果直接調用acos
函數不一定會得到最初傳遞給cos
函數的值,因為acos
函數始終返回一個0~π
的值。asin
函數與atan
函數會返回-π/2~π/2
的值。
atan2
函數用來計算y/x
的反正切值,其中y
是函數的第一個參數,x
是第二個參數。atan2
函數的返回值在-π~π
范圍內。調用atan(x)
與調用atan2(x,1.0)
等價。
23.3.3 雙曲函數
double cosh(double x);
double sinh(double x);
double tanh(double x);
cosh
、sinh
和tanh
函數分別用來計算雙曲余弦、雙曲正弦和雙曲正切:
cosh(0.5) ? 1.12763
sinh(0.5) ? 0.521095
tanh(0.5) ? 0.462117
傳遞給cosh
、sinh
和tanh
函數的實參必須以弧度
表示,而不能以角度表示。
23.3.4 指數函數和對數函數
double exp(double x);
double frexp(double value, int *exp);
double ldexp(double x, int exp);
double log(double x);
double log10(double x);
double modf(double value, double *iptr);
exp
函數返回e
的冪:
exp(3.0) ? 20.0855
log
函數是exp
函數的逆運算,它計算以e
為底的對數。log10
計算“常用”(以10
為底)對數:
log(20.0855) ? 3.0
log10(1000) ? 3.0
對于不以e
或10
為底的對數,計算起來也不復雜。例如,下面的函數對任意的x
和b
,計算以b
為底x
的對數:
double log_base(double x, double b)
{ return log(x) / log(b);
}
modf
函數和frexp
函數將一個double
型的值拆解為兩部分。modf
將它的第一個參數分為整數部分和小數部分,返回其中的小數部分,并將整數部分存入第二個參數所指向的對象中:
modf(3.14159, &int_part) ? 0.14159
//(int_part被賦值為3.0)
雖然int_part
的類型必須為double
,但我們始終可以隨后將它強制轉換成int
或long int
。
frexp
函數將浮點數拆成小數部分?
和指數部分n
,使得原始值等于?×2^n
,其中0.5≤?≤1
或?=0
。函數返回?
,并將n
存入第二個參數所指向的(整數)對象中:
frexp(12.0, &exp) ? 0.75 //(exp被賦值為 4)
frexp(0.25, &exp) ? 0.5 //(exp被賦值為-1)
ldexp
函數會抵消frexp
產生的結果,將小數部分和指數部分組合成一個數:
ldexp(.75, 4) ? 12.0
ldexp(0.5, -1) ? 0.25
一般而言,調用ldexp(x, exp)
將返回x × 2^exp
。
modf
、frexp
和ldexp
函數主要供<math.h>
中的其他函數使用,很少在程序中直接調用。
23.3.5 冪函數
double pow(double x, double y);
double sqrt(double x);
pow
函數計算第一個參數的冪,冪的次數由第二個參數指定:
pow(3.0, 2.0) ? 9.0
pow(3.0, 0.5) ? 1.73205
pow(3.0, -3.0) ? 0.037037
sqrt
函數計算平方根:
sqrt(3.0) ? 1.73205
由于通常sqrt
函數比pow
函數的運行速度快得多,因此使用sqrt
計算平方根更好。
23.3.6 就近舍入、絕對值函數和取余函數
double ceil(double x);
double fabs(double x);
double floor(double x);
double fmod(double x, double y);
ceil
函數返回一個double
類型的值,這個值是大于或等于其參數的最小整數。floor
函數則返回小于或等于其參數的最大整數:
ceil(7.1) ? 8.0
ceil(7.9) ? 8.0
ceil(-7.1) ? -7.0
ceil(-7.9) ? -7.0 floor(7.1) ? 7.0
floor(7.9) ? 7.0
floor(-7.1) ? -8.0
floor(-7.9) ? -8.0
換言之,ceil
“向上舍入”到最近的整數,floor
“向下舍入”到最近的整數。C89
沒有標準庫函數可以用來舍入到最近的整數,但我們可以簡單地使用ceil
函數和floor
函數來實現一個這樣的函數:
double round_nearest(double x)
{ return x < 0.0 ? ceil(x - 0.5) : floor(x + 0.5);
}
fabs
函數計算參數的絕對值:
fabs(7.1) ? 7.1
fabs(-7.1) ? 7.1
fmod
函數返回第一個參數除以第二個參數所得的余數:
fmod(5.5, 2.2) ? 1.1
注意!!C
語言不允許對%
運算符使用浮點操作數,不過fmod
函數足以用來替代%
運算符。
23.4 <math.h>: 數學計算(C99)
C99
的<math.h>
包含了所有C89
版本的內容,同時增加了許多的類型、宏和函數。相關的改動很多,我們將分別介紹。標準委員會為<math.h>
增加這么多內容,有以下幾個原因:
- 更好地支持
IEEE
浮點標準。C99
不強制使用IEEE
標準,其他表示浮點數的方法也是允許的。但是,大多數C
程序運行于支持IEEE
標準的系統上。 - 更好地控制浮點運算。對浮點運算加以更好的控制可以使程序達到更高的精度和速度。
- 使
C
對Fortran
程序員更具吸引力。增加了許多數學函數,并在C99
中做了一些增強(例如,加入了對復數的支持),可以增強C
語言對曾經使用過其他編程語言的程序員(主要是Fortran
程序員)的吸引力。
小補充:普通的
C
程序員可能對這一節并不很感興趣。把C
語言用于傳統應用程序(包括系統編程和嵌入式系統)的人可能不需要用到C99
提供的新函數。但是,開發工程、數學或科學應用程序的程序員可能會覺得這些函數非常有用。
23.4.1 IEEE浮點標準
改動
<math.h>
頭的動機之一是為了更好地支持IEEE 754
標準,這是應用最廣的浮點數表示方法。這個標準完整的名稱為“IEEE Standard for Binary Floating-Point Arithmetic”(ANSI/IEEE 標準754-1985)
,也叫作3IEC 60599
,這是C99
標準中的叫法。
7.2節
描述了IEEE
標準的一些基本性質。該標準提供了兩種主要的浮點數格式:單精度(32位)
和雙精度(64位)
。數值按科學記數法存儲,每個數包括三個部分:符號、指數和小數。對IEEE
標準的這一有限了解足以有效地使用C89
的<math.h>
了。但是,要了解C9
9的<math.h>
,則需要更詳細地了解IEEE
標準。下面是一些我們需要了解的信息。
-
正零/負零。在浮點數的
IEEE
表示中有一位代表數的符號。因此,根據該位的不同取值,零既可以是正數也可以是負數。零具有兩種表示這一事實有時要求我們把它與其他浮點數區別對待。 -
非規范化的數。進行浮點運算的時候,結果可能會太小以至于不能表示,這種情況稱為
下溢出
。考慮使用計算器反復除以一個數的情況:結果最終為零,這是因為數值會變得太小,以至于計算器無法顯示。IEEE
標準提供了一種方法來減弱這種現象的影響。通常浮點數按“規范”格式存儲,二進制小數點的左邊恰好只有一位數字。當數變得足夠小時,就按另一種非規范化的形式來存儲。這些非規范化的數(subnormal number也叫作denormalized number或denormal)
可以比規范化的數小很多,代價是當數變得越來越小時精度會逐漸降低。 -
特殊值。每個浮點格式允許表示三種特殊值:正無窮數、負無窮數和
NaN(非數)
。正數除以零產生正無窮數,負數除以零產生負無窮數,數學上沒有定義的運算(如零除以零)產生的結果是NaN
(更準確的說法是“結果是一種NaN
”而不是“結果是NaN
”,因為IEEE
標準有多種表示NaN
的方式。NaN
的指數部分全為1
,但小數部分可以是任意的非零位序列)。后續的運算中可以用特殊值作為操作數。對無窮數的運算與通常的數學運算是一樣的。例如,正數除以正無窮數結果為零(需要注意,算術表達式的中間結果可能會是無窮數,但最終結果不是無窮數)。對NaN
進行任何運算,結果都為NaN
。 -
舍入方向。當不能使用浮點表示法精確地存儲一個數時,當前的舍入方向(或者叫舍入模式)可以確定選擇哪個浮點值來表示該數。一共有
4
種舍入方向:- 向最近的數舍入,向最接近的可表示的值舍入,如果一個數正好在兩個數值的中間就向“偶”值(最低有效位為
0
)舍入; - 趨零截尾;
- 向正無窮方向舍入;
- 向負無窮方向舍入。
默認的舍入方向是向最近的數舍入。
- 向最近的數舍入,向最接近的可表示的值舍入,如果一個數正好在兩個數值的中間就向“偶”值(最低有效位為
-
異常。有
5
種類型的浮點異常:上溢出、下溢出、除零、無效運算(算術運算的結果是NaN
)和不精確(需要對算術運算的結果舍入)。當檢查到其中任何一個條件時,我們稱拋出異常。
23.4.2 類型
C99
在<math.h>
中加入了兩種類型:float_t
和double_t
。float_t
類型至少和float
型一樣“寬”(意思是說有可能是float
型,也可能是double
等更寬的類型)。同樣地,double_t
要求寬度至少是double
類型的(至少和float_t
一樣寬)。這些類型提供給程序員以最大限度地提高浮點運算的性能。float_t
應該是寬度至少為float的
最有效的浮點類型,double_t
應該是寬度至少為double
的最有效的浮點類型。
float_t
和double_t
類型與宏FLT_EVAL_METHOD(23.1節)
相關,如表23-8
所示。
表23-8 float_t
和double_t
類型與FLT_EVAL_METHOD
宏的關系
FLT_EVAL_METHOD的值 | float_t的含義 | double_t的含義 |
---|---|---|
0 | float | double |
1 | double | double |
2 | long double | long double |
其他 | 由實現定義 | 由實現定義 |
23.4.3 宏
C99
給<math.h>
增加了許多宏,這里只介紹其中的兩個:INFINITY
表示正無窮數和無符號無窮數的float
版本(如果實現不支持無窮數,那么INFINITY
表示編譯時會導致上溢出的float
類型值);NAN
宏表示“非數”的float
版本,更具體地說,它表示“安靜的”NaN
(用于算術表達式時不會拋出異常)。如果不支持安靜的NaN
,NAN
宏不會被定義。
本節后面將介紹<math.h>
中類似于函數的宏以及普通的函數。只和具體函數相關的宏與該函數一起討論。
23.4.4 錯誤
在大多數情況下,
C99
版本的<math.h>
在處理錯誤時和C89
版本的相同,但有幾點需要討論。
首先,C99
提供的一些宏允許在實現時選擇如何提示出錯消息:通過存儲在errno
中的值、通過浮點異常,或者兩者都有。宏MATH_ERRNO
和MATH_ERREXCEPT
分別表示整型常量1
和2
。另一個宏math_errhandling
表示一個int
表達式,其值可以是MATH_ERRNO
、MATH_ERREXCEPT
或者兩者按位或運算的結果(math_errhandling
也可能不是一個真正的宏,它可能是一個具有外部鏈接的標識符)。在程序內math_errhandling
的值不會改變。
其次,我們來看看在調用<math.h>
的函數時出現定義域錯誤的情形。C89
會把EDOM
存放在errno
中。在C99
標準中,如果表達式math_errhandling&MATH_ERRNO
非零(即設置了MATH_ERRNO
位),那么會把EDOM
存放在errno
中;如果表達式math_errhandling&MATH_ERREXCEPT
非零,會拋出無效運算浮點異常。根據math_errhandling
取值的不同,這兩種情況都有可能出現。
最后,討論一下在函數調用過程中出現取值范圍錯誤的處理方式。根據返回值的大小有2
種情形。
上溢出(
overflow
)。如果返回的值太大,C89
標準要求函數根據正確結果的符號返回正的或負的HUGE_VAL
。另外,把ERANGE
存儲在errno
中。C99
標準在發生上溢出時會有更復雜的處理方式。
- 如果采用默認的舍入方向或返回值是“精確的無窮數”(如
log(0.0)
),根據返回類型的不同,函數會返回HUGE_VAL
、HUGE_VALF
或者HUGE_VALL
(HUGE_VALF
和HUGE_VALL
是C99
新增的,分別表示HUGE_VAL
的float
和long double
版本。與HUGE_VAL
一樣,它們可以表示正無窮數)。返回值與正確結果的符號相同。 - 如果
math_errhandling&MATH_ERRNO
的值非零,把ERANGE
存于errno
中。 - 如果
math_errhandling&MATH_ERREXCEPT
的值非零,當數學計算的結果是精確的無窮數時拋出除零浮點異常,否則拋出上溢出異常。
下溢出(
underflow
)。如果返回的值太小而無法表示,C89
要求函數返回0
,一些實現可能也會將ERANGE
存入errno
。C99
中的處理有點不同。
- 函數返回值小于或等于相應返回類型的最小規范化正數。(這個值可以是
0
或者非規范化的數
。) - 如果
math_errhandling&MATH_ERRNO
的值非零,實現中有可能把ERANGE
存于errno
中。 - 如果
math_errhandling&MATH_ERREXCEPT
的值非零,實現中有可能拋出下溢出浮點異常。
注意后兩種情況中的“有可能”,為了執行的效率,實現不要求修改errno
或拋出下溢出異常。
23.4.5 函數
現在可以討論
C99
在<math.h>
中新增的函數了。本節將使用C99
標準中的分類方法把函數分組討論,這種分類和23.3節
中來自C89
的分類有些不一致。
在C99
版本中,對<math.h>
的最大改動是大部分函數都新增了兩個或兩個以上的版本。在C89
中,每個數學函數只有一種版本,通常至少有一個double
類型的參數或返回值是double
類型。C99
另外新增了兩個版本:float
類型和long double
類型。這些函數名和原本的函數名相同,只不過增加了后綴f
或l
。例如,原來的sqrt
函數對double
類型的值求平方根,現在就有了sqrtf(float版本)
和sqrtl(long double版本)
。本節將列出新版本的原型,但不會深入討論相應的函數,因為它們本質上與C89
中的對應函數一樣。
C99
版本的<math.h>
中也有許多全新的函數(以及類似函數的宏)。將會對每一個函數進行簡要的介紹。與23.3節
一樣,本節不會討論這些函數的錯誤條件,但是在附錄D(按字母序列出了所有的標準庫函數)
中會給出相關信息。本節沒有對所有新函數進行詳細描述,而只是描述主要的函數。例如,有三個函數可以計算反雙曲余弦,即acosh
、acoshf
和acoshl
,將只描述acosh
。
一定要記住:很多新的函數是非常特別的。因此描述看起來可能會很粗略,暫時不討論對這些函數具體用法。
23.4.6 分類宏
int fpclassify(實浮點 x);
int isfinite(實浮點 x);
int isinf(實浮點 x);
int isnan(實浮點 x);
int isnormal(實浮點 x);
int signbit(實浮點 x);
我們介紹的第一類包括類似函數的宏,它們用于確定浮點數的值是“規范化”的數
還是無窮數
或NaN
之類的特殊值。這組宏的參數都是任意的實浮點類型(float
、double
或者long double
)。
fpclassify
宏對參數分類,返回表23-9
中的某個數值分類宏。具體的實現可以通過定義以FP_
和大寫字母開頭的其他宏來支持其他分類。
表23-9 數值分類宏
名稱 | 含義 |
---|---|
FP_INFINITE | 無窮數(正或負) |
FP_PAN | 非數 |
FP_NORMAL | 規范化的數(不是0 、非規范化的數、無窮數或 NaN) |
FP_SUBNORMAL | 非規范化的數 |
FP_ZERO | 0 (正或負) |
如果isfinite
宏的參數具有有限值(0
、非規范化的數,或是除無窮數與NaN
之外的規范化的數),該宏返回非零值。如果isinf
的參數值為無窮數(正或負),該宏返回非零值。如果isnan
的參數值是NaN
,該宏返回非零值。如果isnormal
的參數是一個正常值(不是0
、非規范化的數、無窮數或NaN
),該宏返回非零值。(非零值=真)
最后一個宏與其他幾個有點區別。如果參數的符號為負,
signbit
返回非零值。參數不一定是有限數,signbit
也可以用于無窮數和NaN
。
23.4.7 三角函數
float acosf(float x); 見 acos
long double acosl(long double x); 見 acos float asinf(float x); 見 asin
long double asinl(long double x); 見 asin float atanf(float x); 見 atan
long double atanl(long double x); 見 atan float atan2f(float y float x); 見 atan2
long double atan2l(long double y,
long double x); 見 atan2 float cosf(float x); 見 cos
long double cosl(long double x); 見 cos float sinf(float x); 見 sin
long double sinl(long double x); 見 sin float tanf(float x); 見 tan
long double tanl(long double x); 見 tan
C99
中的新三角函數與C89
中的函數相似,具體描述見23.3節
的對應函數。
23.4.8 雙曲函數
double acosh(double x);
float acoshf(float x);
long double acosh1(long double x); double asinh(double x);
float asinhf(float x);
long double asinhl(long double x);double atanh(double x);
float atanhf(float x);
long double atanhl(long double x);float coshf(float x); 見 cosh
long double coshl(long double x); 見 cosh float sinhf(float x); 見 sinh
long double sinhl(long double x); 見 sinh float tanhf(float x); 見 tanh
long double tanhl(long double x); 見 tanh
這一組的6
個函數與C89
函數中的cosh
、sinh
和tanh
相對應。新的函數acosh
計算雙曲余弦,asinh
計算雙曲正弦,atanh
計算雙曲正切。
23.4.9 指數函數和對數函數
float expf(float x); 見 exp
long double expl(long double x); 見 exp double exp2(double x);
float exp2f(float x);
long double exp21(long double x);double expm1(double x);
float expm1f(float x);
long double expm1l(long double x);float frexpf(float value, int *exp); 見 frexp
long double frexpl(long double value, int *exp); 見 frexp int ilogb(double x);
int ilogbf(float x) ;
int ilogbl(long double x);float ldexpf(float x, int exp); 見 ldexp
long double ldexpl(long double x, int exp); 見 ldexp float logf(float x); 見 log
long double logl(long double x); 見 log float log10f(float x); 見 log10
long double log10l(long double x); 見 log10 double log1p(double x);
float log1pf(float x);
long double log1pl(long double x);double log2(double x);
float log2f(float x);
long double log2l(long double x);double logb(double x);
float logbf(float x);
long double logbl(long double x);float modff(float value, float *iptr); 見 modf
long double modfl(long double value, long double *iptr); 見 modf double scalbn(double x, int n);
float scalbnf(float x, int n);
long double scalbnl(long double x, int n);double scalbln(double x, long int n);
float scalblnf(float x, long int n);
long double scalblnl(long double x, long int n);
除了
exp
、frexp
、ldexp
、log
、log10
和modf
的新版本以外,這一類中還有一些全新的函數。其中exp2
和expm1
是exp
函數的變體。當應用于參數x
時,exp2
函數返回 2 x {2^x} 2x,expm1
返回 e x ? 1 {e^x-1} ex?1。
logb
函數返回參數的指數。更準確地說,調用logb(x)
返回log(r為底)(|x|)
,其中r
是浮點算術的基數(由宏FLT_RADIX
定義,通常值為2
)。ilogb
函數把logb
的值強制轉換為int
類型并返回。loglp
函數返回ln(1+x)
,其中x
是參數。log2
函數以2
為底計算參數的對數。
函數
scalbn
返回x乘FLT_RADIX^n
,這個函數能有效地進行計算(不會顯式地計算FLT_RADIX
的n
次冪)。scalbln
除第二個參數是long int
類型之外,其他和scalbn
函數相同。
23.4.10 冪函數和絕對值函數
double cbrt(double x);
float cbrtf(float x);
long double cbrtl(long double x);float fabsf(float x); 見 fabs
long double fabsl(long double x); 見 fabs double hypot(double x, double y);
float hypotf(float x, float y);
long double hypotl(long double x, long double y);float powf(float x, float y); 見 pow
long double powl(long double x, long double y); 見 pow float sqrtf(float x); 見 sqrt
long double sqrtl(long double x); 見 sqrt
這一組中的大部分函數是已有函數(fabs
、pow
和sqrt
)的新版,只有cbrt
和hypot
(以及它們的變體)是全新的。
cbrt
函數計算參數的立方根。pow
函數同樣可用于這個目的,但pow
不能處理負參數(負參數會導致定義域錯誤)。cbrt
既可以用于正參數也可以用于負參數,當參數為負時返回負值。
hypot
函數應用于參數x
和y
時返回 x 2 + y 2 \sqrt{x^2+y^2} x2+y2?。換句話說,這個函數計算的是邊長為x
和y
的直角三角形的斜邊。
23.4.11 誤差函數和伽馬函數
double erf(double x);
float erff(float x);
long double erfl(long double x);double erfc(double x);
float erfcf(float x);
long double erfcl(long double x);double lgamma(double x);
float lgammaf(float x);
long double lgammal(long double x);double tgamma(double x);
float tyammaf(float x);
long double tgammal(long double x);
函數
erf
計算誤差函數erf
(通常也叫高斯誤差函數
),常用于概率、統計和偏微分方程。erf
的數學定義如下:
e r f ( x ) = 2 π ∫ 0 x e ? t 2 d t erf(x) = \frac{2}{\sqrt{\pi}}\int_0^x{e^{-t^2}}dt erf(x)=π?2?∫0x?e?t2dt
erfc計算余誤差函數(complementary error function)
, e r f c ( x ) = 1 ? e r f ( x ) erfc(x)=1-erf(x) erfc(x)=1?erf(x)。
伽馬函數(gammafunction)
Γ \Gamma Γ是階乘函數的擴展,不僅可以應用于整數,還可以應用于實數。當應用于整數n
時, Γ ( n ) = ( n ? 1 ) ! {\Gamma(n)=(n-1)!} Γ(n)=(n?1)!。用于非整數的 Γ \Gamma Γ函數定義更為復雜。tgamma
函數計算 Γ \Gamma Γ。lgamma
函數計算 l n ( ∣ Γ ( x ) ∣ ) {ln(|\Gamma(x)|)} ln(∣Γ(x)∣),它是伽馬函數絕對值的自然對數。lgamma
有些時候會比伽馬函數本身更有用,因為伽馬函數增長太快,計算時容易導致溢出。
23.4.12 就近舍入函數
float ceilf(float x); 見 ceil
long double ceill(long double x); 見 ceil float floorf(float x); 見 floor
long double floorl(long double x); 見 floor double nearbyint(double x);
float nearbyintf(float x);
long double nearbyintl(long double x);double rint(double x);
float rintf(float x);
long double rintl(long double x);long int lrint (double x);
long int lrintf(float x);
long int lrintl(long double x);
long long int llrint(double x);
long long int llrintf(float x);
long long int llrintl(long double x);double round(double x);
float roundf(float x);
long double roundl(long double x); long int lround (double x);
long int lroundf(float x);
long int lroundl(long double x);
long long int llround(double x);
long long int llroundf(float x);
long long int llroundl(long double x); double trunc(double x);
float truncf(float x);
long double truncl(long double x);
除了ceil
和floor
的新增版本,C99
還新增了許多函數,用于把浮點值轉換為最接近的整數。在使用這些函數時需要注意:盡管它們都返回整數,但一些函數按浮點格式
(如float
、double
或long double
值)返回,一些函數按整數格式
(如long int
或long long int
值)返回。
nearbyint
函數對參數舍入,并以浮點數
的形式返回。nearbyint
使用當前的舍入方向,且不會拋出不精確浮點異常。rint
與nearbyint
相似,但當返回值與參數不相同時,有可能拋出不精確浮點異常。
lrint
函數根據當前的舍入方向對參數向最近的整數舍入。lrint
返回long int
類型的值。llrint
與lrint
相似,但返回long long int
類型的值。
round
函數對參數向最近的整數舍入,并以浮點數
的形式返回。round
函數總是向遠離零的方向舍入(如3.5
舍入為4.0
)。
lround
函數對參數向最近的整數舍入,并以long int
類型值的形式返回。和round
函數一樣,它總是向遠離零的方向舍入。llround
與lround
相似,但返回long long int
類型的值。
trunc
函數對參數向不超過參數的最近的整數舍入。(換句話說,它把參數趨零截尾。)trunc
以浮點數的形式返回結果。
23.4.13 取余函數
float fmodf(float x, float y); 見 fmod
long double fmodl(long double x, long double y); 見 fmod double remainder(double x, double y);
float remainderf(float x, float y);
long double remainderl(long double x, long double y);double remquo(double x, double y, int *quo);
float remquof(float x, float y, int *quo);
long double remquol(long double x, long double y, int *quo);
除了fmod
的新版本之外,這一類還包含兩種新增的函數:remainder
和remquo
。
remainder
返回的是xREMy
的值,其中REM
是IEEE
標準定義的函數。當y
不等于0
時,xREMy
的值為r=x-ny
,其中n
是與x/y
的準確值最接近的整數。(如果x/y
的值恰好位于兩個整數的中間,n
取偶數。)如果r=0
,則與x
的符號相一致。
remquo
函數的前兩個參數值與remainder
的相等時,其返回值也與remainder
的相等。另外,remquo
函數會修改參數quo
指向的對象,使其包含整數商|x/y|
的n
個低位字節,其中n
依賴于具體的實現但至少為3
。如果x/y<0
,存儲在該對象中的值為負。
23.4.14 操作函數
double copysign(double x, double y);
float copysignf(float x, float y);
long double copysignl(long double x, long double y);double nan(const char *tagp);
float nanf(const char *tagp);
long double nanl(const char *tagp);double nextafter(double x, double y);
float nextafterf(float x, float y);
long double nextafterl(long double x, long double y);double nexttoward(double x, long double y);
float nexttowardf(float x, long double y);
long double nexttowardl(long double x, long double y);
這些神秘的“操作函數”都是
C99
新增的。它們提供了對浮點數底層細節的訪問。
-
copysign
函數復制一個數的符號到另一個數。函數調用copysign(x,y)
返回的值大小與x
相等,符號與y
一樣。 -
nan
函數將字符串轉換為NaN
值。調用nan("n個字符的序列")
等價于strtod("NAN(n個字符的序列)",(char**)NULL)
。[討論strtod函數(26.2節)
時描述了n
個字符的序列的格式。]調用nan("")
等價于strtod("NAN()",(char**)NULL)
。如果nan
的參數既不是"n個字符的序列"
又不是""
,那么該調用等價于strtod("NAN",(char**)NULL)
。如果系統不支持安靜的NaN
,那么nan
返回0
。對nanf
和nanl
的調用分別等價于對strtof
和strtold
調用。這個函數用于構造包含特定二進制模式的NaN
值。(回憶一下本節前面的論述,NaN
值的小數部分是任意的。) -
nextafter
函數用于確定數值x
之后的可表示的值(如果x
類型的所有值都按序排列,這個值將恰好在x
之前或x
之后)。y
的值確定方向:如果y<x
,則函數返回恰好在x
之前的那個值;如果x<y
,則返回恰好在x
之后的那個值;如果x
和y
相等,則返回y
。 -
nexttoward
函數和nextafter
函數相似,區別在于參數y
的類型為long double
而不是double
。如果x
和y
相等,nexttoward
將返回被轉換為函數的返回類型的y
。nexttoward
函數的優勢在于,任意(實)浮點類型都可以作為第二個參數,而不用擔心會錯誤地將其轉換為較窄的類型。
23.4.15 最大值函數、最小值函數和正差函數
double fdim(double x, double y);
float fdimf(float x, float y);
long double fdiml(long double x, long double y);double fmax(double x, double y);
float fmaxf(float x, float y);
long double fmaxl(long double x, long double y);double fmin(double x, double y);
float fminf(float x, float y);
long double fmainl(long double x, long double y);
函數
fdim
計算x
和y
的正差:
f ( x ) = { x ? y , x > y + 0 , x ≤ y f(x) = \begin{cases} x-y,\,\,x>y\\ +0,\,\,x\le{y}\\ \end{cases} f(x)={x?y,x>y+0,x≤y?
fmax
函數返回兩個參數中較大的一個,fmin
返回較小的一個。
23.4.16 浮點乘加
double fma(double x, double y, double z);
float fmaf(float x, float y, float z);
long double fmal(long double x, long double y, long double z);
fma
函數是將它的前兩個參數相乘再加上第三個參數。換句話說,我們可以將語句
a = b * c + d;
替換為
a = fma(b, c, d);
在C99
中增加這個函數是因為一些新的CPU
具有“融合乘加”(fused multiply-add)
指令,該指令既執行乘法也執行加法。調用fma
告訴編譯器使用這個指令(如果可以的話),這樣比分別執行乘法指令和加法指令要快。而且,融合乘加指令只進行一次舍入,而不是兩次,所以可以產生更加精確的結果。融合乘加指令特別適用于需要執行一系列乘法和加法運算的算法,如計算兩個向量點積的算法或兩個矩陣相乘的算法。
為了確定是否可以調用
fma
函數,C99
程序可以測試FP_FAST_FMA
宏是否有定義。如果有定義,那么調用fma
應該會比分別進行乘法運算和加法運算要快(至少一樣快)。對于fmaf
函數和fmal
函數,FP_FAST_FMAF
和FP_FAST_FMAL
宏分別扮演著同樣的角色。
把乘法和加法合并成一條指令來執行是C99
標準中所說的“緊縮”(contraction)
的一個例子。緊縮把兩個或多個數學運算合并起來,當成一條指令來執行。從fma
函數可以看出,緊縮通常可以獲得更快的速度和更高的精度。但是,因為緊縮可能會導致結果發生細微的變化,所以程序員希望能控制緊縮是否自動進行(上面的fma
是顯式要求進行緊縮的)。極端情況下,緊縮可以避免拋出浮點異常。
C99
中可以用包含FP_CONTRACT
的#pragma
指令來實現對緊縮的控制,用法如下:
#pragma STDC FP_CONTRACT 開關
開關的值可以是ON
、OFF
或DEFAULT
。如果選擇ON
,編譯器允許對表達式進行緊縮;如果選擇OFF
,編譯器禁止對表達式進行緊縮;DEFAULT
用于恢復默認設置(ON
或OFF
)。如果在程序的外層(所有函數定義的外部)使用該指令,該指令將持續有效,直到在同一個文件中遇到另一條包含FP_CONTRACT
的#pragma
指令或者到達文件末尾。如果在復合語句(包括函數體)中使用該指令,必須將其放在所有聲明和語句之前;在到達復合語句的末尾之前,該指令都是有效的,除非被另一條#pragma
覆蓋。即便用FP_CONTRACT
禁止了對表達式的自動緊縮,程序仍然可以調用fma
執行顯式的緊縮。
23.4.17 比較宏
int isgreater(實浮點 x, 實浮點 y);
int isgreaterequal(實浮點 x, 實浮點 y);
int isless(實浮點 x, 實浮點 y);
int islessequal(實浮點 x, 實浮點 y);
int islessgreater(實浮點 x, 實浮點 y);
int isunordered(實浮點 x, 實浮點 y);
最后一類類似函數的宏對兩個數進行比較,它們的參數可以是任意實浮點類型。
增加比較宏是因為使用普通的關系運算符(如
<
和>
)比較浮點數時會出現問題。如果任一操作數(或兩個)是NaN
,那么這樣的比較就可能導致拋出無效運算浮點異常,因為NaN
的值(不同于其他浮點數的值)被認為是無序的。比較宏可以用來避免這種異常。這些宏可以稱作關系運算符的“安靜”版本,因為它們在執行時不會拋出異常。
isgreater
、isgreaterequal
、isless
和islessequal
宏分別執行與>
、>=
、<
和<=
相同的運算,區別在于,當參數無序時它們不會拋出無效運算浮點異常。
調用
islessgreater(x,y)
等價于(x)<(y)||(x)>(y)
,唯一的區別在于前者不會對x
和y
求兩次值,而且(與之前提到的宏一樣)當x
和y
無序時不會導致拋出無效運算浮點異常。
isunordered
宏在參數無序(其中至少一個是NaN
)時返回1
,否則返回0
。
23.5 <ctype.h>: 字符處理
<ctype.h>
提供了兩類函數:字符分類函數
(如isdigit
函數,用來檢測一個字符是否是數字)和字符大小寫映射函數
(如toupper
函數,用來將一個小寫字母轉換成大寫字母)。
雖然C
語言并不要求必須使用<ctype.h>
中的函數來測試字符或進行大小寫轉換,但我們仍建議使用<ctype.h>
中定義的函數來進行這類操作:
- 第一,這些函數已經針對運行速度進行過優化(實際上,大多數都是用宏實現的);
- 第二,使用這些函數會使程序的可移植性更好,因為這些函數可以在任何字符集上運行;
- 第三,當
地區(locale 25.1節)
改變時,<ctype.h>
中的函數會相應地調整其行為,使我們編寫的程序可以正確地運行在世界上不同的地點。
<ctype.h>
中定義的函數都具有int
類型的參數,并返回int
類型的值。許多情況下,參數事先存放在一個int
型的變量中(通常是調用fgetc
、getc
或getchar
讀取的結果)。當參數類型為char
時,需要小心。C
語言可以自動將char
類型的參數轉換為int
類型;如果char
是無符號類型或者使用ASCII
之類的7
位字符集,轉換不會出問題,但如果char
是有符號類型且有些字符需要用8
位來表示,那么把這樣的字符從char
轉換為int
就會得到負值。當參數為負時,<ctype.h>
中的函數行為是未定義的(EOF
除外),這樣可能會造成一些嚴重的問題。這種情況下應把參數強制轉換為unsigned char
類型以確保安全。(為了最大化可移植性,一些程序員在使用<ctype.h>
中的函數之前總是把char
類型的參數強制轉換為unsigned char
類型。)
23.5.1 字符分類函數
int isalnum(int c);
int isalpha(int c);
int isblank(int c);
int iscntrl(int c);
int isdigit(int c);
int isgraph(int c);
int islower(int c);
int isprint(int c);
int ispunct(int c);
int isspace(int c);
int isupper(int c);
int isxdigit(int c);
如果參數具有某種特定的性質,字符分類函數會返回非零值。表23-10
列出了每個函數所測試的性質。
表23-10 字符分類函數
取值 | 取值對應的舍入模式 |
---|---|
isalnum? | c是否是字母或數字 |
isalpha? | c是否是字母 |
isblank? | c是否是標準空白字符① |
iscntrl? | c是否是控制字符② |
isdigit? | c是否是十進制數字 |
isgraph? | c是否是可顯示字符(除空格外) |
islower? | c是否是小寫字母 |
isprint? | c是否是可打印字符(包括空格) |
ispunct? | c是否是標點符號③ |
isspace? | c是否是空白字符④ |
isupper? | c是否是大寫字母 |
isxdigit? | c是否是十六進制數字 |
①標準空白字符是空格
和水平制表符(\t)
。這是C99
中的新函數。
②在ASCII
字符集中,控制字符包括\x00
至\x1f
,以及\x7f
。
③標點符號包括所有可打印字符,但要除掉使isspace
或isalnum
為真的字符。
④空白字符包括空格
、換頁符(\f)
、換行符(\n)
、回車符(\r)
、水平制表符(\t)
和垂直制表符(\v)
。
ispunct
在C99
中的定義與在C89
中的定義略有不同。在C89
中,ispunct(c)
測試c
是否為除空格符和使isalnum(c)
為真的字符以外的可打印字符。在C99
中,ispunct(c)
測試c
是否為除了使isspace(c)
或isalnum(c)
為真的字符以外的可打印字符。
23.5.2 字符大小寫映射函數
int tolower(int c);
int toupper(int c);
tolower
函數返回與作為參數傳遞的字母相對應的小寫字母,而toupper
函數返回與作為參數傳遞的字母相對應的大寫字母。對于這兩個函數,如果所傳參數不是字母,那么將返回原始字符,不加任何改變。
下面的程序對字符串
"aA0!"
中的字符進行大小寫轉換:
/*
tcasemap.c
--Tests the case-mapping functio
*/
#include <ctype.h>
#include <stdio.h>
int main(void)
{ char *p; for (p = "aA0!"; *p != '\0'; p++) { printf("tolower('%c') is '%c'; ", *p, tolower(*p)); printf("toupper('%c') is '%c'\n", *p, toupper(*p)); } return 0;
}
/*
這段程序產生的輸出如下:
tolower('a') is 'a'; toupper('a') is 'A'
tolower('A') is 'a'; toupper('A') is 'A'
tolower('0') is '0'; toupper('0') is '0'
tolower('!') is '!'; toupper('!') is '!'
*/
23.6 <string.h>: 字符串處理
我們第一次見到
<string.h>
是在13.5節
,那一節中討論了最基本的字符串操作:字符串復制
、字符串拼接
、字符串比較
以及字符串長度計算
。接下來我們將看到,除了用于字符數組(不需要以空字符結尾)的字符串處理函數之外,<string.h>
中還有許多其他字符串處理函數。前一類函數的名字以mem
開頭,以表明它們處理的是內存塊而不是字符串。這些內存塊可以包含任何類型的數據,因此mem
函數的參數類型為void *
而不是char *
。
<string.h>
提供了5
種函數:
- 復制函數,將字符從內存中的一處復制到另一處。
- 拼接函數,向字符串末尾追加字符。
- 比較函數,用于比較字符數組。
- 搜索函數,在字符數組中搜索一個特定字符、一組字符或一個字符串。
- 其他函數,初始化字符數組或計算字符串的長度。
23.6.1 復制函數
void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
void *memmove(void * s1, const void * s2, size_t n);
char *strcpy(char * restrict s1, const char * restrict s2);
char *strncpy(char * restrict s1, const char * restrict s2, size_t n);
這一類函數將字符(字節)從內存的一處(源)移動到另一處(目的地)。每個函數都要求第一個參數指向目的地,第二個參數指向源。所有的復制函數都會返回第一個參數(即指向目的地的指針)。
-
memcpy
函數從源向目的地復制n
個字符,其中n
是函數的第三個參數。如果源和目的地之間有重疊,memcpy
函數的行為是未定義的。memmove
函數與memcpy
函數類似,只是在源和目的地重疊時它也可以正常工作。 -
strcpy
函數將一個以空字符結尾的字符串從源復制到目的地。strncpy
與strcpy
類似,只是它不會復制多于n
個字符,其中n
是函數的第三個參數。(如果n
太小,strncpy
可能無法復制結尾的空字符。)如果strncpy
遇到源字符串中的空字符,它會向目的字符串不斷追加空字符,直到寫滿n
個字符為止。與memcpy
類似,strcpy
和strncpy
不保證當源和目的地相重疊時可以正常工作。
下面的例子展示了所有的復制函數,注釋中給出了哪些字符會被復制:
char source[] = {'h', 'o', 't', '\0', 't', 'e', 'a'};
char dest[7];
memcpy(dest, source, 3); /* h, o, t */
memcpy(dest, source, 4); /* h, o, t, \0 */
memcpy(dest, source, 7); /* h, o, t, \0, t, e, a */
memmove(dest, source, 3); /* h, o, t */
memmove(dest, source, 4); /* h, o, t, \0 */
memmove(dest, source, 7); /* h, o, t, \0, t, e, a */
strcpy(dest, source); /* h, o, t, \0 */
strncpy(dest, source, 3); /* h, o, t */
strncpy(dest, source, 4); /* h, o, t, \0 */
strncpy(dest, source, 7); /* h, o, t, \0, \0, \0, \0 */
注意!!
memcpy
、memmove
和strncpy
都不要求使用空字符結尾的字符串,它們對任意內存塊都可以正常工作。而strcpy
函數則會持續復制字符,直到遇到一個空字符為止,因此strcpy
僅適用于以空字符結尾的字符串。
13.5節
給出了strcpy
和strncpy
的常見用法示例。這兩個函數都不完全安全,但至少strncpy
提供了一種方法來限制所復制字符的個數。
23.6.2 拼接函數
char *strcat(char * restrict s1, const char * restrict s2);
char *strncat(char * restrict s1, const char * restrict s2, size_t n);
strcat
函數將它的第二個參數追加到第一個參數的末尾。兩個參數都必須是以空字符結尾的字符串。strcat
函數會在拼接后的字符串末尾添加空字符。考慮下面的例子:
char str[7] = "tea";
strcat(str, "bag"); /* adds b, a, g, \0 to end of str */
字母b
會覆蓋"tea"
中字符a
后面的空字符,因此現在str
包含字符串"teabag"
。strcat
函數會返回它的第一個參數(指針)。
strncat
函數與strcat
函數基本一致,只是它的第三個參數會限制所復制字符的個數:
char str[7] = "tea";
strncat(str, "bag", 2); /* adds b, a, \0 to str */
strncat(str, "bag", 3); /* adds b, a, g, \0 to str */
strncat(str, "bag", 4); /* adds b, a, g, \0 to str */
正如上面的例子所示,strnact
函數會保證其結果字符串始終以空字符結尾。
在13.5節中我們發現,
strncat
的調用通常具有如下形式:
strncat(str1, str2, sizeof(str1) – strlen(str1) – 1);
第三個參數計算str1
中剩余的空間大小(由表達式sizeof(str1) – strlen(str1)
給定),然后減1
以確保給空字符留出空間。
23.6.3 比較函數
int memcmp(const void *s1, const void *s2, size_t n);
int strcmp(const char *s1, const char *s2);
int strcoll(const char *s1, const char *s2);
int strncmp(const char *s1, const char *s2, size_t n);
size_t strxfrm(char * restrict s1, const char * restrict s2, size_t n);
比較函數分為2
組。第一組中的函數(memcmp
、strcmp
和strncmp
)比較兩個字符數組的內容,第二組中的函數(strcoll
函數和strxfrm
函數)在需要考慮地區(25.1節)
時使用。
memcmp
、strcmp
和strncmp
函數有許多共性。這三個函數都需要以指向字符數組的指針作為參數,然后用第一個字符數組中的字符逐一地與第二個字符數組中的字符進行比較。這三個函數都是在遇到第一個不匹配的字符時返回。另外,這三個函數都根據比較結束時第一個字符數組中的字符是小于、等于還是大于第二個字符數組中的字符,而相應地返回負整數
、0
或正整數
。
這三個函數之間的差異在于,如果數組相同,則何時停止比較。memcmp
函數包含第三個參數n
,n
會用來限制參與比較的字符個數,但memcmp
函數不會關心空字符。strcmp
函數沒有對字符數設定限制,因此會在其中任意一個字符數組中遇到空字符時停止比較。(因此,strcmp
函數只能用于以空字符結尾的字符串。)strncmp
結合了memcmp
和strcmp
,當比較的字符數達到n
個或在其中任意一個字符數組中遇到空字符
時停止比較。
下面的例子展示了
memcmp
、strcmp
和strncmp
的用法:
char s1[] = {'b', 'i', 'g', '\0', 'c', 'a', 'r'};
char s2[] = {'b', 'i', 'g', '\0', 'c', 'a', 't'};
if (memcmp(s1, s2, 3) == 0) ... /* true */
if (memcmp(s1, s2, 4) == 0) ... /* true */
if (memcmp(s1, s2, 7) == 0) ... /* false */
if (strcmp(s1, s2) == 0)... /* true */
if (strncmp(s1, s2, 3) == 0) ... /* true */
if (strncmp(s1, s2, 4) == 0) ... /* true */
if (strncmp(s1, s2, 7) == 0) ... /* true */
strcoll
函數與strcmp
函數類似,但比較的結果依賴于當前的地區。
大多數情況下,strcoll
都足夠用來處理依賴于地區的字符串比較。但有些時候,我們可能需要多次進行比較(strcoll
的一個潛在問題是,它不是很快),或者需要改變地區而不影響比較的結果。在這些情況下,strxfrm
函數(“字符串變換”)可以用來代替strcoll
使用。
strxfrm
函數會對它的第二個參數(一個字符串)進行變換,將變換的結果放在第一個參數所指向的字符串中。第三個參數用來限制向數組輸出的字符個數,包括最后的空字符。用兩個變換后的字符串作為參數調用strcmp
函數所產生的結果應該與用原始字符串作為參數調用strcoll
函數所產生的結果相同(負
、0
或正
)。
strxfrm
函數返回變換后字符串的長度,因此strxfm
函數通常會被調用兩次:一次用于判斷變換后字符串的長度,一次用來進行變換。下面是一個例子:
size_t len;
char *transformed;
len = strxfrm(NULL, original, 0);
transformed = malloc(len + 1);
strxfrm(transformed, original, len);
23.6.4 搜索函數
void *memchr(const void *s, int c, size_t n);
char *strchr(const char *s, int c);
size_t strcspn(const char *s1, const char *s2);
char *strpbrk(const char *s1, const char *s2);
char *strrchr(const char *s, int c);
size_t strspn(const char *s1, const char *s2);
char *strstr(const char *s1, const char *s2);
char *strtok(char * restrict s1, const char * restrict s2);
strchr
函數在字符串中搜索特定字符。下面的例子說明了如何使用strchr
函數在字符串中搜索字母f
:
char *p, str[] = "Form follows function.";
p = strchr(str, 'f'); /* finds first 'f' */
strchr
函數會返回一個指針,這個指針指向str
中出現的第一個f
(即單詞follows
中的f
)。如果需要多次搜索字符也很簡單,例如,可以使用下面的調用搜索str
中的第二個f
(即單詞function
中的f
):
p = strchr(p + 1, 'f'); /* finds next 'f' */
//如果不能定位所需的字符,strchr返回空指針。
memchr
函數與strchr
函數類似,但memchr
函數會在搜索了指定數量的字符后停止搜索,而不是當遇到首個空字符時才停止。memchr
函數的第三個參數用來限制搜索時需要檢測的字符總數。當不希望對整個字符串進行搜索或搜索的內存塊不是以空字符結尾時,memchr
函數會十分有用。下面的例子用memchr
函數在一個沒有以空字符結尾的字符數組中進行搜索:
char *p, str[22] = "Form follows function.";
p = memchr(str, 'f', sizeof(str));
與strchr
函數類似,memchr
函數也會返回一個指針指向該字符第一次出現的位置。如果找不到所需的字符,memchr
函數返回空指針。
strrchr
函數與strchr
類似,但它會反向搜索字符:
char *p, str[] = "Form follows function.";
p = strrchr(str, 'f'); /* finds last 'f' */
在此例中,strrchr
函數會首先找到字符串末尾的空字符,然后反向搜索字母f
(單詞function
中的f
)。與strchr
和memchr
一樣,如果找不到指定的字符,strrchr
函數也返回空指針。
strpbrk
函數比strchr
函數更通用,它返回一個指針,該指針指向第一個參數中與第二個參數中任意一個字符匹配的最左邊一個字符:
char *p, str[] = "Form follows function.";
p = strpbrk(str, "mn"); /* finds first 'm' or 'n' */
/*
在此例中,p最終會指向單詞Form中的字母m。
當找不到匹配的字符時,strpbrk函數返回空指針。
*/
strspn
函數和strcspn
函數與其他的搜索函數不同,它們會返回一個表示字符串中特定位置的整數(size_t
類型)。當給定一個需要搜索的字符串以及一組需要搜索的字符時,strspn
函數返回字符串中第一個不屬于該組字符的字符的下標。對于同樣的參數,strcspn
函數返回第一個屬于該組字符的字符的下標。下面是使用這兩個函數的例子:
size_t n;
char str[] = "Form follows function.";
n = strspn(str, "morF"); /* n = 4 */
n = strspn(str, " \t\n"); /* n = 0 */
n = strcspn(str, "morF"); /* n = 0 */
n = strcspn(str, " \t\n"); /* n = 4 */
strstr
函數在第一個參數(字符串)中搜索第二個參數(也是字符串)。在下面的例子中,strstr
函數搜索單詞fun
:
char *p, str[] = "Form follows function.";
p = strstr(str, "fun"); /* locates "fun" in str */
strstr
函數返回一個指向待搜索字符串第一次出現的地方的指針。如果找不到,則返回空指針。在上例的調用后,p
會指向function
中的字母f
。
strtok
函數是最復雜的搜索函數。它的目的是在字符串中搜索一個“記號”——就是一系列不包含特定分隔字符的字符。調用strtok(s1,s2
)會在s1
中搜索不包含在s2
中的非空字符序列。strtok
函數會在記號末尾的字符后面存儲一個空字符作為標記,然后返回一個指針指向記號的首字符。
strtok
函數最有用的特點是以后可以調用strtok
函數在同一字符串中搜索更多的記號。調用strtok(NULL,s2)
就可以繼續上一次的strtok
函數調用。和上一次調用一樣,strtok
函數會用一個空字符來標記新的記號的末尾,然后返回一個指向新記號的首字符的指針。這個過程可以持續進行,直到strtok
函數返回空指針,這表明找不到符合要求的記號。
這里就不討論
strtok
函數的工作原理了,要明白,strtok
有幾個眾所周知的問題,這些問題限制了它的使用。這里只說以下兩個問題。首先,strtok
每次只能處理一個字符串,不能同時搜索兩個不同的字符串。其次,strtok
把一組分隔符與一個分隔符同等看待;因此,如果字符串中有些字段用分隔符(例如逗號)分開,有些字段為空,那么strtok
就不適用了。
23.6.5 其他函數
void *memset(void *s, int c, size_t n);
size_t strlen(const char *s);
memset
函數會將一個字符的多個副本存儲到指定的內存區域。假設p
指向一塊N
字節的內存,調用
memset(p, ' ', N);
會在這塊內存的每個字節中存儲一個空格。memset
函數的一個用途是將數組全部初始化為0
:
memset(a, 0, sizeof(a));
memset
函數會返回它的第一個參數(指針)。
strlen
函數返回字符串的長度,字符串末尾的空字符
不計算在內。strlen
函數的調用示例見13.5節
。
此外還有一個字符串函數——strerror函數(24.2節)
,會和<errno.h>
一起討論。
問與答
問1:
expml
函數的作用僅僅是從exp
函數的返回值里減去1
,為什么需要這個函數呢?
答:把exp
函數應用于接近0
的數時,其返回結果非常接近1
。因為舍入誤差的存在,從exp
的返回值里減去1
可能不精確。這種情況下expml
可以用來獲得更精確的結果。
loglp
函數的作用也是類似的。對于接近0
的x
值,loglp(x)
比log(1+x)
更精確。
問2:計算伽馬函數的函數為什么命名為
tgamma
而不是gamma
呢?
答:起草C99
標準的時候,有些編譯器已提供了名為gamma
的函數,但計算的是伽馬函數的對數。這個函數后來重命名為lgamma
。把伽馬函數的名字選為gamma
可能會和已有的程序相沖突,所以C99
委員會決定改用tgamma
(意為“truegamma”
)。
問3:描述
nextafter
函數時,為什么說當x
和y
相等時返回y
呢?如果x
和y
相等,返回x
與返回y
有區別嗎?
答:考慮調用nextafter(-0.0,+0.0)
,從數學上講兩個參數是相等的。如果返回y
而不是x
,函數的返回值為+0.0
(而不是-0.0
,那樣有違直覺)。類似地,調用nextafter(+0.0,-0.0)
返回-0.0
。
問4:為什么
<string.h>
中提供了那么多方法來做同一件事呢?真的需要4
個復制函數(memcpy
、memmove
、strcpy
和strncpy
)嗎?
答:我們先看memcpy
函數和strcpy
函數,使用這兩個函數的目的是不同的:strcpy
函數只會復制一個以空字符結尾的字符數組(也就是字符串),memcpy
函數可以復制沒有這一終止字符的內存塊(如整數數組)。
另外兩個函數可以使我們在安全性和運行速度之間做出選擇。strncpy
函數比strcpy
函數更安全,因為它限制了復制字符的個數。當然安全也是有代價的,因為strncpy
函數比strcpy
函數慢一點。使用memmove
函數也需要做出類似的抉擇。memmove
函數可以將字符從一塊內存區域復制到另一塊可能會與之相重疊的內存區域中。在同樣的情況下,memcpy
函數無法保證能夠正常工作;然而,如果可以確保沒有重疊,memcpy
函數很可能會比memmove
函數要快一些。
問5:為什么
strspn
函數有這么一個奇怪的名字?
答:不要將strspn
函數的返回值理解為不屬于指定字符集合的第一個字符的下標,而要將它的返回值理解為屬于指定字符集合的字符的最長“跨度”(span)
。
寫在最后
本文是博主閱讀《C語言程序設計:現代方法(第2版·修訂版)》時所作筆記,日后會持續更新后續章節筆記。歡迎各位大佬閱讀學習,如有疑問請及時聯系指正,希望對各位有所幫助,Thank you very much!