C缺陷和陷阱-笔记(7)

目录

库函数

一、返回整数的getchar 函数

getchar 函数

二、更新顺序文件

三、缓冲输出与内存分配

程序输出

四、使用errno 检测错误

五、库函数signal 


库函数

C语言中没有定义输入/输出语句,任何一个有用的C程序(起码必须接受零个或多个输入,生成一个或多个输出)都必须调用库函数来完成最基本的输入/输出操作。ANSIC 标准毫无疑问地意识到了这一点,因而定义了一个包含大量标准库函数的集合。从理论上说,任何一个C语言实现都应该提供这些标准库函数。

有关库函数的使用,我们能给出的最好建议是尽量使用系统头文件。特别是库文件的编写者已经提供了精确描述库函数的头文件,在ANSI C 中这一点尤其重要因为头文件中包括了库函数的参数类型以及返回类型的声明。


一、返回整数的getchar 函数

我们首先考虑下面的例子:
# include <stdio. h>


main()
{
        char c ;


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

}

getchar 函数

getchar 函数在一般情况下返回的是标准输入文件中的下一个字符,当没有输入时返回EOF(一个在头文件stdio .h中被定义的值,不同于任何一个字符)。这个程序乍一看似乎是把标准输入复制到标准输出,实则不然。


原因在于程序中的变量c被声明为char类型,而不是int类型。这意味着c无法容下所有可能的字符,特别是,可能无法容下EOF。

因此,最终结果存在三种可能。

1.某些合法的输入字符在被“截断”后使得c的取值与EOF相同。

2.c 根本不可能取到EOF这个值。对于前一种情况,程序将在文件复制的中途终止;对于后一种情况,程序将陷入一个死循环。

3.程序表面上似乎能够正常工作,但完全是因为巧合。尽管函数getchar 的返回结果在赋给char类型的变量c时会发生“截断”操作,尽管while 语句中比较运算的操作数不是函数getchar 的返回值,而是被“截断”的值c,然而许多编译器对上述表达式的实现并不正确。这些编译器确实对函数getchar 的返回值作了“截断”处理,并把低端字节部分赋给了变量c。但是,它们在比较表达式中并不是比较c与EOF,而是比较getchar 函数的返回值与EOF!编译器如果采取的是这种做法,上面的例子程序看上去就能够“正常”运行了.

二、更新顺序文件

许多系统中的标准输入/输出库都允许程序打开一个文件,同时进行写入和读出的操作:
FILE * fp;
fp = fopen ( file, "r+");


上面的例子代码打开了文件名由变量file指定的文件,对于存取权限的设定表明程序希望对这个文件进行输入和输出操作。
编程者也许认为,程序一旦执行上述操作完毕,就可以自由地交错进行读出和写入的操作。遗憾的是,事实总难遂人所愿,为了保持与过去不能同时进行读写操作的程序的向下兼容性,一个输入操作不能随后直接紧跟一个输出操作,反之亦然。如果要同时进行输入和输出操作,必须在其中插入fseek 函数的调用。


下面的程序片段似乎更新了一个顺序文件中选定的记录:
FILE * fp;
struct record rec;
..
while (fread(( char * ) & rec, sizeof( rec) , 1, fp) == 1)  {
               /*对rec执行某些操作*/
              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 值,因此首先必须将其转换为有符号类型才有可能将其反号)。但是这段代码仍然可能运行失败,而且出错的方式非常难于察觉。


问题出在:如果一个记录需要被重新写入文件,也就是说,fwrite 函数得到执行,对这个文件执行的下一个操作将是循环开始的fread 函数。因为在fwrite 函数调用与fread 函数调用之间缺少了一个fseek 函数调用,所以无法进行上述操作。解决的办法是把这段代码改写为:
FILE * fp;
struct record rec;
..
while (fread(( char * ) & rec, sizeof( rec) , 1, fp) == 1)  {
              /*对rec执行某些操作*/
             if(/*rec必须被重新写入*/){
                    fseek(fp,-(long)sizeof(rec),1);
                    fwrite( (char * ) & rec, sizeof( rec) , 1, fp);

                    fseek( fp, OL, 1);
                }
}


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

三、缓冲输出与内存分配

程序输出

程序输出有两种方式:一种是即时处理方式,另一种是先暂存起来,然后再大块写入的方式,前者往往造成较高的系统负担。因此,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);
}


遗憾的是,这个程序是错误的,仅仅是因为一个细微的原因。程序中对库函数setbuf 的调用,通知了输入输出库所有字符的标准输出应该首先缓存在buf中。要找到问题出自何处,我们不妨思考一下buf缓冲区最后一次被清空是在什么时候?答案是在main函数结束之后,作为程序交回控制给操作系统之前C运行时库所必须进行的清理工作的一部分。但是,在此之前buf字符数组已经被释放!

