热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

cchar清空_[C陷阱与缺陷](五)库函数

码字不易,对你有帮助点赞转发关注支持一下作者微信搜公众号:不会编程的程序圆看更多干货,获取第一时间更新代码,练习上传至&#x
7b524ddad8461fb26aa1eaf16bbe1025.png
码字不易,对你有帮助 点赞/转发/关注 支持一下作者
微信搜公众号:不会编程的程序圆看更多干货,获取第一时间更新
代码,练习上传至:https://github.com/hairrrrr/C-CrashCourse

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

有关库函数的使用,我们能给出的最好建议是尽量使用系统头文件。

一 库函数

1. 返回整数的 getchar 函数

#includemain(void){char c;while((c = getchar()) != EOF)putchar(c);
}

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

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

因此,最终结果存在两种可能。一种可能是,某些合法的输入字符在被“截断”后使得 c 的取值与 EOF 相同;另一种可能是, c 根本不可能取到EOF这个值。对于前一种情况,程序将在文件复制的中途终止;对于后一种情况,程序将陷入一个死循环。

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

2. 更新顺序文件

许多系统中的标准输入/输出库都允许程序打开一个文件,同时进行写入和读出的操作:

FILE *fp;
fp = open(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函数调用,所以无法进行上述操作。解决的办法是把这段代码改写为:

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, 0L, 1);}
}

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

程序圆帮你理解:

  • &rec为何要强转成 char*类型:这就要理解 fread 函数(size_t fread ( void * ptr, size_t size, size_t count, FILE * stream )):fread 函数的参数有四个,简单的来说就是:从 stream 中读 count 个 size 大小的元素到 ptr 指向的内存中。而 fread 内部在读取一个 size 大小的元素时会调用 size 次 fgetc 函数,所以我猜测是每次用 fgetc 函数读一个字节然后将该值赋给 ptr 指向的那个地址。既然 fgetc 每次只能读一个,那也应该将 ptr 强转为 char* 类型。(但是函数原型是 void* 类型,会发生实参提升,转成 void*,这又是个问题了)。
  • 其实上面的程序可以简化为:
    fread();
    fseek();
    fwrite();
    fread();
    我们知道,读写之间需要调用一次 fseek,这就是为什么要在 fwrite 后调用 fseek 了。

3.缓冲输出 与内存分配

当一个程序生成输出时,是否有必要将输出立即展示给用户?这个问题的答案根据不同的程序而定。

程序输出有两种方式:一种是即时处理方式,另一种是先暂存起来,然后再大块写入的方式,前者往往造成较高的系统负担。因此,C语言实现通常都允许程序员进行实际的写操作之前控制产生的输出数据量。

这种控制能力一般是通过库函数 setbuf 实现的。如果buf是一个大小适当的字符数组,那么

setbuf(stdout, buf);

语句将通知输入/输出库,所有写入到 stdout 的输出都应该使用 buf 作为输出缓冲区,直到 buf 缓冲区被填满或者程序员直接调用 flush (译注:对于由写操作打开的文件,调用 fflush 将导致输出缓冲区的内容被实际地写入该文件),buf 缓冲区中的内容才实际写入到stdout 中。缓冲区的大小由系统头文件中的 BUFSIZ 定义。

程序圆帮你理解: setbuf 比较老,现在可以用 C99 引入的函数 setvbuf

下面的程序的作用是把标准输入的内容复制到标准输出中,演示了setbuf 库函数最显而易见的用法:

#include 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[BUFSIZ];

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

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

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

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

4. 使用errno检测错误

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

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

出错原因在于,在库函数调用没有失败的情况下,并没有强制要求库函数一定要设置 errno 为0,这样errno 的值就可能是前一个执行失败的库函数设置的值。

下面的代码作了更正,似乎能够工作,很可惜还是错误的:

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

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

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

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

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

5. 库函数 signal

