[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 地址的编程者。
[C陷阱和缺陷] 第5章 库函数的更多相关文章
- [C陷阱和缺陷] 第1章 词法“陷阱”
有感自己的C语言在有些地方存在误区,所以重新仔细把"C陷阱和缺陷"翻出来看看,并写下这篇博客,用于读书总结以及日后方便自身复习. 第1章 词法"陷阱" 1.1 ...
- [C陷阱和缺陷] 第3章 语义“陷阱”
第3章 语义"陷阱" 一个句子哪怕其中的每个单词都拼写正确,而且语法也无懈可击,仍然可能有歧义或者并非书写者希望表达的意思.程序也有可能表面上是一个意思,而实际上的意思却相 ...
- [C陷阱和缺陷] 第7章 可移植性缺陷
C语言在许多不同的系统平台上都有实现.的确,使用C语言编写程序的一个首要原因就是,C程序能够方便地在不同的编程环境中移植. 不同的系统有不同的需求,因此我们应该能够预料到,机器不同则其上的C语 ...
- [C陷阱和缺陷] 第2章 语法“陷阱”
第2章 语法陷阱 2.1 理解函数声明 当计算机启动时,硬件将调用首地址为0位置的子例程,为了模拟开机时的情形,必须设计出一个C语言,以显示调用该子例程,经过一段时间的思考,得出语句如下: ( * ...
- [C陷阱和缺陷] 第6章 预处理器
在严格意义上的编译过程开始之前,C语言预处理器首先对程序代码作了必要的转换处理.因此,我们运行的程序实际上并不是我们所写的程序.预处理器使得编程者可以简化某些工作,它的重要性可以由两个主要的原因说 ...
- [C陷阱和缺陷] 第4章 连接
一个C程序可能是由多个分别编译的部分组成,这些不同部分通过连接器合并成一个整体.在本章中,我们将考查一个典型的连接器,注意它是如何对C程序进行处理的,从而归纳出一些由于连接器的特点而可能导致的错误. ...
- 读书笔记--C陷阱与缺陷(七)
第七章 1.null指针并不指向任何对象,所以只用于赋值和比较运算,其他使用目的都是非法的. 误用null指针的后果是未定义的,根据编译器各异. 有的编译器对内存位置0只读,有的可读写. 书中给出了一 ...
- 我的《C陷阱与缺陷》读书笔记
第一章 词法“陷阱” 1. =不同于== if(x = y) break; 实际上是将y赋给x,再检查x是否为0. 如果真的是这样预期,那么应该改为: if((x = y) != 0) break; ...
- 阅读《C陷阱与缺陷》的知识增量
版权声明:本文为Focustc原创文章.转载请注明作者及出处. https://blog.csdn.net/caozhankui/article/details/35925939 看完<C陷阱与 ...
随机推荐
- 概率dp呜呜
概率dp有环怎么办? 答案可劲迭代 ,然后可劲消元 , 怎么消? 我就不知道了. 呵呵
- Java 学习(5):修饰符 运算符
目录 --- 修饰符 --- 运算符 --- 循环结构 --- 分支结构 修饰符: 修饰符用来定义类.方法或者变量,通常放在语句的最前端.如下: public class className { // ...
- next_permitation
了解一个C++ STL的函数 next_permitation 可用于生成全排列 如下例子 #include <iostream> #include <stdio.h> #in ...
- hdu - 1565 方格取数(1) && 1569 方格取数(2) (最大点权独立集)
http://acm.hdu.edu.cn/showproblem.php?pid=1565 两道题只是数据范围不同,都是求的最大点权独立集. 我们可以把下标之和为奇数的分成一个集合,把下标之和为偶数 ...
- Redis集群方案之主从复制(待实践)
Redis有主从复制的功能,一台主可以有多台从,从还可以有多台从,但是从只能有一个主.并且在从写入的数据不会复制到主. 配置 在Redis中,要实现主从复制架构非常简单,只需要在从数据库的配置文件中加 ...
- WebLogic11g-创建域(Domain)及基本配置
最近看到经常有人提问weblogic相关问题,所以闲暇之际写几篇博文(基于weblogic11),仅供大家参考. 具体weblogic的介绍以及安装,这里就不赘述了. 以域的创建开篇,虽然简单,但 ...
- Web端口复用正向后门研究实现与防御
0×01背景 现在的很多远控/后门因为目前主流防火墙规则的限制,基本上都采用TCP/UDP反弹回连的通讯形式:但是在较高安全环境下,尤其负责web相关业务的环境,因为安防设备(防火墙,IDS,IPS等 ...
- MariaDB ----单表查询
1>按一定条件查询某字段的数据 (where) ; ( 查询 id > 的数据) #补充: ; 注意“ select * from students1: (此命令需谨慎使用, 数据量大 ...
- Node后台使用mysql并开启事务
如题:node后台使用mysql数据库,并使用事务来管理数据库操作. 这里主要讲一个事务的封装并写了一个INSERT 插入操作. code: 基础code: db.config.js const my ...
- python开发【第4篇】【进程、线程、协程】
一.进程与线程概述: 进程,是并发执行的程序在执行过程中分配和管理资源的基本单位,每一个进程都有一个自己的地址空 间. 线程,是进程的一部分,一个没有线程的进程可以被看作是单线程的.线程有时又被称为轻 ...