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); 

printfscanf这样的函数具有一个不同寻常的性质:它们允许任意数量的参数。而且,这种能处理可变数量的参数的能力并不仅限于库函数。<stdarg.h>头提供的工具使我们能够自己编写带有变长参数列表的函数<stdarg.h>声明了一种类型(va_list)并定义了几个宏。C89中一共有三个宏,分别名为va_satrtva_argva_endC99增加了一个类似函数的宏va_copy

为了了解这些宏的工作原理,这里将用它们来编写一个名为max_int的函数。此函数用来在任意数量的整数参数中找出最大数。下面是此函数的调用过程:

max_int(3, 10, 30, 20)

函数的第一个实参指明后面有几个参数。这里的max_int函数调用将返回30(即103020中的最大数)。

下面是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宏把srcva_list类型的值)复制到dest(也是va_list类型的值)中。va_copy之所以能起作用,是因为在把src复制到dest之前可能已经多次用src来调用va_arg了。调用va_copy可以使函数记住在参数列表中的位置,从而以后可以回到同一位置继续处理相应的参数(及其后面的参数)。

每次调用va_startva_copy时都必须与va_end成对使用,而且这些成对的调用必须在同一个函数中。所有的va_arg调用必须出现在va_start(或va_copy)与配对的va_end调用之间。

请注意!!当调用带有可变参数列表的函数时,编译器会在省略号对应的所有参数上执行默认实参提升(9.3节)。特别地,char类型和short类型的参数会被提升为int类型,float类型的值会被提升为double类型。因此把charshortfloat类型的值作为参数传递给va_arg是没有意义的,(提升后的)参数不可能具有这些类型


26.1.1 调用带有可变参数列表的函数

调用带有可变参数列表的函数存在固有的风险。早在第3章我们就认识到,给printf函数和scanf函数传递错误的参数是很危险的。其他带有可变参数列表的函数也同样很敏感。主要的难点在于,带有可变参数列表的函数无法确定参数的数量和类型。这一信息必须被传递给函数或者由函数来假定。示例中的max_int函数依靠第一个参数来指明后面有多少参数,并且它假定参数都是int类型的。而像printfscanf这样的函数则是依靠格式串来描述其他参数的数量以及每个参数的类型。

另外一个问题是关于以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> 

vfprintfvprintfvsprintf函数(即v...printf函数)都属于<stdio.h>。这些函数放在本节讨论,是因为它们总是和<stdarg.h>中的宏联合使用。C99增加了vsnprintf函数。

v...printf函数和fprintfprintf以及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函数”vfscanfvscanfvsscanf分别与fscanfscanfsscanf等价,区别在于前者具有一个va_list类型的参数用于接受可变参数列表。与v...printf函数一样,v...scanf函数也主要用于具有可变数量参数的包装函数。包装函数需要在调用v...scanf函数之前调用va_start,并在v...scanf函数返回后调用va_end


26.2 <stdlib.h>: 通用的实用工具

<stdlib.h>涵盖了全部不适合于其他头的函数。<stdlib.h>中的函数可以分为以下8组:

  • 数值转换函数;
  • 伪随机序列生成函数;
  • 内存管理函数;
  • 与外部环境的通信;
  • 搜索和排序工具;
  • 整数算术运算函数;
  • 多字节/宽字符转换函数;
  • 多字节/宽字符串转换函数。

下面将逐个介绍每组函数,但是有三组例外:内存管理函数多字节/宽字符转换函数以及多字节/宽字符串转换函数

内存管理函数(即malloccallocreallocfree)允许程序分配内存块,以后再释放或者改变内存块的大小。第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

旧函数(atofatoiatol)把字符串分别转换成doubleint或者long int类型值。不过,这些函数不能指出转换过程中处理了字符串中的多少字符,也不能指出转换失败的情况。[这些函数的一些实现可以在转换失败时修改errno变量(24.2节),但不能保证会这么做。]