关于 signal 函数使用需要避免的情况:

  • 信号处理函数不应该调用复杂的库函数(例如:malloc)
    例如,假设malloc函数的执行过程被一个信号中断。 此时,malloc 函数用来跟踪可用内存的数据结构很可能只有部分被更新。如果 signal 处理函数再调用 malloc 函数,结果可能是 malloc 函数用到的数据结构完全崩溃,后果不堪设想!
  • 从 siganl 函数中使用 longjup 退出
    基于同样的原因,从 signal 处理函数中使用 longjmp 退出,通常情况下也是不安全的:因为信号可能发生在 malloc 或者其他库函数开始更新某个数据结构,却又没有最后完成的过程中。因此,signal 处理函数能够做的安全的事情,似乎就只有设置一个标志然后返回,期待以后主程序能够检查到这个标志,发现一个信号已经发生。
  • 算数运算错误
    然而,就算这样做也并不总是安全的。当一个算术运算错误(例如溢出或者零作除数)引发一个信号时,某些机器在signal 处理函数返回后还将重新执行失败的操作。而当这个算术运算重新执行时,我们并没有一个可移植的办法来改变操作数。这种情况下,最可能的结果就是马上又引发一个同样的信号。因此,对于算术运算错误,signal 处理函数的惟一安全、 可移植的操作就是打印一条出错消息,然后使用 longjmp 或 exit 立即退出程序。

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

练习

练习5-1

当一个程序异常终止时,程序输出的最后几行常常会去失,原因是什么?我们能够采取怎样的措施来解决这个问题?

一个异常终止的程序可能没有机会来清空其输出缓冲区。

解决方案就是在调试时强制不允许对输出进行缓冲。要做到这一点,不同的系统有不同的做法,这些做法虽然存在细微差别,但大致如下:

setbuf(stdout, (char *)0);

这个语句必须在任何输出被写入到 stdout(包括任何对 printf 函数的调用)之前执行。该语句最恰当的位置就是作为main函数的第一个语句。

练习5-2

下 面程序的作用是把它的输入复制到输出:

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

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

#define EOP -1
main()
{register int c;while ((c = getchar()) != EOF)putchar (c) ;
}

这个程序在许多系统中仍然能够运行,但是在某些系统运行起来却慢得多。这是为什么?

函数调用需要花费较长的程序执行时间,因此getchar经常被实现为宏。这个宏在stdio.h头文件中定义,因此如果一个程序没有包含 stdio.h 头文件,编译器对 getchar 的定义就一无所知。 在这种情况下,编译器会假定 getchar 是一个返回类型为整型的函数。

实际上,很多C语言实现在库文件中都包括有 getchar 函数,原因部分是预防编程者粗心大意,部分是为了方便那些需要得到 getchar 地址的编程者。因此,程序中忘记包含 stdio.h 头文件的效果就是,在所有 getchar 宏出现的地方,都getchar 函数调用来替换 getchar 宏。这个程序之所以运行变慢,就是因为函数调用所导致的开销增多。同样的依据也完全适用于putchar 。

参考资料:《C 缺陷与陷阱》


以上就是本次的内容,感谢观看。

如果文章有错误欢迎指正和补充,感谢!

最后,如果你还有什么问题或者想知道到的,可以在评论区告诉我呦,我在后面的文章可以加上。

最后,关注我,看更多干货!

我是程序圆,我们下次再见。



