本文主要是介绍C现代方法(第23章)笔记——库对数值和字符数据的支持,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 第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∫0xe−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!
这篇关于C现代方法(第23章)笔记——库对数值和字符数据的支持的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!