文章目录
- [第23章 库对数值和字符数据的支持](#第23章 库对数值和字符数据的支持)
-
- [23.1 <float.h>: 浮点类型的特性](#23.1 <float.h>: 浮点类型的特性)
- [23.2 <limits.h>: 整数类型的大小](#23.2 <limits.h>: 整数类型的大小)
- [23.3 <math.h>: 数学计算(C89)](#23.3 <math.h>: 数学计算(C89))
-
- [23.3.1 错误](#23.3.1 错误)
- [23.3.2 三角函数](#23.3.2 三角函数)
- [23.3.3 双曲函数](#23.3.3 双曲函数)
- [23.3.4 指数函数和对数函数](#23.3.4 指数函数和对数函数)
- [23.3.5 幂函数](#23.3.5 幂函数)
- [23.3.6 就近舍入、绝对值函数和取余函数](#23.3.6 就近舍入、绝对值函数和取余函数)
- [23.4 <math.h>: 数学计算(C99)](#23.4 <math.h>: 数学计算(C99))
-
- [23.4.1 IEEE浮点标准](#23.4.1 IEEE浮点标准)
- [23.4.2 类型](#23.4.2 类型)
- [23.4.3 宏](#23.4.3 宏)
- [23.4.4 错误](#23.4.4 错误)
- [23.4.5 函数](#23.4.5 函数)
- [23.4.6 分类宏](#23.4.6 分类宏)
- [23.4.7 三角函数](#23.4.7 三角函数)
- [23.4.8 双曲函数](#23.4.8 双曲函数)
- [23.4.9 指数函数和对数函数](#23.4.9 指数函数和对数函数)
- [23.4.10 幂函数和绝对值函数](#23.4.10 幂函数和绝对值函数)
- [23.4.11 误差函数和伽马函数](#23.4.11 误差函数和伽马函数)
- [23.4.12 就近舍入函数](#23.4.12 就近舍入函数)
- [23.4.13 取余函数](#23.4.13 取余函数)
- [23.4.14 操作函数](#23.4.14 操作函数)
- [23.4.15 最大值函数、最小值函数和正差函数](#23.4.15 最大值函数、最小值函数和正差函数)
- [23.4.16 浮点乘加](#23.4.16 浮点乘加)
- [23.4.17 比较宏](#23.4.17 比较宏)
- [23.5 <ctype.h>: 字符处理](#23.5 <ctype.h>: 字符处理)
-
- [23.5.1 字符分类函数](#23.5.1 字符分类函数)
- [23.5.2 字符大小写映射函数](#23.5.2 字符大小写映射函数)
- [23.6 <string.h>: 字符串处理](#23.6 <string.h>: 字符串处理)
-
- [23.6.1 复制函数](#23.6.1 复制函数)
- [23.6.2 拼接函数](#23.6.2 拼接函数)
- [23.6.3 比较函数](#23.6.3 比较函数)
- [23.6.4 搜索函数](#23.6.4 搜索函数)
- [23.6.5 其他函数](#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一样大的数,可以使用下面的预处理指令:
c
#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:
c
#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 三角函数
c
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函数会产生如下的结果:
c
cos(PI/4) ⇒ 0.707107
sin(PI/4) ⇒ 0.707107
tan(PI/4) ⇒ 1.0
//注意!!传递给 cos、sin和tan函数的实参是以弧度表示的,而不是以角度表示的。
acos、asin和atan函数分别用来计算反余弦、反正弦和反正切:
c
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 双曲函数
c
double cosh(double x);
double sinh(double x);
double tanh(double x);
cosh、sinh和tanh函数分别用来计算双曲余弦、双曲正弦和双曲正切:
c
cosh(0.5) ⇒ 1.12763
sinh(0.5) ⇒ 0.521095
tanh(0.5) ⇒ 0.462117
传递给cosh、sinh和tanh函数的实参必须以弧度表示,而不能以角度表示。
23.3.4 指数函数和对数函数
c
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的幂:
c
exp(3.0) ⇒ 20.0855
log函数是exp函数的逆运算,它计算以e为底的对数。log10计算"常用"(以10为底)对数:
c
log(20.0855) ⇒ 3.0
log10(1000) ⇒ 3.0
对于不以e或10为底的对数,计算起来也不复杂。例如,下面的函数对任意的x和b,计算以b为底x的对数:
c
double log_base(double x, double b)
{
return log(x) / log(b);
}
modf函数和frexp函数将一个double型的值拆解为两部分 。modf将它的第一个参数分为整数部分和小数部分,返回其中的小数部分,并将整数部分存入第二个参数所指向的对象中:
c
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存入第二个参数所指向的(整数)对象中:
c
frexp(12.0, &exp) ⇒ 0.75 //(exp被赋值为 4)
frexp(0.25, &exp) ⇒ 0.5 //(exp被赋值为-1)
ldexp函数会抵消frexp产生的结果,将小数部分和指数部分组合成一个数:
c
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 幂函数
c
double pow(double x, double y);
double sqrt(double x);
pow函数计算第一个参数的幂,幂的次数由第二个参数指定:
c
pow(3.0, 2.0) ⇒ 9.0
pow(3.0, 0.5) ⇒ 1.73205
pow(3.0, -3.0) ⇒ 0.037037
sqrt函数计算平方根:
c
sqrt(3.0) ⇒ 1.73205
由于通常sqrt函数比pow函数的运行速度快得多,因此使用sqrt计算平方根更好。
23.3.6 就近舍入、绝对值函数和取余函数
c
double ceil(double x);
double fabs(double x);
double floor(double x);
double fmod(double x, double y);
ceil函数返回一个double类型的值,这个值是大于或等于其参数的最小整数。floor函数则返回小于或等于其参数的最大整数:
c
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函数来实现一个这样的函数:
c
double round_nearest(double x)
{
return x < 0.0 ? ceil(x - 0.5) : floor(x + 0.5);
}
fabs函数计算参数的绝对值:
c
fabs(7.1) ⇒ 7.1
fabs(-7.1) ⇒ 7.1
fmod函数返回第一个参数除以第二个参数所得的余数:
c
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>了。但是,要了解C99的<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 分类宏
c
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 三角函数
c
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 双曲函数
c
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 指数函数和对数函数
c
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 幂函数和绝对值函数
c
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 误差函数和伽马函数
c
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 就近舍入函数
c
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 取余函数
c
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 操作函数
c
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 最大值函数、最小值函数和正差函数
c
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 浮点乘加
c
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函数是将它的前两个参数相乘再加上第三个参数。换句话说,我们可以将语句
c
a = b * c + d;
替换为
c
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指令来实现对紧缩的控制,用法如下:
c
#pragma STDC FP_CONTRACT 开关
开关的值可以是ON、OFF或DEFAULT。如果选择ON,编译器允许对表达式进行紧缩;如果选择OFF,编译器禁止对表达式进行紧缩;DEFAULT用于恢复默认设置(ON或OFF)。如果在程序的外层(所有函数定义的外部)使用该指令,该指令将持续有效,直到在同一个文件中遇到另一条包含FP_CONTRACT的#pragma指令或者到达文件末尾。如果在复合语句(包括函数体)中使用该指令,必须将其放在所有声明和语句之前;在到达复合语句的末尾之前,该指令都是有效的,除非被另一条#pragma覆盖。即便用FP_CONTRACT禁止了对表达式的自动紧缩,程序仍然可以调用fma执行显式的紧缩。
23.4.17 比较宏
c
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 字符分类函数
c
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 字符大小写映射函数
c
int tolower(int c);
int toupper(int c);
tolower函数返回与作为参数传递的字母相对应的小写字母,而toupper函数返回与作为参数传递的字母相对应的大写字母。对于这两个函数,如果所传参数不是字母,那么将返回原始字符,不加任何改变。
下面的程序对字符串
"aA0!"中的字符进行大小写转换:
c
/*
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 复制函数
c
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不保证当源和目的地相重叠时可以正常工作。
下面的例子展示了所有的复制函数,注释中给出了哪些字符会被复制:
c
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 拼接函数
c
char *strcat(char * restrict s1, const char * restrict s2);
char *strncat(char * restrict s1, const char * restrict s2, size_t n);
strcat函数将它的第二个参数追加到第一个参数的末尾 。两个参数都必须是以空字符结尾的字符串。strcat函数会在拼接后的字符串末尾添加空字符。考虑下面的例子:
c
char str[7] = "tea";
strcat(str, "bag"); /* adds b, a, g, \0 to end of str */
字母b会覆盖"tea"中字符a后面的空字符,因此现在str包含字符串"teabag"。strcat函数会返回它的第一个参数(指针)。
strncat函数与strcat函数基本一致,只是它的第三个参数会限制所复制字符的个数:
c
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的调用通常具有如下形式:
c
strncat(str1, str2, sizeof(str1) -- strlen(str1) -- 1);
第三个参数计算str1中剩余的空间大小(由表达式sizeof(str1) -- strlen(str1)给定),然后减1以确保给空字符留出空间。
23.6.3 比较函数
c
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的用法:
c
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函数通常会被调用两次:一次用于判断变换后字符串的长度,一次用来进行变换。下面是一个例子:
c
size_t len;
char *transformed;
len = strxfrm(NULL, original, 0);
transformed = malloc(len + 1);
strxfrm(transformed, original, len);
23.6.4 搜索函数
c
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:
c
char *p, str[] = "Form follows function.";
p = strchr(str, 'f'); /* finds first 'f' */
strchr函数会返回一个指针,这个指针指向str中出现的第一个f(即单词follows中的f)。如果需要多次搜索字符也很简单,例如,可以使用下面的调用搜索str中的第二个f(即单词function中的f):
c
p = strchr(p + 1, 'f'); /* finds next 'f' */
//如果不能定位所需的字符,strchr返回空指针。
memchr函数与strchr函数类似,但memchr函数会在搜索了指定数量的字符后停止搜索,而不是当遇到首个空字符时才停止 。memchr函数的第三个参数用来限制搜索时需要检测的字符总数。当不希望对整个字符串进行搜索或搜索的内存块不是以空字符结尾时,memchr函数会十分有用。下面的例子用memchr函数在一个没有以空字符结尾的字符数组中进行搜索:
c
char *p, str[22] = "Form follows function.";
p = memchr(str, 'f', sizeof(str));
与strchr函数类似,memchr函数也会返回一个指针指向该字符第一次出现的位置。如果找不到所需的字符,memchr函数返回空指针。
strrchr函数与strchr类似,但它会反向搜索字符:
c
char *p, str[] = "Form follows function.";
p = strrchr(str, 'f'); /* finds last 'f' */
在此例中,strrchr函数会首先找到字符串末尾的空字符,然后反向搜索字母f(单词function中的f)。与strchr和memchr一样,如果找不到指定的字符,strrchr函数也返回空指针。
strpbrk函数比strchr函数更通用,它返回一个指针,该指针指向第一个参数中与第二个参数中任意一个字符匹配的最左边一个字符:
c
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函数返回第一个属于该组字符的字符的下标。下面是使用这两个函数的例子:
c
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:
c
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 其他函数
c
void *memset(void *s, int c, size_t n);
size_t strlen(const char *s);
memset函数会将一个字符的多个副本存储到指定的内存区域 。假设p指向一块N字节的内存,调用
c
memset(p, ' ', N);
会在这块内存的每个字节中存储一个空格。memset函数的一个用途是将数组全部初始化为0:
c
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!