C89中的函数(strtodstrtolstrtoul)更复杂一些。首先,它们会通过修改endptr指向的变量来指出转换停止的位置。(如果不在乎转换结束的位置,那么函数的第二个参数可以为空指针。)为了检测函数是否可以对整个字符串完成转换,只需检测此变量是否指向空字符。如果不能进行转换,将把nptr的值赋给endptr指向的变量(前提是endptr不是空指针)。此外,strtolstrtoul还有一个base参数用来说明待转换数的基数。基数在2~36范围内都可以(包括236)。

除了比原来的旧函数更通用以外,strtodstrtolstrtoul函数还更善于检测错误。如果转换得到的值超出了函数返回类型的表示范围,那么每个函数都会在errno变量中存储ERANGE。此外,strtod函数返回正的或负的HUGE_VAL(23.3节)strtol函数和strtoul函数返回相应返回类型的最小值或最大值。(strtol返回LONG_MINLONG_MAXstrtoul返回ULONG_MAX。)

C99增加了函数atollstrtofstrtoldstrtollstrtoullatollatol类似,区别在于前者把字符串转换为long long int类型的值。strtofstrtoldstrtod类似,区别在于前两者分别把字符串转换为floatlong double类型的值。strtollstrtol类似,区别在于前者把字符串转换为long long int类型的值。strtoullstrtoul类似,区别在于前者把字符串转换为unsigned long long int类型的值。C99还对浮点数值转换函数做了一些小的改动:传递给strtod(以及strtofstrtold)的字符串可以包含十六进制的浮点数、无穷数或NaN


26.2.1.1 测试数值转换函数

下面这个程序通过应用C89中的6个数值转换函数中的每一个来把字符串转换为数值格式。在调用了strtodstrtolstroul函数之后,程序还会显示出是否每种转换都产生了有效的结果,以及是否每种转换可以对整个字符串完成转换。程序将从命令行中获得输入字符串。

/*
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都会立刻返回0str...函数不会改变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)执行操作系统的命令。

其中_ExitC99新增的;at_quick_exitquick_exitC11新增的。尤其需要注意的是,从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使程序正常终止,但不会调用那些用atexitsignal注册的函数。它首先按照和注册时相反的顺序调用那些用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");

这会调用UNIXls命令,并要求其把当前目录下的文件列表写入名为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)的结构。例如,如果ansdiv_t类型的变量,那么可以写出下列语句:

ans = div(5, 2); 
printf("Quotient: %d Remainder: %d\n", ans.quot, ans.rem);

ldiv函数和div函数很类似,但用于处理长整数。ldiv函数返回ldiv_t类型的结构,该结构也包含quotrem两个成员。(div_t类型和ldiv_t类型在<stdlib.h>中声明。)

C99提供了两个新函数。llabs函数返回long long int类型值的绝对值。lldiv类似于divldiv,区别在于它把两个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小时后边的分钟059
    tm_hour从午夜开始计算的小时023
    tm_mday月内的第几天131
    tm_mon一月以来的月数011
    tm_year1900年以来的年数0
    tm_wday星期日以来的天数06
    tm_yday1月1日以来的天数0365
    tm_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 tmstruct timespec类型的值需要的空间大得多,但是这类值通常易于使用。C标准规定clock_ttime_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类型。C99CLOCKS_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_mdaytm_montm_year进行操作。
  • 在调整完结构的其他成员后(如果必要),mktime函数会给tm_wday(一星期的第几天)和tm_yday(一年的第几天)设置正确的值。在调用mktime函数之前,从来不需要对tm_wdaytm_yday的值进行任何初始化,因为mktime函数会忽略这些成员的初始值。

mktime函数调整tm结构成员的能力对于和时间相关的算术计算非常有用。例如,现在用mktime函数来回答下面这个问题:如果2012年的奥林匹克运动会从727日开始,并且历时16天,那么结束的日期是哪天?我们首先把日期2012727日存储到tm结构中:

struct tm t; 

t.tm_mday = 27; 
t.tm_mon = 6;  /* July */ 
t.tm_year  112; /* 2012 */ 

