[C陷阱和缺陷] 第5章 库函数

  有关库函数的使用,我们能给出的最好建议是尽量使用系统头文件,当然也可以自己造轮子,随个人喜好。本章将探讨某些常用的库函数,以及编程者在使用它们的过程中可能出错之处。
 

5.1 返回整数的getchar函数

  我们首先考虑下面的例子:

    #include<stdio.h>

    int main()
    {
        char c;

        while( ( (c = getchar())  !=  EOF ) )
            putchar(c);

        return 0;
    }

  上面代码在某些情况下可能出错,原因在于变量 c 被声明为 char 类型,而不是 int 类型。这意味着 c 无法容纳下所有可能的字符,(有可能发生“截断” )特别是,可能无法容纳下EOF。
  因此可能出现两种可能。一种可能是,某些合法的输入字符在被“截断” 后使得 c 的取值与EOF相同;另一种可能是,c 根本不可能取到EOF这个值。对于前一种情况,程序将在文件复制的中途终止;对于后一种情况,程序将陷入一个死循环。

5.2 更新顺序文件

  看下面这段代码:

    FILE    *fp;
    fp = fopen(filename,"r+");
    struct record rec;
    .....
    while(fread( fread( (char*)&rec, sizeof(rec), 1, fp ) == 1 )
    {
        if(/*rec必须被重新写入*/)
        {
            fseek(fp, -(long)sizeof(rec), 1); //文件指针向前移动一个结构体大小的长度
            fwrite( (char*)&rec, sizeof(rec), 1, fp );
        }
    }

 
  这段代码咋看上去毫无问题: &rec 在传入 fread 和 fwrite 函数时被小心翼翼地转换为字符指针类型,sizeof(rec)被转换为长整型 ( fseek 函数要求第二个参数是long类型,因为 int 类型的整数可能无法包含一个文件的大小;sizeof返回一个unsigned值,因此首先将其转换为有符号类型才有可能将其反号)。但是这段代码仍然可能运行失败,而且出错的方式非常难以察觉。
  问题出在:为了保持与过去不能同时进行读写操作的程序的向下兼容性,一个输入操作不能随后直接紧跟一个输出操作,反之亦然。如果要同时进行输入和输出操作,必须在其中插入 fseek 函数的调用,即使fseek什么也没做。解决的办法是把这段代码改写为:

    while(fread( fread( (char*)&rec, sizeof(rec), 1, fp ) == 1 )
    {
        if(/*rec必须被重新写入*/)
        {
            fseek(fp, -(long)sizeof(rec), 1); //文件指针向前移动一个结构体大小的长度
            fwrite( (char*)&rec, sizeof(rec), 1, fp );
            fseek(fp, 0L, 1);
        }
    }

  第二个 fseek 函数虽然看上去什么也没做,但它改变了文件的状态,使得文件现在可以正常地进行读取了。

5.3 缓存输出与内存分配

  程序输出有两种方式:一种是即时处理方式,另一种是先暂存起来,然后再大块写入的方式,前者往往造成较高的系统负担。
  因此,C语言实现通常都允许程序员进行实际的写操作之前控制产生的输出数据量。这种控制能力一般是通过库函数setbuf实现的。如果buf是一个大小适当的字符数组,那么
  setbuf(stdout, buf); //设置了,程序输出不再是即时处理方式
  语句将通知输入/输出库,所有写入到stdout的输出都应该使用buf作为输出缓冲区,直到buf输出缓冲区被填满或者程序员直接调用fflush(译注:对于由写操作打开的文件,调用fflush将导致输出缓冲区的内容被实际地写入该文件),buf缓冲区中的内容才实际写入到stdout中。缓冲区的大小由系统头文件<stdio.h>中的BUFSIZ定义。
 
  下面的程序的作用是把标准输入的内容复制到标准输出中,演示了setbuf库函数最显而易见的用法:

    #include<stdio.h>

    main()
    {
        int c;

        char buf[BUFSIZ];
        setbuf(stdout, buf);
    
        while( (c = getchar()) != EOF )
            putchar(c);
    }

  遗憾的是,这个程序是错误的。原因:我们不妨思考一下buf缓冲区最后一次被清空是在什么时候?答案是在main函数结束之后,作为程序交回控制给操作系统之前C运行时库所必须进行的清理工作的一部分。但是buf 为局部数组,在main函数结束的时候会释放该缓冲区,这样在C运行时库进行清理工作时缓冲区已经提前被释放了,所以有问题。
  要避免这种类型的错误有两种办法。第一种办法是让缓存数组称为静态数组;第二种方式是把buf声明完全移到main函数之外;第三种办法是动态分配缓冲区。这样在程序中并不主动释放分配的缓冲区:

    static char buf[BUFSIZ];

    char *malloc();
    setbuf(stdout, malloc(BUFSIZ));

  如果读者关系一些编程“小技巧”,也许会注意到这里其实并不需要检查malloc函数调用是否成功。如果malloc函数调用失败,将返回一个NULL指针。setbuf函数的第二个参数取值可以为null,此时标准输出不需要进行缓冲。这种情况下,程序仍然能够工作,只不过速度较慢而已。

5.4 使用errno检测错误

  很多库函数,特别是那些与操作系统有关的,当执行失败时会通过一个名称为 errno 的外部变量,通知程序该函数调用失败。下面的代码利用这一特性进行错误处理,似乎再明白不过,然而却是错误的:

    LibraryFun(); /* 调用库函数 */
    if(errno)
        xx;    /* 处理错误 */

  出错原因在于,可能当前的库函数调用没有失败,不会设置errno值,errno的值可能是前一个执行失败的库函数设置的值,而在前面errno没有即时清零。下面的代码作了更正,似乎能够工作,但很可惜还是错误的:

    errno = 0;
    LibraryFun(); /* 调用库函数 */    
    /* 调用库函数 */
    if (errno)
        xx;    /* 处理错误 */    

  存在一种可能,当库函数 fopen 函数新建一个文件以供程序输出,在已经存在一个同名文件时,会先删除它,然后新建一个文件。这样, fopen 函数可能需要调用其他的库函数,以检测同名文件是否已经存在,则被调用的其他的库函数就有可能设置 errno。
  因此,在调用库函数时,我们应当首先检测作为错误指示的返回值,确定程序执行已经失败。然后,再检查errno,来搞清楚出错原因:

    ERROR_CODE rev = libraryFun(); /* 调用库函数 */
    if ( rev ) /* 检测作为错误指示的返回值
        检查 errno


5.5 库函数signal

  实际上所有的C语言实现中都包括有signal库函数,作为捕获异步事件的一种方式。要使用该库函数,需要在源文件中加上

    #include <signal.h>

  以引用相关的声明。要处理一个特定的signal(信号),可以这样调用signal函数:

    signal(signal type, handler function);

  这里的 signal type 代表系统头文件 signal.h 中定义的某些常量,这些常量用来标识 signal 函数将要捕获的信号类型。这里的 handler function 是当制定的事件发生时,将要调用的事件处理函数
  在许多C语言实现中,信号是真正意义上的“异步”。从理论上说,一个信号可能在C程序执行期间的任何时刻上发生。需要特别强调的是,信号甚至可能出现在某些复杂库函数(如 malloc)的执行过程。因此,从安全的角度考虑,信号的处理函数不应该调用上述类型的库函数。
  例如,假设 malloc 函数的执行过程被一个信号中断。此时, malloc 函数用来跟踪可用内存的数据结构很可能只有部分被更新。如果 signal 处理函数再调用 malloc 函数,结果可能是 malloc 函数用到的数据结构完全崩溃,后果不堪设想!
  因此, signal 处理函数能够做的安全的事情,似乎就只有设置一个标志然后返回,期待以后主程序能够检查到这个标志,发现一个信号已经发生。
  然而,就算这样做也并不是总是安全的。当一个算术运算错误(例如溢出或者零做除数)引发一个信号时,某些机器在signal处理函数返回后还将重新执行失败的操作。这种情况下,最可能的结果就是马上又引发一个同样的信号。因此,对于算术运算错误,signal 处理函数的唯一安全、可移植的操作就是打印一条出错消息,然后使用 longjmp 或 exit 立即退出程序。
  由此,我们得到的结论是:信号非常复杂棘手,而且具有一些从本质上而言不可移植的特性。解决这个问题我们最好采取“守势”,让signal处理函数尽可能地简单,并将它们组织在一起。这样,当需要适应一个新系统时,我们很容易地进行修改。
 
  练习 5-1 当一个程序异常终止时,程序输出的最后几行常常会丢失,原因是什么?我们能够采取怎样的措施来解决这个问题?
  答:

  • 一个异常终止的程序可能没有机会来清空其输出缓冲区。因此,该程序的输出可能位于内存的某个位置,但却永远不会被写出了。
  • 对于试图调试这类程序的编程者来说,这种丢失输出的情况经常会误导他们,因为它会造成这样一种印象,程序发生失败的时刻比实际上运行失败的真正时刻要早得多。解决方案就是在调试时强制不允许对输出进行缓冲。要做到这一点,不同的系统有不同的做法,这些做法虽然存在细微差别,但大致如下:
    setbuf( stdout, (char *)0 );

  这个语句必须在任何输出被写入到 stdout (包括任何对 printf 函数的调用)之前执行。该语句最恰当的位置就是作为 main函数的第一条语句。
  练习 5-2 下面程序的作用是把它的输出复制到输出:

    #include <stdio.h>
    main()
    {
        register int c;
        while ( (c = getchar() != EOF) )
            putchar(c);
    }

  从这个程序中移除#include语句,将导致程序不能通过编译,因为这时 EOF 是未定义的。假定我们手工定义了 EOF(当然这是一种不好的做法):

    #defien EOF -1

    main()
    {
        register int c;
        while ( (c = getchar() != EOF) )
            putchar(c);
    }

  答:
  这个程序在许多系统中任然能够运行,但是在某些系统运行起来却慢得多。这是为什么?
  函数调用需要花费较长的程序执行时间,因此在某些系统 getchar 会被实现为宏。这个宏在 stdio.h 头文件中被定义,因此如果一个程序没有包含 stdio.h 头文件,编译器对 getchar 的定义就一无所知。在这种情况下,编译器会假定 getchar 是一个返回类型为整型的函数,导致程序运行变慢。同样的依据也完全适用于 putchar。
  实际上,很多C语言实现在库函数都包括有 getchar 函数,原因部分是预防编程者粗心大意,部分是为了方便那些需要得到 getchar 地址的编程者。

转载于:https://www.cnblogs.com/linuxAndMcu/p/10067594.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值