国内老牌的室内设计网站,论吉林省网站职能建设,同一域名可以做相同网站吗,电影网站建设教学视频文章目录 第26章 stdarg.h、stdlib.h和time.h标准库26.1 stdarg.h: 可变参数26.1.1 调用带有可变参数列表的函数26.1.2 v...printf函数26.1.3 v...scanf函数(C99) 26.2 stdlib.h: 通用的实用工具26.2.1 数值转换函数26.2.1.1 测试数值… 文章目录 第26章 stdarg.h、stdlib.h和time.h标准库26.1 stdarg.h: 可变参数26.1.1 调用带有可变参数列表的函数26.1.2 v...printf函数26.1.3 v...scanf函数(C99) 26.2 stdlib.h: 通用的实用工具26.2.1 数值转换函数26.2.1.1 测试数值转换函数 26.2.2 伪随机序列生成函数26.2.3 与环境的通信26.2.4 搜索和排序实用工具26.2.5 整数算术运算函数26.2.6 地址对齐的内存分配(C1X) 26.3 time.h: 日期和时间26.3.1 时间处理函数26.3.2 时间转换函数 问与答写在最后 第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后边开始。带有可变数量参数的函数必须至少有一个“正常的”形式参数省略号总是出现在形式参数列表的末尾在最后一个正常参数的后边。
语句largestva_arg(ap,int);获取max_int函数的第二个参数n后面的那个并将其赋值给变量largest然后自动前进到下一个参数处。语句中的单词int表明我们希望max_int函数的第二个实参是int类型的。当程序执行内部循环时语句currentva_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宏把srcva_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_VAL23.3节strtol函数和strtoul函数返回相应返回类型的最小值或最大值。strtol返回LONG_MIN或LONG_MAXstrtoul返回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 3e09
atoi 2147483647
atol 2147483647 Function Return Value Valid? String Consumed?
-------- ------------ ------ ----------------
strtod 3e09 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 807009856Enter 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分钟后边的秒061①tm_min小时后边的分钟059tm_hour从午夜开始计算的小时023tm_mday月内的第几天131tm_mon一月以来的月数011tm_year1900年以来的年数0—tm_wday星期日以来的天数06tm_yday1月1日以来的天数0365tm_isdst夏令时标志②②①允许两个额外的“闰秒”。C99中最大值为60。 ②如果夏令时有效就为正数如果无效就为零如果这一信息未知就为负数。 struct timespec这是从C11开始新增的结构类型用来保存一个用秒和纳秒来指定的时间间隔可用于描述一个基于特定时期的日历时间。表26-2给出了这种结构类型的成员。 表26-2 struct timespec结构的成员 名称描述最小值最大值tv_sec完整的秒数0取决于实现tv_nsec纳秒0999 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_SECtime.h中定义的宏。 当用clock函数来确定程序已运行多长时间时习惯做法是调用clock函数两次一次在main函数开始处另一次在程序就要终止之前。 #include stdio.h
#include time.hint 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_mday1212日tm_mon78月tm_year1122012年tm_wday0星期日tm_yday224这一年的第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); 时间转换函数可以把日历时间转换成分解时间还可以把时间日历时间或分解时间转换成字符串格式。下图说明了这些函数之间的关联关系 #mermaid-svg-IonwHBOWK5mDDbjY {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-IonwHBOWK5mDDbjY .error-icon{fill:#552222;}#mermaid-svg-IonwHBOWK5mDDbjY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IonwHBOWK5mDDbjY .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-IonwHBOWK5mDDbjY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IonwHBOWK5mDDbjY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IonwHBOWK5mDDbjY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IonwHBOWK5mDDbjY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IonwHBOWK5mDDbjY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IonwHBOWK5mDDbjY .marker.cross{stroke:#333333;}#mermaid-svg-IonwHBOWK5mDDbjY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IonwHBOWK5mDDbjY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IonwHBOWK5mDDbjY .cluster-label text{fill:#333;}#mermaid-svg-IonwHBOWK5mDDbjY .cluster-label span{color:#333;}#mermaid-svg-IonwHBOWK5mDDbjY .label text,#mermaid-svg-IonwHBOWK5mDDbjY span{fill:#333;color:#333;}#mermaid-svg-IonwHBOWK5mDDbjY .node rect,#mermaid-svg-IonwHBOWK5mDDbjY .node circle,#mermaid-svg-IonwHBOWK5mDDbjY .node ellipse,#mermaid-svg-IonwHBOWK5mDDbjY .node polygon,#mermaid-svg-IonwHBOWK5mDDbjY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IonwHBOWK5mDDbjY .node .label{text-align:center;}#mermaid-svg-IonwHBOWK5mDDbjY .node.clickable{cursor:pointer;}#mermaid-svg-IonwHBOWK5mDDbjY .arrowheadPath{fill:#333333;}#mermaid-svg-IonwHBOWK5mDDbjY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IonwHBOWK5mDDbjY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IonwHBOWK5mDDbjY .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-IonwHBOWK5mDDbjY .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-IonwHBOWK5mDDbjY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IonwHBOWK5mDDbjY .cluster text{fill:#333;}#mermaid-svg-IonwHBOWK5mDDbjY .cluster span{color:#333;}#mermaid-svg-IonwHBOWK5mDDbjY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IonwHBOWK5mDDbjY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} gmtime localtime mktime ctime asctime strftime 日历时间time_t 分解时间struct tm 字符串 图中包含了mktime函数。C标准把此函数划分为“处理”函数而不是“转换”函数。 gmtime函数和localtime函数很类似。当传递指向日历时间的指针时这两种函数都会返回一个指向结构的指针该结构含有等价的分解时间。localtime函数会产生本地时间而gmtime函数的返回值则是用UTC表示的。gmtime函数和localtime函数的返回值指向一个静态分配的结构该结构可以被后续的gmtime或localtime调用修改。 asctimeASCII时间函数返回一个指向以空字符结尾的字符串的指针字符串的格式如下 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月内的第几天0131%D①等价于%m/%d/%y%e①月内的第几天131单个数字前加空格%F①等价于%Y-%m-%d%g①ISO 8601中按星期计算的年份的最后两位数字00~99%G①ISO 8601中按星期计算的年份%h①等价于%b%H24小时制的小时0023%I12小时制的小时0112%j年内的第几天001366%m月份0112%M分钟0059%n①换行符%pAM/PM指示符AM 或PM%r①12小时制的时间如05:48:34 PM%R①等价于%H:%M%S秒0061C99中最大值为60%t①水平制表符%T①等价于%H:%M:%S%u①ISO 8601中的星期17星期一为1%U星期的编号0053第一个星期日是第1个星期的开始%V①ISO 8601中星期的编号0153%w星期几06星期天为0%W星期的编号0053第一个星期一是第1个星期的开始%x完整的日期如06/03/07%X完整的时间如17:48:34%y年份的最后两位数字0099%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%pAM或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月内的第几日用备选的数值符号表示前面加空格%OH24小时制的小时用备选的数值符号表示%OI12小时制的小时用备选的数值符号表示%Om月份用备选的数值符号表示%OM分钟用备选的数值符号表示%OS秒用备选的数值符号表示%OuISO 8601中的星期用备选的格式表示该数星期一为1%OU星期的编号用备选的数值符号表示%OVISO 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(current)); /* Print date and time, using strftime to format */ strftime(date_time, sizeof(date_time), %m-%d-%Y %I:%M%p\n, localtime(current)); puts(date_time); /* Print date and time, using printf to format */ ptr localtime(current); 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标准的一部分无法移植。把数转换成为字符串的最好做法就是调用诸如sprintf22.8节这样的函数来把格式化的输出写入字符串
char str[20];
int i;
...
sprintf(str, %d, i); /* writes i into the string str */sprintf函数不但可以移植而且可以对数的显示提供了大量的控制。 问2strtod函数的描述指出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的时候。 问4abort函数和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函数计算的答案是不依赖于实现的。商趋零截尾余数则根据公式nq×dr计算得出其中n是原始数q是商d是除数而r是余数。下面是几个例子
n d q r
7 3 2 1
-7 3 -2 -1
7 -3 -2 1
-7 -3 2 -1C99中/运算符和%运算符同div函数和ldiv函数的结果一样。
效率是div函数和ldiv函数存在的另一个原因。许多机器可以在一条指令里计算出商和余数所以调用div函数或ldiv函数可能比分别使用/运算符和%运算符要快。 问6gmtime函数的名字如何而来 答gmttime代表格林尼治标准时间GreenwichMeanTime,GMT它是英国格林尼治皇家天文台的本地时间太阳时。1884年GMT被采纳为国际参考时间其他时区都用“GMT之前”或“GMT之后”的小时数来表示。1972年世界协调时间UTC取代GMT称为了国际时间参考该系统基于原子钟而不是对太阳的观察。通过每隔几年加一个“闰秒”UTC与GMT的时间差可以控制在0.9秒以内。所以如果不考虑最精确的时间度量可以认为这两个系统基本上是一样的。 写在最后 本文是博主阅读《C语言程序设计现代方法第2版·修订版》时所作笔记日后会持续更新后续章节笔记。欢迎各位大佬阅读学习如有疑问请及时联系指正希望对各位有所帮助Thank you very much!