C现代方法(第26章)笔记——<stdarg.h>、<stdlib.h>和<time.h>标准库
第26章 <stdarg.h>、<stdlib.h>和<time.h>标准库
——确定程序参数的应该是用户,而不应该是它们的创造者。
<stdarg.h>
、<stdlib.h>
和<time.h>
(前面几章中未讨论过的C89
头只有这三个了)不同于标准库中的其他头。<stdarg.h>头(26.1节)
可使编写的函数带有可变数量的参数,<stdlib.h>头(26.2节)
是一类不适合放在其他库中的函数,<time.h>头(26.3节)
允许程序处理日期和时间。
26.1 <stdarg.h>: 可变参数
类型 va_arg(va_list ap, 类型);
void va_copy(va_list dest, va_list src);
void va_end(va_list ap);
void va_start(va_list ap, parmN);
printf
和scanf
这样的函数具有一个不同寻常的性质:它们允许任意数量的参数。而且,这种能处理可变数量的参数的能力并不仅限于库函数。<stdarg.h>
头提供的工具使我们能够自己编写带有变长参数列表的函数。<stdarg.h>
声明了一种类型(va_list
)并定义了几个宏。C89
中一共有三个宏,分别名为va_satrt
、va_arg
和va_end
。C99
增加了一个类似函数的宏va_copy
。
为了了解这些宏的工作原理,这里将用它们来编写一个名为
max_int
的函数。此函数用来在任意数量的整数参数中找出最大数。下面是此函数的调用过程:
max_int(3, 10, 30, 20)
函数的第一个实参指明后面有几个参数。这里的max_int
函数调用将返回30
(即10
、30
和20
中的最大数)。
下面是
max_int
函数的定义:
int max_int(int n, ...) /* n must be at least 1 */
{
va_list ap;
int i, current, largest;
va_start(ap, n);
largest = va_arg(ap, int);
for (i = 1; i < n; i++) {
current = va_arg(ap, int);
if (current > largest)
largest = current;
}
va_end(ap);
return largest;
}
形式参数列表中的...
符号(省略号)表示参数n
后面有可变数量的参数。
max_int
函数体从声明va_list
类型的变量开始:
va_list ap;
//为了使max_int函数可以访问到跟在n后边的实参,必须声明这样的变量。
语句va_start(ap, n);
指出了参数列表中可变长度部分开始的位置(这里从n
后边开始)。带有可变数量参数的函数必须至少有一个“正常的”形式参数;省略号总是出现在形式参数列表的末尾,在最后一个正常参数的后边。
语句largest=va_arg(ap,int);
获取max_int
函数的第二个参数(n
后面的那个)并将其赋值给变量largest
,然后自动前进到下一个参数处。语句中的单词int
表明我们希望max_int
函数的第二个实参是int
类型的。当程序执行内部循环时,语句current=va_arg(ap,int);
会逐个获取max_int
函数余下的参数。
请注意!!不要忘记在获取当前参数后,宏
va_arg
始终会前进到下一个参数的位置上。正是由于这个特点,这里不能用如下方式编写max_int
函数的循环:for (i = 1; i < n; i++) if (va_arg(ap, int) > largest) /*** WRONG ***/ largest = va_arg(ap, int);
在函数返回之前,要求用语句va_end(ap)
;进行“清理”。(如果不返回,函数可以调用va_start
并且再次遍历参数列表。)
va_copy
宏把src
(va_list
类型的值)复制到dest
(也是va_list
类型的值)中。va_copy
之所以能起作用,是因为在把src
复制到dest
之前可能已经多次用src
来调用va_arg
了。调用va_copy
可以使函数记住在参数列表中的位置,从而以后可以回到同一位置继续处理相应的参数(及其后面的参数)。
每次调用va_start
或va_copy
时都必须与va_end
成对使用,而且这些成对的调用必须在同一个函数中。所有的va_arg
调用必须出现在va_start
(或va_copy
)与配对的va_end
调用之间。
请注意!!当调用带有可变参数列表的函数时,编译器会在省略号对应的所有参数上执行
默认实参提升(9.3节)
。特别地,char
类型和short
类型的参数会被提升为int
类型,float
类型的值会被提升为double
类型。因此把char
、short
或float
类型的值作为参数传递给va_arg
是没有意义的,(提升后的)参数不可能具有这些类型。
26.1.1 调用带有可变参数列表的函数
调用带有可变参数列表的函数存在固有的风险。早在第
3
章我们就认识到,给printf
函数和scanf
函数传递错误的参数是很危险的。其他带有可变参数列表的函数也同样很敏感。主要的难点在于,带有可变参数列表的函数无法确定参数的数量和类型。这一信息必须被传递给函数或者由函数来假定。示例中的max_int
函数依靠第一个参数来指明后面有多少参数,并且它假定参数都是int
类型的。而像printf
和scanf
这样的函数则是依靠格式串来描述其他参数的数量以及每个参数的类型。
另外一个问题是关于以NULL
作为参数的。NULL
通常用于表示0
。当把0
作为参数传递给带有可变参数列表的函数时,编译器会假定它表示一个整数——无法用于表示空指针。解决这一问题的方法就是添加一个强制类型转换,用(void*)NULL
或(void*)0
来代替NULL
。(关于这一点的更多讨论见第17
章末尾的“问与答”
部分。)
26.1.2 v…printf函数
int vfprintf(FILE * restrict stream,
const char * restrict format,
va_list arg); //来自<stdio.h>
int vprintf(const char * restrict format,
va_list arg); //来自<stdio.h>
int vsnprintf(char * restrict s, size_t n,
const char * restrict format,
va_list arg); //来自<stdio.h>
int vsprintf(char * restrict s,
const char * restrict format,
va_list arg); //来自<stdio.h>
vfprintf
、vprintf
和vsprintf
函数(即v...printf
函数)都属于<stdio.h>
。这些函数放在本节讨论,是因为它们总是和<stdarg.h>
中的宏联合使用。C99
增加了vsnprintf
函数。
v...printf
函数和fprintf
、printf
以及sprinf
函数密切相关。但是,不同于这些函数的是,v...printf
函数具有固定数量的参数。每个v...printf
函数的最后一个参数都是一个va_list
类型的值,这表明v...printf
函数将由带有可变参数列表的函数调用。实际上,v...printf
函数主要用于编写具有可变数量的参数的“包装”函数,包装函数会把参数传递给v...printf
函数。
举一个例子,假设程序需要不时地显示出错消息,而且我们希望每条消息都以下列格式的前缀开始:
** Error n:
这里的n
在显示第一条出错消息时是1
,以后每显示一条出错消息就增加1
。为了使产生出错消息更加容易,我们将编写一个名为errorf
的函数。此函数类似于printf
函数,但它总在输出的开始处添加**Errorn:
,并且总是向stderr
而不是向stdout
输出。errorf
函数将调用vfprintf
函数来完成大部分的实际输出工作。
下面是
errorf
函数可能的写法:
int errorf(const char *format, ...)
{
static int num_errors = 0;
int n;
va_list ap;
num_errors++;
fprintf(stderr, "** Error |%d: ", num_errors);
va_start(ap, format);
n = vfprintf(stderr, format, ap);
va_end(ap);
fprintf(stderr, "\n");
return n;
}
包装函数(本例中是errorf
)需要在调用v...printf
函数之前调用va_start
,并在v...printf
函数返回后调用va_end
。在调用v...printf
函数之前,包装函数可以对va_arg
调用一次或多次。
C99
版本的<stdio.h>
中新增了vsnprintf
函数,该函数与snprintf
函数(22.8节
讨论过)相对应。snprintf
也是C99
新增的函数。
26.1.3 v…scanf函数(C99)
int vfscanf(FILE * restrict stream,
const char * restrict format,
va_list arg); //来自<stdio.h>
int vscanf(const char * restrict format,
va_list arg); //来自<stdio.h>
int vsscanf(const char * restrict s,
const char * restrict format,
va_list arg); //来自<stdio.h>
C99
在<stdio.h>
中增加了一组“v...scanf函数”
。vfscanf
、vscanf
和vsscanf
分别与fscanf
、scanf
和sscanf
等价,区别在于前者具有一个va_list
类型的参数用于接受可变参数列表。与v...printf
函数一样,v...scanf
函数也主要用于具有可变数量参数的包装函数。包装函数需要在调用v...scanf
函数之前调用va_start
,并在v...scanf
函数返回后调用va_end
。
26.2 <stdlib.h>: 通用的实用工具
<stdlib.h>
涵盖了全部不适合于其他头的函数。<stdlib.h>
中的函数可以分为以下8
组:
- 数值转换函数;
- 伪随机序列生成函数;
- 内存管理函数;
- 与外部环境的通信;
- 搜索和排序工具;
- 整数算术运算函数;
- 多字节/宽字符转换函数;
- 多字节/宽字符串转换函数。
下面将逐个介绍每组函数,但是有三组例外:内存管理函数
、多字节/宽字符转换函数
以及多字节/宽字符串转换函数
。
内存管理函数(即
malloc
、calloc
、realloc
和free
)允许程序分配内存块,以后再释放或者改变内存块的大小。第17
章已经详细描述了这4
个函数。
多字节/宽字符转换函数用于把多字节字符转换为宽字符或执行反向转换。多字节/宽字符串转换函数在多字节字符串与宽字符串之间执行类似的转换。这两组函数都在25.2节
讨论过。
26.2.1 数值转换函数
double atof(const char *nptr);
int atoi(const char *nptr);
long int atol(const char *nptr);
long long int atoll(const char *nptr);
double strtod(const char * restrict nptr, char ** restrict endptr);
float strtof(const char * restrict nptr, char ** restrict endptr);
long double strtold(const char * restrict nptr, char ** restrict endptr);
long int strtol(const char * restrict nptr, char ** restrict endptr, int base);
long long int strtoll(const char * restrict nptr,
char ** restrict endptr, int base);
unsigned long int strtoul(
const char * restrict nptr,
char ** restrict endptr, int base);
unsigned long long int strtoull(
const char * restrict nptr,
char ** restrict endptr, int base);
数值转换函数(C89
中称为“字符串转换函数”
)会把含有数值的字符串从字符格式转换成等价的数值。这些函数中有3
个函数是非常旧的,另外有3
个函数是在创建C89
标准时添加的,其余的5
个函数是C99
新增的。
所有的数值转换函数(不论新旧)的工作原理都差不多。每个函数都试图把(
nptr
参数指向的)字符串转换为数。每个函数都会跳过字符串开始处的空白字符,并且把后续字符看作数(可能以加号或减号开头)的一部分,而且还会在遇到第一个不属于数的字符处停止。此外,如果不能执行转换(字符串为空,或者前导空白之后的字符的形式不符合函数的要求),每个函数都会返回0
。
旧函数(atof
、atoi
和atol
)把字符串分别转换成double
、int
或者long int
类型值。不过,这些函数不能指出转换过程中处理了字符串中的多少字符,也不能指出转换失败的情况。[这些函数的一些实现可以在转换失败时修改errno变量(24.2节)
,但不能保证会这么做。]
C89
中的函数(strtod
、strtol
和strtoul
)更复杂一些。首先,它们会通过修改endptr
指向的变量来指出转换停止的位置。(如果不在乎转换结束的位置,那么函数的第二个参数可以为空指针。)为了检测函数是否可以对整个字符串完成转换,只需检测此变量是否指向空字符。如果不能进行转换,将把nptr
的值赋给endptr
指向的变量(前提是endptr
不是空指针)。此外,strtol
和strtoul
还有一个base
参数用来说明待转换数的基数。基数在2~36
范围内都可以(包括2
和36
)。
除了比原来的旧函数更通用以外,strtod
、strtol
和strtoul
函数还更善于检测错误。如果转换得到的值超出了函数返回类型的表示范围,那么每个函数都会在errno
变量中存储ERANGE
。此外,strtod
函数返回正的或负的HUGE_VAL(23.3节)
,strtol
函数和strtoul
函数返回相应返回类型的最小值或最大值。(strtol
返回LONG_MIN
或LONG_MAX
,strtoul
返回ULONG_MAX
。)
C99
增加了函数atoll
、strtof
、strtold
、strtoll
和strtoull
。atoll
与atol
类似,区别在于前者把字符串转换为long long int
类型的值。strtof
和strtold
与strtod
类似,区别在于前两者分别把字符串转换为float
和long double
类型的值。strtoll
与strtol
类似,区别在于前者把字符串转换为long long int
类型的值。strtoull
与strtoul
类似,区别在于前者把字符串转换为unsigned long long int
类型的值。C99
还对浮点数值转换函数做了一些小的改动:传递给strtod
(以及strtof
和strtold
)的字符串可以包含十六进制的浮点数、无穷数或NaN
。
26.2.1.1 测试数值转换函数
下面这个程序通过应用
C89
中的6
个数值转换函数中的每一个来把字符串转换为数值格式。在调用了strtod
、strtol
和stroul
函数之后,程序还会显示出是否每种转换都产生了有效的结果,以及是否每种转换可以对整个字符串完成转换。程序将从命令行中获得输入字符串。
/*
tnumconv.c
--Tests C89 numeric conversion funct
*/
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#define CHK_VALID printf(" |%s |%s\n",\
errno != ERANGE ? "Yes" : "No ",\
*ptr == '\0' ? "Yes" : "No")
int main(int argc, char *argv[])
{
char *ptr;
if (argc != 2) {
printf("usage: tnumconv string\n");
exit(EXIT_FAILURE);
}
printf("Function Return Value\n");
printf("-------- ------------\n");
printf("atof |%g\n", atof(argv[1]));
printf("atoi |%d\n", atoi(argv[1]));
printf("atol |%ld\n\n", atol(argv[1]));
printf("Function Return Value Valid? "
"String Consumed?\n"
"-------- ------------ ------ "
"----------------\n");
errno = 0;
printf("strtod |%-12g", strtod(argv[1], &ptr));
CHK_VALID;
errno = 0;
printf("strtol |%-12ld", strtol(argv[1], &ptr, 10));
CHK_VALID;
errno = 0;
printf("strtoul |%-12lu", strtoul(argv[1], &ptr, 10));
CHK_VALID;
return 0;
}
如果
3000000000
是命令行参数,那么程序的输出可能如下:
Function Return Value
-------- ------------
atof 3e+09
atoi 2147483647
atol 2147483647
Function Return Value Valid? String Consumed?
-------- ------------ ------ ----------------
strtod 3e+09 Yes Yes
strtol 2147483647 No Yes
strtoul 3000000000 Yes Yes
虽然3000000000
是有效的无符号长整数,但它对许多机器而言都太长了,以至于无法表示为长整数。atoi
函数和atol
函数无法指出参数所表示的数值越界。在给出的输出中,它们都返回2147483647
(最大的长整数),但C
标准不能保证总会如此。strtoul
函数能够正确地执行转换,而strtol
函数则会返回2147483647
(标准要求它返回最大的长整数)并且把ERANGE
存储到errno
中。
如果命令行参数是
123.456
,那么输出将是:
Function Return Value
-------- ------------
atof 123.456
atoi 123
atol 123
Function Return Value Valid? String Consumed?
-------- ------------ ------ ----------------
strtod 123.456 Yes Yes
strtol 123 Yes No
strtoul 123 Yes No
所有这6
个函数都会把这个字符串看作有效的数,但是整数函数会在小数点处停止。strtol
函数和strtoul
函数可以指出它们没有能够对整个字符串完成转换。
如果命令行参数是
foo
,那么输出将是:
Function Return Value
-------- ------------
atof 0
atoi 0
atol 0
Function Return Value Valid? String Consumed?
-------- ------------ ------ ----------------
strtod 0 Yes No
strtol 0 Yes No
strtoul 0 Yes No
所有函数看到字母f
都会立刻返回0
。str...
函数不会改变errno
,但是从函数没有处理字符串这一事实可以知道一定出错了。
26.2.2 伪随机序列生成函数
int rand(void);
void srand(unsigned int seed);
rand
函数和srand
函数都可以用来生成伪随机数。这两个函数用于模拟程序和玩游戏程序(例如,在纸牌游戏中用来模拟骰子滚动或者发牌)。
每次调用
rand
函数时,它都会返回一个0~RAND_MAX
(定义在<stdlib.h>
中的宏)的数。rand
函数返回的数事实上不是随机的,这些数是由“种子”
值产生的。但是,对于偶然的观察者而言,rand
函数似乎能够产生不相关的数值序列。
调用srand
函数可以为rand
函数提供种子值。如果在srand
函数之前调用rand
函数,那么会把种子值设定为1
。每个种子值确定了一个特定的伪随机序列。srand
函数允许用户选择自己想要的序列。
始终使用同一个种子值的程序总会从
rand
函数得到相同的数值序列。这个性质有时是非常有用的:程序在每次运行时按照相同的方式运行,这样会使测试更加容易。但是,用户通常希望每次程序运行时rand
函数都能产生不同的序列。(玩纸牌的程序如果总是发同样的牌,估计就没人玩了。)使种子值“随机化”的最简单方法就是调用time函数(26.3节)
,它会返回一个对当前日期和时间进行编码的数。把time
函数的返回值传递给srand
函数,这样可以使rand
函数在每次运行时的行为都不相同。这种方法的示例见10.2节
中的guess.c
程序和guess2.c
程序。
下面这个程序首先显示由
rand
函数返回的前5
个值,然后让用户选择新的种子值。此过程会反复执行直到用户输入零作为种子值为止。
/*
trand.c
--Tests the pseudo-random sequence generation functions
*/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int i, seed;
printf("This program displays the first five values of "
"rand.\n");
for (;;) {
for (i = 0; i < 5; i++)
printf("|%d ", rand());
printf("\n\n");
printf("Enter new seed value (0 to terminate): ");
scanf("|%d", &seed);
if (seed == 0)
break;
srand(seed);
}
return 0;
}
下面给出了可能的程序会话:
This program displays the first five values of rand.
1804289383 846930886 1681692777 1714636915 1957747793
Enter new seed value (0 to terminate): 100
677741240 611911301 516687479 1039653884 807009856
Enter new seed value (0 to terminate): 1
1804289383 846930886 1681692777 1714636915 1957747793
Enter new seed value (0 to terminate): 0
编写rand
函数的方法有很多,所以这里不保证每种rand
函数的版本都能生成这些数。注意,选择1
作为种子值与不指定种子值所得到的数列相同。
26.2.3 与环境的通信
_Noreturn void abort(void);
int atexit(void (*func)(void));
_Noreturn int at_quick_exit(void (* func) (void));
_Noreturn void exit(int status);
_Noreturn void _Exit(int status);
_Noreturn void quick_exit(int status);
char *getenv(const char *name);
int system(const char *string);
这一组函数提供了简单的操作系统接口。它们允许程序:
(1)
正常或不正常地终止,并且向操作系统返回一个状态码;(2)
从用户的外部环境获取信息;(3)
执行操作系统的命令。
其中_Exit
是C99
新增的;at_quick_exit
和quick_exit
是C11
新增的。尤其需要注意的是,从C11
开始,为那些不返回的函数添加了函数指定符_Noreturn
。
在程序中的任何位置执行
exit(n)
调用通常等价于在main
函数中执行return n;
语句:程序终止,并且把n
作为状态码返回给操作系统。<stdlib.h>
定义了宏EXIT_FAILURE
和宏EXIT_SUCCESS
,这些宏可以用作exit
函数的参数。exit
函数仅有的另一个可移植参数是0
,它和宏EXIT_SUCCESS
意义相同。返回除这些以外的其他状态码也是合法的,但是不一定对所有操作系统都可移植。
程序终止时,它通常还会在后台执行一些最后的动作,包括清洗包含未输出数据的输出缓冲区,关闭打开的流,以及删除临时文件。我们也可以定义其他希望程序终止时执行的“清理”操作。atexit
函数允许用户“注册”在程序终止时要调用的函数。例如,为了注册名为cleanup
的函数,可以用如下方式调用atexit
函数:
atexit(cleanup);
当把函数指针传递给atexit
函数时,它会把指针保存起来留给将来引用。以后当程序(通过exit
函数调用或main
函数中的return
语句)正常终止时,atexit
注册的函数都会被自动调用。(如果注册了两个或更多的函数,那么将按照与注册顺序相反的顺序调用它们。)
_Exit
函数类似于exit
函数,但是_Exit
不会调用atexit
注册的函数,也不会调用之前传递给signal函数(24.3节)
的信号处理函数。此外,_Exit
函数不需要清洗输出缓冲区,关闭打开的流,以及删除临时文件,是否会执行这些操作是由实现定义的。
abort
函数也类似于exit
函数,但调用它会导致异常的程序终止。atexit
函数注册的函数不会被调用。根据具体的实现,它可能不会清洗包含未输出数据的输出缓冲区,不会关闭打开的流,也不会删除临时文件abort
函数返回一个由实现定义的状态码来指出“不成功的终止”。
quick_exit
使程序正常终止,但不会调用那些用atexit
和signal
注册的函数。它首先按照和注册时相反的顺序调用那些用at_quick_exit
注册的函数,然后调用_Exit
函数。
at_quick_exit
注册由参数func
指向的函数,这些函数在用quick_exit
函数快速终止程序时调用。当前的标准至少支持注册32
个函数。如果注册成功,该函数返回0
;失败返回非零值。
许多操作系统都会提供一个“环境”,即一组描述用户特性的字符串。这些字符串通常包含用户运行程序时要搜索的路径、用户终端的类型(多用户系统的情况)等。例如,UNIX
系统的搜索路径可能如下所示:
PATH=/usr/local/bin:/bin:/usr/bin:.
getenv
函数提供了访问用户环境中的任意字符串的功能。例如,为了找到PATH
字符串的当前值,可以这样写:
char *p = getenv("PATH");
p
现在指向字符串"/usr/local/bin:/bin:/usr/bin:."
。留心getenv
函数:它返回一个指向静态分配的字符串的指针,该字符串可能会被后续的getenv
函数调用改变。
system
函数允许C
程序运行另一个程序(可能是一个操作系统命令)。system
函数的参数是包含命令的字符串,类似于我们在操作系统提示下输入的内容。例如,假设正在编写的程序需要当前目录中的文件列表。UNIX
程序将按照下列方式调用system
函数:
system("ls >myfiles");
这会调用UNIX
的ls
命令,并要求其把当前目录下的文件列表写入名为myfiles
的文件中。
system
函数的返回值是由实现定义的。通常情况下,system
函数会返回要求它运行的那个程序的终止状态码,测试这个返回值可以检测程序是否正常工作。以空指针作为参数调用system
函数有特殊的含义:如果命令处理程序是有效的,那么函数会返回非零值。
26.2.4 搜索和排序实用工具
void *bsearch(const void *key, const void *base,
size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
bsearch
函数在有序数组中搜索一个特定的值(键)。当调用bsearch
函数时,形式参数key
指向键,base
指向数组,nmemb
是数组中元素的数量,size
是每个元素的大小(按字节计算),而compar
是指向比较函数的指针。比较函数类似于qsort
函数所需的函数:当(按顺序)把指向键的指针和指向数组元素的指针传递给比较函数时,函数必须根据键是小于、等于还是大于数组元素而分别返回负整数、零或正整数。bsearch
函数返回一个指向与键匹配的元素的指针;如果找不到匹配的元素,那么bsearch
函数会返回一个空指针。
虽然C
标准不要求,但是bsearch
函数通常会使用二分搜索算法来搜索数组。bsearch
函数首先把键与数组的中间元素进行比较。如果相匹配,那么函数就返回。如果键小于数组的中间元素,那么bsearch
函数将把搜索限制在数组的前半部分。如果键大于数组的中间元素,那么bsearch
函数只搜索数组的后半部分。bsearch
函数会重复这种方法直到它找到键或者没有元素可搜索。这种方法使bsearch
运行起来很快——搜索有1000
个元素的数组最多只需进行10
次比较。搜索有1000000
个元素的数组需要的比较次数不超过20
。
17.7节
讨论了可以对任何数组进行排序的qsort
函数。bsearch
函数只能用于有序数组,但我们总可以在用bsearch
函数搜索数组之前先用qsort
函数对其进行排序。
下面的程序用来计算从纽约到不同的国际城市之间的航空里程。程序首先要求用户输入城市的名称,然后显示从纽约到这一城市的里程:
Enter city name: Shanghai
Shanghai is 7371 miles from New York City.
程序将把城市/里程数据对存储在数组中。通过使用bsearch
函数在数组中搜索城市名,程序可以很容易地找到相应的里程数。
/*
airmiles.c
--Determines air mileage from New York to other cities
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct city_info {
char *city;
int miles;
};
int compare_cities(const void *key_ptr,
const void *element_ptr);
int main(void)
{
char city_name[81];
struct city_info *ptr;
const struct city_info mileage[] =
{{"Berlin", 3965}, {"Buenos Aires", 5297},
{"Cairo", 5602}, {"Calcutta", 7918},
{"Cape Town", 7764}, {"Caracas", 2132},
{"Chicago", 713}, {"Honolulu", 4964},
{"Istanbul", 4975}, {"Lisbon", 3364},
{"London", 3458}, {"Los Angeles", 2451},
{"Manila", 8498}, {"Mexico City", 2094},
{"Montreal", 320}, {"Moscow", 4665},
{"Paris", 3624}, {"Rio de Janeiro", 4817},
{"Rome", 4281}, {"San Francisco", 2571},
{"Shanghai", 7371}, {"Stockholm", 3924},
{"Sydney", 9933}, {"Tokyo", 6740},
{"Warsaw", 4344}, {"Washington", 205}};
printf("Enter city name: ");
scanf("%80[^\n]", city_name);
ptr = bsearch(city_name, mileage,
sizeof(mileage) / sizeof(mileage[0]),
sizeof(mileage[0]), compare_cities);
if (ptr != NULL)
printf("%s is %d miles from New York City.\n", city_name, ptr->miles);
else
printf("%s wasn’t found.\n", city_name);
return 0;
}
int compare_cities(const void *key_ptr, const void *element_ptr)
{
return strcmp((char *) key_ptr, ((struct city_info *) element_ptr)->city);
}
26.2.5 整数算术运算函数
int abs(int j);
long int labs(long int j);
long long int llabs(long long int j);
div_t div(int numer, int denom);
ldiv_t ldiv(long int numer, long int denom);
lldiv_t lldiv(long long int number, long long int denom);
abs
函数返回int
类型值的绝对值,labs
函数返回long int
类型值的绝对值。
div
函数用第一个参数除以第二个参数,并且返回一个div_t
类型值。div_t
是一个含有商成员(命名为quot
)和余数成员(命名为rem
)的结构。例如,如果ans
是div_t
类型的变量,那么可以写出下列语句:
ans = div(5, 2);
printf("Quotient: %d Remainder: %d\n", ans.quot, ans.rem);
ldiv
函数和div
函数很类似,但用于处理长整数。ldiv
函数返回ldiv_t
类型的结构,该结构也包含quot
和rem
两个成员。(div_t
类型和ldiv_t
类型在<stdlib.h>
中声明。)
C99
提供了两个新函数。llabs
函数返回long long int
类型值的绝对值。lldiv
类似于div
和ldiv
,区别在于它把两个long long int
类型的值相除,并返回lldiv_t
类型的结构。(lldiv_t
类型也是C99
新增的。)
26.2.6 地址对齐的内存分配(C1X)
void * aligned_alloc(size_t alignment, size_t size);
aligned_alloc
函数为对象分配存储空间,空间的位置必须符合参数alignment
指定的对齐要求,空间的大小由参数size
指定。如果alignment
指定了当前平台不支持的无效对齐要求,则该函数执行失败并返回空指针。
下面的语句要求分配
80
字节的空间,而且必须起始于能被8
整除的内存地址:
if ((ptr = aligned_alloc(8, 80)) == NULL)
printf("Aligned allocation failed.\n");
26.3 <time.h>: 日期和时间
<time.h>
提供了用于确定时间(包括日期)、对时间值进行算术运算以及为了显示而对时间进行格式化的函数。在介绍这些函数之前,我们先讨论一下时间是如何存储的。<time.h>
提供了4
种类型,每种类型表示一种存储时间的方法。
-
clock_t
:按照“时钟嘀嗒”进行度量的时间值。 -
time_t
:紧凑的时间和日期编码(日历时间)。 -
struct tm
:把时间分解成秒、分、时等。struct tm
类型的值通常称为分解时间。表26-1
给出了tm
结构的成员,所有成员都是int
类型的。表26-1
tm
结构的成员名称 描述 最小值 最大值 tm_sec 分钟后边的秒 0 61① tm_min 小时后边的分钟 0 59 tm_hour 从午夜开始计算的小时 0 23 tm_mday 月内的第几天 1 31 tm_mon 一月以来的月数 0 11 tm_year 1900年以来的年数 0 — tm_wday 星期日以来的天数 0 6 tm_yday 1月1日以来的天数 0 365 tm_isdst 夏令时标志 ② ② ①允许两个额外的“闰秒”。
C99
中最大值为60
。②如果夏令时有效,就为正数;如果无效,就为零;如果这一信息未知,就为负数。
-
struct timespec
:这是从C11
开始新增的结构类型,用来保存一个用秒和纳秒来指定的时间间隔,可用于描述一个基于特定时期的日历时间。表26-2
给出了这种结构类型的成员。表26-2
struct timespec
结构的成员名称 描述 最小值 最大值 tv_sec 完整的秒数 0 取决于实现 tv_nsec 纳秒 0 999 999 999
这些类型用于不同的目的。
clock_t
类型的值只能表示时间区间。而time_t
类型的值、struct tm
类型的值和struct timespec
类型的值则可以存储完整的日期和时间。time_t
类型的值是紧密编码的,所以它们占用的空间很少。struct tm
和struct timespec
类型的值需要的空间大得多,但是这类值通常易于使用。C
标准规定clock_t
和time_t
必须是“算术运算类型”,但没有细说。我们甚至不知道clock_t
值和time_t
值是作为整数存储还是作为浮点数存储的。
现在来看看<time.h>
中的函数。这些函数分为两组:时间处理函数
和时间转换函数
。
26.3.1 时间处理函数
clock_t clock(void);
double difftime(time_t time1, time_t time0);
time_t mktime(struct tm *timeptr);
time_t time(time_t *timer);
int timespec_get (struct timespec * ts, int base);
clock
函数返回一个clock_t
类型的值,这个值表示程序从开始执行到当前时刻的处理器时间。为了把这个值转换为秒,将其除以CLOCKS_PER_SEC
(<time.h>
中定义的宏)。
当用
clock
函数来确定程序已运行多长时间时,习惯做法是调用clock
函数两次:一次在main
函数开始处,另一次在程序就要终止之前。
#include <stdio.h>
#include <time.h>
int main(void)
{
clock_t start_clock = clock();
...
printf("Processor time used: %g sec.\n",
(clock() – start_clock) / (double) CLOCKS_PER_SEC);
return 0;
}
初始调用clock
函数的理由是,由于有隐藏的“启动”代码,程序在到达main
函数之前会使用一些处理器时间。在main
函数开始处调用clock
函数可以确定启动代码需要多长时间,以后可以减去这部分时间。
C89
标准只提到clock_t
是算术运算类型,没有说明宏CLOCKS_PER_SEC
的类型。因此,表达式
(clock() – start_clock) / CLOCKS_PER_SEC
的类型可能会因具体实现的不同而不同,这样就很难用printf
函数来显示其内容。为了解决这个问题,我们在示例中把宏CLOCKS_PER_SEC
转换成double
类型,从而使整个表达式具有double
类型。C99
把CLOCKS_PER_SEC
的类型指定为clock_t
,但clock_t
仍然是由实现定义的类型。
time
函数返回当前的日历时间。如果实参不是空指针,那么time
函数还会把日历时间存储在实参指向的对象中。time
函数以两种不同方式返回时间有其历史原因,不过这也为用户提供了两种书写的选择,既可以用
cur_time = time(NULL);
//也可以用
time(&cur_time);
//这里的cur_time是time_t类型的变量。
difftime
函数返回time0
(较早的时间)和time1
之间按秒衡量的差值。因此,为了计算程序的实际运行时间(不是处理器时间),可以采用下列代码:
#include <stdio.h>
#include <time.h>
int main(void)
{
time_t start_time = time(NULL);
...
printf("Running time: %g sec.\n", difftime(time(NULL), start_time));
return 0;
}
mktime
函数把分解时间(存储在函数参数指向的结构中)转换为日历时间,然后返回该日历时间。作为副作用,mktime
函数会根据下列规则调整结构的成员。
mktime
函数会改变值不在合法范围(见表26-1
)内的所有成员,这样的改变可能会进一步要求改变其他成员。例如,如果tm_sec
过大,那么mktime
函数会把它减少到合适的范围内(0~59
),并且会把额外的分钟数加到tm_min
上。如果现在tm_min
过大,那么mktime
函数会减少tm_min
,同时把额外的小时数加到tm_hour
上。如果必要,此过程还将继续对成员tm_mday
、tm_mon
和tm_year
进行操作。- 在调整完结构的其他成员后(如果必要),
mktime
函数会给tm_wday
(一星期的第几天)和tm_yday
(一年的第几天)设置正确的值。在调用mktime
函数之前,从来不需要对tm_wday
和tm_yday
的值进行任何初始化,因为mktime
函数会忽略这些成员的初始值。
mktime
函数调整tm
结构成员的能力对于和时间相关的算术计算非常有用。例如,现在用mktime
函数来回答下面这个问题:如果2012
年的奥林匹克运动会从7
月27
日开始,并且历时16
天,那么结束的日期是哪天?我们首先把日期2012
年7
月27
日存储到tm
结构中:
struct tm t;
t.tm_mday = 27;
t.tm_mon = 6; /* July */
t.tm_year 112; /* 2012 */
我们还要对结构的其他成员进行初始化(成员tm_wday
和tm_yday
除外),以确保它们不包含可能影响结果的未定义的值:
t.tm_sec = 0;
t.tm_min = 0;
t.tm_hour = 0;
t.tm_isdst = -1;
接下来,给成员tm_mday
加上16
:
t.tm_mday += 16;
这样就使成员tm_mday
变成了43
,这个值超出了这一成员的取值范围。调用mktime
函数可以使该结构的这一成员恢复到正确的取值范围内:
mktime(&t);
这里将舍弃
mktime
函数的返回值,因为我们只对函数在t
上的效果感兴趣。现在,t
的相关成员具有如表26-3
所示的值:
表26-3 t
的相关成员值及其对应含义
成员 | 值 | 含义 |
---|---|---|
tm_mday | 12 | 12日 |
tm_mon | 7 | 8月 |
tm_year | 112 | 2012年 |
tm_wday | 0 | 星期日 |
tm_yday | 224 | 这一年的第225天 |
从C11
开始新增了一个时间处理函数timespec_get
。该函数将参数ts
所指向的对象设置为基于指定基准时间的日历时间。
如果传递给
base
的参数是TIME-UTC
[这是从C11
开始,头<time.h>
中定义的宏,用来表示以世界协调时间(UTC)
为基准],那么,tv_sec
成员被设置为自C
实现定义的某个时期以来所经历的秒数;tv_nsec
成员被设置为纳秒数,按系统时钟的分辨率进行舍入。该函数执行成功后返回值是传入的base
(非零值);否则返回0
。
26.3.2 时间转换函数
char *asctime(const struct tm *timeptr);
char *ctime(const time_t *timer);
struct tm *gmtime(const time_t *timer);
struct tm *localtime(const time_t *timer);
size_t strftime(char * restrict s, size_t maxsize,
const char * restrict format,
const struct tm * restrict timeptr);
时间转换函数可以把日历时间转换成分解时间,还可以把时间(日历时间或分解时间)转换成字符串格式。下图说明了这些函数之间的关联关系:
图中包含了mktime
函数。C
标准把此函数划分为“处理”函数而不是“转换”函数。
gmtime
函数和localtime
函数很类似。当传递指向日历时间的指针时,这两种函数都会返回一个指向结构的指针,该结构含有等价的分解时间。localtime
函数会产生本地时间,而gmtime
函数的返回值则是用UTC表示的。gmtime
函数和localtime
函数的返回值指向一个静态分配的结构,该结构可以被后续的gmtime
或localtime
调用修改。
asctime
(ASCII
时间)函数返回一个指向以空字符结尾的字符串的指针,字符串的格式如下:
Sun Jun 3 17:48:34 2007\n
此字符串由函数参数所指向的分解时间
构成。
ctime
函数返回一个指向描述本地时间的字符串的指针。如果cur_time
是time_t
类型的变量,那么调用
ctime(&cur_time)
就等价于调用
asctime(localtime(&cur_time))
asctime
函数和ctime
函数的返回值指向一个静态分配的结构,该结构可以被后续的asctime
或ctime
调用修改。
strftime
函数和asctime
函数一样,也把分解时间转换成字符串格式。然而,不同于asctime
函数的是,strftime
函数提供了大量对时间进行格式化的控制。事实上,strftime
函数类似于sprintf函数(22.8节)
,因为strftime
函数会根据格式串(函数的第三个参数)把字符“写入”到字符串s
(函数的第一个参数)中。格式串可能含有普通字符(原样不动地复制给字符串s
)和表26-4
中的转换说明(用指定的字符串代替)。函数的最后一个参数指向tm
结构,此结构用作日期和时间的来源。函数的第二个参数是对可以存储在字符串s
中的字符数量的限制。
表26-4 用于strftime
函数的转换说明
转换说明 | 替换的内容 |
---|---|
%a | 缩写的星期名(如Sun) |
%A | 完整的星期名(如Sunday) |
%b | 缩写的月份名(如Jun) |
%B | 完整的月份名(如June) |
%c | 完整的日期和时间(如Sun Jun 3 17:48:34 2007) |
%C① | 把年份除以100 并向下截断舍入(00~99) |
%d | 月内的第几天(01~31) |
%D① | 等价于%m/%d/%y |
%e① | 月内的第几天(1~31),单个数字前加空格 |
%F① | 等价于%Y-%m-%d |
%g① | ISO 8601中按星期计算的年份的最后两位数字(00~99) |
%G① | ISO 8601中按星期计算的年份 |
%h① | 等价于%b |
%H | 24小时制的小时(00~23) |
%I | 12小时制的小时(01~12) |
%j | 年内的第几天(001~366) |
%m | 月份(01~12) |
%M | 分钟(00~59) |
%n① | 换行符 |
%p | AM/PM指示符(AM 或PM) |
%r① | 12小时制的时间(如05:48:34 PM) |
%R① | 等价于%H:%M |
%S | 秒(00~61),C99中最大值为60 |
%t① | 水平制表符 |
%T① | 等价于%H:%M:%S |
%u① | ISO 8601中的星期(1~7),星期一为1 |
%U | 星期的编号(00~53),第一个星期日是第1个星期的开始 |
%V① | ISO 8601中星期的编号(01~53) |
%w | 星期几(0~6),星期天为0 |
%W | 星期的编号(00~53),第一个星期一是第1个星期的开始 |
%x | 完整的日期(如06/03/07) |
%X | 完整的时间(如17:48:34) |
%y | 年份的最后两位数字(00~99) |
%Y | 年份 |
%z① | 与UTC时间的偏差,用ISO 8601格式表示(比如-0530或+0200) |
%Z | 时区名或缩写(如EST) |
%% | % |
①从C99
开始有。
strftime
函数不同于<time.h>
中的其他函数,它对当前地区(25.1节)
是很敏感的。改变LC_TIME
类别可能会影响转换说明的行为。表26-4
中的例子仅针对"C"
地区。在德国地区,%A
可能会产生Dienstag
而不是Tuesday
。
C99
标准精确地指出了一些转换说明在"C"
地区的替换字符串。(C89
没有这么详细。)表26-5
列出了这些转换说明及相应的替换字符串。
表26-5 strftime
转换说明在"C"
地区的替换字符串
转换说明 | 替换的内容 |
---|---|
%a | %A的前三个字符 |
%A | “Sunday”、“Monday” … "Saturday"之一 |
%b | %B的前三个字符 |
%B | “January”、“February” … "December"之一 |
%c | 等价于"%a %b %e %T %Y" |
%p | "AM"或"PM"其中之一 |
%r | 等价于"%I:%M:%S %p" |
%x | 等价于"%m/%d/%y" |
%X | 等价于%T |
%Z | 由实现定义 |
C99
还增加了许多strftime
转换说明,如表26-4
所示。增加这些转换说明的原因之一是需要支持ISO 8601
标准。
C99
允许用E
或O
来修改特定的strftime
转换说明的含义。以E
或O
指定符开头的转换说明会导致以一种依赖于当前地区的备选格式来执行替换。如果该格式在当前地区不存在,那么指定符不起作用。("C"
地区忽略E
和O
。)表26-7
列出了所有可以加E
或O
指定符的转换说明。
表26-7 可以用E
或O
修饰的strftime
转换说明(从C99
开始)
转换说明 | 替换的内容 |
---|---|
%Ec | 备选的日期和时间表示 |
%EC | 基年(期)名字的备选表示 |
%Ex | 备选的日期表示 |
%EX | 备选的时间表示 |
%Ey | 与%EC(仅基年)的偏移量的备选表示 |
%EY | 完整的年份的备选表示 |
%Od | 月内的第几日,用备选的数值符号表示(前面加零;如果没有用于零的备选符号,前面加空格) |
%Oe | 月内的第几日,用备选的数值符号表示(前面加空格) |
%OH | 24小时制的小时,用备选的数值符号表示 |
%OI | 12小时制的小时,用备选的数值符号表示 |
%Om | 月份,用备选的数值符号表示 |
%OM | 分钟,用备选的数值符号表示 |
%OS | 秒,用备选的数值符号表示 |
%Ou | ISO 8601中的星期,用备选的格式表示该数,星期一为1 |
%OU | 星期的编号,用备选的数值符号表示 |
%OV | ISO 8601中星期的编号,用备选的数值符号表示 |
%Ow | 星期几的数值表示,用备选的数值符号表示 |
%OW | 星期的编号,用备选的数值符号表示 |
%Oy | 年份的最后两位数字,用备选的数值符号表示 |
现在需要一个显示当前日期和时间的程序。当然,程序的第一步是要调用
time
函数来获得日历时间,第二步是把时间转换成字符串格式并显示出来。第二步最简单的做法就是调用ctime
函数,它会返回一个指向含有日期和时间的字符串的指针,然后把此指针传递给puts
函数或printf
函数。到目前为止,一切都很顺利。可是,如果希望程序按照特定的方式显示日期和时间会怎样呢?假设这里需要如下的显示格式:
06-03-2007 5:48p
其中06
是月份,03
是月内的第几日。ctime
函数总是对日期和时间采用相同的格式,所以对此无能为力。strftime
函数相对好一些,使用它基本可以满足需求。但是strftime
函数无法显示不以零开头的单数字小时数,而且strftime
函数使用AM
和PM
,而不是a
和p
。
看来strftime
函数还不够好,因此我们采用另外一种方法:把日历时间转换为分解时间,然后从tm
结构中提取相关的信息,并使用printf
函数或类似的函数对信息进行格式化。我们甚至可以使用strftime
函数来实现某些格式化,然后用其他函数来完成整个工作。
下面的程序说明了这种方案。程序用三种格式显示了当前日期和时间:一种格式是由
ctime
函数格式化的,一种格式是接近于我们需求的(由strftime
函数产生的),还有一种则是所需的格式(由printf
函数产生的)。采用ctime
函数的版本容易实现,采用strftime
函数的版本稍微难一些,而采用printf
函数的版本最难。
/*
datetime.c
--Displays the current date and time in three formats
*/
#include <stdio.h>
#include <time.h>
int main(void)
{
time_t current = time(NULL);
struct tm *ptr;
char date_time[21];
int hour;
char am_or_pm;
/* Print date and time in default format */
puts(ctime(¤t));
/* Print date and time, using strftime to format */
strftime(date_time, sizeof(date_time),
"%m-%d-%Y %I:%M%p\n", localtime(¤t));
puts(date_time);
/* Print date and time, using printf to format */
ptr = localtime(¤t);
hour = ptr->tm_hour;
if (hour <= 11)
am_or_pm = 'a';
else {
hour -= 12;
am_or_pm = 'p';
}
if (hour == 0)
hour = 12;
printf("%.2d-%.2d-%d %2d:%.2d%c\n", ptr->tm_mon + 1, ptr->tm_mday,
ptr->tm_year + 1900, hour, ptr->tm_min, am_or_pm);
return 0;
}
/*
输出如下:
Sun Jun 3 17:48:34 2007
06-03-2007 05:48PM
06-03-2007 5:48p
*/
问与答
问1:虽然
<stdlib.h>
提供了许多把字符串转换成数的函数,但是它没有给出任何把数转换成字符串的函数。为什么呢?
答:C
的某些库提供名字类似itoa
的函数来把数转换为字符串。但是,使用这类函数不是一个好主意,因为它们不是C
标准的一部分,无法移植。把数转换成为字符串的最好做法就是调用诸如sprintf(22.8节
)这样的函数来把格式化的输出写入字符串:
char str[20];
int i;
...
sprintf(str, "%d", i); /* writes i into the string str */
sprintf
函数不但可以移植,而且可以对数的显示提供了大量的控制。
问2:
strtod
函数的描述指出,C99
允许字符串参数包含十六进制浮点数、无穷数以及NaN
。这些数的格式是怎样的呢?
答:十六进制浮点数以0x
或0X
开头,后面跟着一个或多个十六进制数字(可能包括小数点字符),然后是二进制的指数。(第7
章末尾的“问与答”
部分讨论了十六进制浮点常量的格式,该格式与十六进制浮点数类似,但不完全一样。)无穷数的形式为INF
或INFINITY
,其中的任何字母都可以小写,都小写也没问题。NaN
用字符串NAN
(也可以忽略大小写)表示,后面可能有一对圆括号。圆括号里面可以为空,也可以包含一系列字符,其中每个字符可以是字母、数字或下划线。这些字符可以用于为NaN
值的二进制表示指定某些位,但准确的含义是由实现定义的;这些字符(C99
标准称之为n
个字符的序列)还可以用于nan函数(23.4节)
的调用。
问3:在程序的任何地方调用
exit(n)
通常都等价于执行main
函数中的语句return n;
。什么时候两者不等价呢?
答:存在两种情况。首先,当main
函数返回时,其局部变量的生命周期结束[假定它们具有自动存储期(18.2节
),没有声明为static
的局部变量都具有自动存储期],但是调用exit
函数时没有这种现象。如果程序终止时需要访问这些变量(例如调用之前用atexit
注册的函数,或者清洗输出流的缓冲区),那么就会出问题了。特别地,程序可能已经调用了setvbuf函数(22.2节)
,并用main
中的变量作为缓冲区。可见,个别情况下从main
中返回可能不合适,而调用exit
则可行。
另一种情况只在C99
中出现。C99
允许main
函数使用int
之外的返回类型,当然前提是具体的实现显式地允许程序员这么做。在这样的情况下,exit(n)
函数调用不一定等价于执行main
函数中的return n;
。事实上,语句return n;
可能是不合法的(比如main
的返回类型为void
的时候)。
问4:
abort
函数和SIGABRT
信号之间是否存在联系呢?
答:存在。调用abort
函数时,实际上会产生SIGABRT
信号。如果没有处理SIGABRT
的函数,那么程序会像26.2节
中描述的那样异常终止。如果(通过调用signal
函数,24.3节
)为SIGABRT
安装了处理函数,那么就会调用处理函数。如果处理函数返回,随后程序会异常终止。但是,如果处理函数不返回(比如它调用了longjmp
函数,24.4节
),那么程序就不终止。
问5:为什么存在
div
函数和ldiv
函数呢?难道只用/
和%
运算符不行吗?
答:div
函数和ldiv
函数同/
运算符和%
运算符不完全一样。回顾4.1节
就会知道,如果把/
运算符和%
运算符用于负的操作数,在C89
中无法得到可移植的结果。如果i
或j
为负数,那么i/j
的值是向上舍入还是向下舍入是由实现定义的;i%j
的符号也是如此。但是,由div
函数和ldiv
函数计算的答案是不依赖于实现的。商趋零截尾,余数则根据公式n=q×d+r
计算得出,其中n
是原始数,q
是商,d
是除数,而r
是余数。下面是几个例子:
n d q r
7 3 2 1
-7 3 -2 -1
7 -3 -2 1
-7 -3 2 -1
C99
中,/
运算符和%
运算符同div
函数和ldiv
函数的结果一样。
效率是div
函数和ldiv
函数存在的另一个原因。许多机器可以在一条指令里计算出商和余数,所以调用div
函数或ldiv
函数可能比分别使用/
运算符和%
运算符要快。
问6:
gmtime
函数的名字如何而来?
答:gmttime
代表格林尼治标准时间(GreenwichMeanTime,GMT)
,它是英国格林尼治皇家天文台的本地时间(太阳时)。1884
年,GMT
被采纳为国际参考时间,其他时区都用“GMT
之前”或“GMT
之后”的小时数来表示。1972
年,世界协调时间(UTC)
取代GMT
称为了国际时间参考,该系统基于原子钟而不是对太阳的观察。通过每隔几年加一个“闰秒”,UTC
与GMT
的时间差可以控制在0.9
秒以内。所以如果不考虑最精确的时间度量,可以认为这两个系统基本上是一样的。
写在最后
本文是博主阅读《C语言程序设计:现代方法(第2版·修订版)》时所作笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对各位有所帮助,Thank you very much!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!