推荐阅读
  • 在软件开发过程中,经常需要将多个项目或模块进行集成和调试,尤其是当项目依赖于第三方开源库(如Cordova、CocoaPods)时。本文介绍了如何在Xcode中高效地进行多项目联合调试,分享了一些实用的技巧和最佳实践,帮助开发者解决常见的调试难题,提高开发效率。 ... [详细]
  • Android 构建基础流程详解
    Android 构建基础流程详解 ... [详细]
  • Java Socket 关键参数详解与优化建议
    Java Socket 的 API 虽然被广泛使用,但其关键参数的用途却鲜为人知。本文详细解析了 Java Socket 中的重要参数,如 backlog 参数,它用于控制服务器等待连接请求的队列长度。此外,还探讨了其他参数如 SO_TIMEOUT、SO_REUSEADDR 等的配置方法及其对性能的影响,并提供了优化建议,帮助开发者提升网络通信的稳定性和效率。 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 在处理 XML 数据时,如果需要解析 `` 标签的内容,可以采用 Pull 解析方法。Pull 解析是一种高效的 XML 解析方式,适用于流式数据处理。具体实现中,可以通过 Java 的 `XmlPullParser` 或其他类似的库来逐步读取和解析 XML 文档中的 `` 元素。这样不仅能够提高解析效率,还能减少内存占用。本文将详细介绍如何使用 Pull 解析方法来提取 `` 标签的内容,并提供一个示例代码,帮助开发者快速解决问题。 ... [详细]
  • 字符串学习时间:1.5W(“W”周,下同)知识点checkliststrlen()函数的返回值是什么类型的?字 ... [详细]
  • 在分析Android的Audio系统时,我们对mpAudioPolicy->get_input进行了详细探讨,发现其背后涉及的机制相当复杂。本文将详细介绍这一过程及其背后的实现细节。 ... [详细]
  • 网站访问全流程解析
    本文详细介绍了从用户在浏览器中输入一个域名(如www.yy.com)到页面完全展示的整个过程,包括DNS解析、TCP连接、请求响应等多个步骤。 ... [详细]
  • [转]doc,ppt,xls文件格式转PDF格式http:blog.csdn.netlee353086articledetails7920355确实好用。需要注意的是#import ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 本文总结了一些开发中常见的问题及其解决方案,包括特性过滤器的使用、NuGet程序集版本冲突、线程存储、溢出检查、ThreadPool的最大线程数设置、Redis使用中的问题以及Task.Result和Task.GetAwaiter().GetResult()的区别。 ... [详细]
  • 秒建一个后台管理系统?用这5个开源免费的Java项目就够了
    秒建一个后台管理系统?用这5个开源免费的Java项目就够了 ... [详细]
  • 如何将TS文件转换为M3U8直播流:HLS与M3U8格式详解
    在视频传输领域,MP4虽然常见,但在直播场景中直接使用MP4格式存在诸多问题。例如,MP4文件的头部信息(如ftyp、moov)较大,导致初始加载时间较长,影响用户体验。相比之下,HLS(HTTP Live Streaming)协议及其M3U8格式更具优势。HLS通过将视频切分成多个小片段,并生成一个M3U8播放列表文件,实现低延迟和高稳定性。本文详细介绍了如何将TS文件转换为M3U8直播流,包括技术原理和具体操作步骤,帮助读者更好地理解和应用这一技术。 ... [详细]
  • V8不仅是一款著名的八缸发动机,广泛应用于道奇Charger、宾利Continental GT和BossHoss摩托车中。自2008年以来,作为Chromium项目的一部分,V8 JavaScript引擎在性能优化和技术创新方面取得了显著进展。该引擎通过先进的编译技术和高效的垃圾回收机制,显著提升了JavaScript的执行效率,为现代Web应用提供了强大的支持。持续的优化和创新使得V8在处理复杂计算和大规模数据时表现更加出色,成为众多开发者和企业的首选。 ... [详细]
  • 深入探索HTTP协议的学习与实践
    在初次访问某个网站时,由于本地没有缓存,服务器会返回一个200状态码的响应,并在响应头中设置Etag和Last-Modified等缓存控制字段。这些字段用于后续请求时验证资源是否已更新,从而提高页面加载速度和减少带宽消耗。本文将深入探讨HTTP缓存机制及其在实际应用中的优化策略,帮助读者更好地理解和运用HTTP协议。 ... [详细]
author-avatar
mobiledu2502861377
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有