我们还要对结构的其他成员进行初始化(成员tm_wdaytm_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); 

时间转换函数可以把日历时间转换成分解时间,还可以把时间(日历时间或分解时间)转换成字符串格式。下图说明了这些函数之间的关联关系:

gmtime
localtime
mktime
ctime
asctime
strftime
日历时间time_t
分解时间struct tm
字符串

图中包含了mktime函数。C标准把此函数划分为“处理”函数而不是“转换”函数。

gmtime函数和localtime函数很类似。当传递指向日历时间的指针时,这两种函数都会返回一个指向结构的指针,该结构含有等价的分解时间。localtime函数会产生本地时间,而gmtime函数的返回值则是用UTC表示的gmtime函数和localtime函数的返回值指向一个静态分配的结构,该结构可以被后续的gmtimelocaltime调用修改。


asctimeASCII时间)函数返回一个指向以空字符结尾的字符串的指针,字符串的格式如下:

Sun Jun  3 17:48:34 2007\n

此字符串由函数参数所指向的分解时间构成。

ctime函数返回一个指向描述本地时间的字符串的指针。如果cur_timetime_t类型的变量,那么调用

ctime(&cur_time) 

就等价于调用

asctime(localtime(&cur_time))

asctime函数和ctime函数的返回值指向一个静态分配的结构,该结构可以被后续的asctimectime调用修改。

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
%H24小时制的小时(00~23)
%I12小时制的小时(01~12)
%j年内的第几天(001~366)
%m月份(01~12)
%M分钟(00~59)
%n①换行符
%pAM/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允许用EO来修改特定的strftime转换说明的含义。以EO指定符开头的转换说明会导致以一种依赖于当前地区的备选格式来执行替换。如果该格式在当前地区不存在,那么指定符不起作用。("C"地区忽略EO。)表26-7列出了所有可以加EO指定符的转换说明。

表26-7 可以用EO修饰的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函数使用AMPM,而不是ap

看来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标准的一部分,无法移植。把数转换成为字符串的最好做法就是调用诸如sprintf(22.8节)这样的函数来把格式化的输出写入字符串:

char str[20]; 
int i; 
... 
sprintf(str, "%d", i); /* writes i into the string str */

sprintf函数不但可以移植,而且可以对数的显示提供了大量的控制。

问2strtod函数的描述指出,C99允许字符串参数包含十六进制浮点数、无穷数以及NaN。这些数的格式是怎样的呢?

答:十六进制浮点数以0x0X开头,后面跟着一个或多个十六进制数字(可能包括小数点字符),然后是二进制的指数。(第7章末尾的“问与答”部分讨论了十六进制浮点常量的格式,该格式与十六进制浮点数类似,但不完全一样。)无穷数的形式为INFINFINITY,其中的任何字母都可以小写,都小写也没问题。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中无法得到可移植的结果。如果ij为负数,那么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函数可能比分别使用/运算符和%运算符要快。

问6gmtime函数的名字如何而来?

答:gmttime代表格林尼治标准时间(GreenwichMeanTime,GMT),它是英国格林尼治皇家天文台的本地时间(太阳时)1884年,GMT被采纳为国际参考时间,其他时区都用“GMT之前”或“GMT之后”的小时数来表示。1972年,世界协调时间(UTC)取代GMT称为了国际时间参考,该系统基于原子钟而不是对太阳的观察。通过每隔几年加一个“闰秒”,UTCGMT的时间差可以控制在0.9秒以内。所以如果不考虑最精确的时间度量,可以认为这两个系统基本上是一样的


写在最后

本文是博主阅读《C语言程序设计:现代方法(第2版·修订版)》时所作笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对各位有所帮助,Thank you very much!

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

New_Teen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值