要避免这种类型的错误有两种办法。

第一种办法是让缓冲数组成为静态数组,既可以直接显式声明buf为静态
static char buf [ BUFSLZ]; 

也可以把buf声明完全移到main函数之外。

第二种办法是动态分配缓冲区,在程序中并不主动释放分配的缓冲区(译注:由于缓冲区是动态分配的,所以main函数结束时并不会释放该缓冲区,这样C运行时库进行清理工作时就不会发生缓冲区已释放的情况):
char * malloc();
setbuf( stdout, malloc(BUFSIZ));

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

四、使用errno 检测错误

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


出错原因在于,在库函数调用没有失败的情况下,并没有强制要求库函数一定要设置errno 为0,这样errno 的值就可能是前一个执行失败的库函数设置的值。下面的代码作了更正,似乎能够工作,很可惜还是错误的:
errno = 0;
/*调用库函数*/
if (errno);

    /*处理错误*/


库函数在调用成功时,既没有强制要求对errno 清零,但同时也没有禁止设置errno 。既然库函数已经调用成功,为什么还有可能设置errno 呢?要理解这一点,我们不妨假想一下库函数fopen 在调用时可能会发生什么情况。

当fopen 函数被要求新建一个文件以供程序输出时,如果已经存在一个同名文件,fopen 函数将先删除它,然后新建一个文件。

这样,fopen 函数可能需要调用其他的库函数,以检测同名文件是否已经存在。(译注:假设用于检测文件的库函数在文件不存在时,会设置errno 。那么,fopen 函数每次新建一个事先并不存在的文件时,即使没有任何程序错误发生,errno 也仍然可能被设置。)

因此,在调用库函数时,我们应该首先检测作为错误指示的返回值,确定程序执行已经失败。然后,再检查errno ,来搞清楚出错原因:


/*调用库函数*/
if(返回的错误值)
        检查errno ;

五、库函数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 处理函数中使用longjmp 退出,通常情况下也是不安全的:因为信号可能发生在malloc 或者其他库函数开始更新某个数据结构,却又没有最后完成的过程中。

因此,signal 处理函数能够做的安全的事情,似乎就只有设置一个标志然后返回,期待以后主程序能够检查到这个标志,发现一个信号已经发生。

然而,就算这样做也并不总是安全的。当一个算术运算错误(例如溢出或者零作除数)引发一个信号时,某些机器在signal 处理函数返回后还将重新执行失败的操作。而当这个算术运算重新执行时,我们并没有一个可移植的办法来改变操作数。这种情况下,最可能的结果就是马上又引发一个同样的信号。因此,对于算术运算错误,signal 处理函数的惟一安全、可移植的操作就是打印一条出错消息,然后使用longjmp 或exit立即退出程序。


由此,我们得到的结论是:信号非常复杂棘手,而且具有一些从本质上而言不可移植的特性。解决这个问题我们最好采取“守势”,让signal 处理函数尽可能地简单,并将它们组织在一起。这样,当需要适应一个新系统时,我们可以很容易地进行修改。
 


  • 13
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言是一种广泛应用于计算机科学和软件开发的编程语言。它具有强大的功能和灵活性,适用于开发各种类型的应用程序。 C语言专题精讲篇是一个对C语言进行深入学习和讲解的系列文章或课程。它汇总了C语言相关的重要知识点和技巧,旨在帮助学习者更好地理解和运用C语言。 这个专题中的笔记涵盖了C语言的各个方面,包括基本语法、数据类型、运算符、流程控制、函数、数组、指针、结构体、文件操作等。通过系统性的学习和总结,这些笔记可以帮助学习者逐步掌握C语言的核心概念和常用技巧。 在这个专题中,学习者可以学到如何编写简单的C程序,如何使用变量和运算符进行计算,如何使用条件和循环语句控制程序流程,如何使用函数进行代码的模块化,如何使用数组和指针进行数据的处理,如何使用结构体组织复杂数据,如何进行文件的读写等等。 C语言专题精讲篇的目的是帮助学习者全面、深入地了解C语言的各个方面,并能够独立编写和调试简单到中等难度的C程序。通过反复实践和练习,学习者可以逐渐提高自己的编程能力,并为进一步学习更高级的编程语言打下坚实的基础。 总之,C语言专题精讲篇的笔记汇总是一份重要的学习资料,可以帮助学习者系统地学习和掌握C语言的基础知识和常用技巧,为他们未来的编程之路打下坚实的基石。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值