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 处理函数尽可能地简单,并将它们组织在一起。这样,当需要适应一个新系统时,我们可以很容易地进行修改。
 


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/785801.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

求组合背包II(acwing)

题目描述&#xff1a; 给定n组循问&#xff0c;每组询问给定两个整数a&#xff0c;b&#xff0c;请你输出Ca^b mod (1e9 7)的值&#xff0c;。 输入格式&#xff1a; 第一行包含整数n。 接下来2行&#xff0c;每行包含一组a和b。 输出格式&#xff1a; …

类的函数成员(三):拷贝构造函数

一.什么是拷贝构造函数&#xff1f; 1.1 概念 同一个类的对象在内存中有完全相同的结构&#xff0c;如果作为一个整体进行复制或称拷贝是完全可行的。这个拷贝过程只需要拷贝数据成员&#xff0c;而函数成员是共用的&#xff08;只有一份拷贝&#xff09;。 在建立对象…

深入详解MongoDB索引的数据组织结构

MongoDB&#xff0c;作为最受欢迎的NoSQL数据库之一&#xff0c;以其灵活的数据模型和强大的性能而著称。其中&#xff0c;索引是提高MongoDB查询性能不可或缺的一部分。本文将更加深入地探讨MongoDB索引的数据组织结构&#xff0c;揭示其背后的工作原理和优化策略。 目录 一、…

ngrok使用心得记录

1&#xff0c;官网 https://ngrok.com/ 注册(Sign up for free)账号&#xff0c;这里我使用邮箱注册&#xff0c;本来使用github账号登录的&#xff0c;不过需要一个二次短信确认&#xff0c;而且发短信前要先选择国家&#xff0c;国家列表里没有China86&#xff0c;所以只能选…

每天学点儿Python(3) -- for循环

for循环结构格式如下 for 循环变量 in 遍历对象:语句块 举例一、 for i in "Hello"print(i) 执行结果如下 举例二、 #打印100-999之间的水仙花数 #注意&#xff1a;Python中 / 除法&#xff0c;运输后为浮点数, // 为取除法后的整数&#xff0c;而不是C/C中的注释…

Java与Go的并发世界:理解Work Sharing与Work Stealing

概述 最近在理解Golang中的Per P概念&#xff0c;于是我就去Go的源码中挖呀挖&#xff0c;结果挖到了Go的调度器设计。 Golang的调度器设计文档提到了Go中的P(OS线程)调度器使用的是work-stealing调度算法论文。 论文中提到了两个多线程调度算法&#xff1a;work sharing和wor…

【计算机考研】408会炸,还是自命题会炸

自命题是有没有学上的问题。 我记得去年九月一些学校宣布改考408的时候&#xff0c;整个群里都炸了&#xff0c;同学一片哀嚎。要知道九月的时候要重新准备408肯定是不可能了&#xff0c;一来408复习的基础阶段已经过去了&#xff0c;二来英语政治都加入战场了&#xff0c;复习…

电力设备热设计原理(二)

本篇为西安交通大学本科课程《电力设备设计原理》的笔记。 本篇为这一单元的第二篇笔记。上一篇传送门。 电力设备传导换热 主要讨论稳态导热的计算。 通过单层和多层平壁的传导 如上图所示的大平板是一维传导问题&#xff0c;流过平板的热流量和平板两侧温度和平板厚度之间…

vue2使用axios封装请求数据

1、在完成下面的步骤之前&#xff0c;先脚手架创建vue项目&#xff0c;然后再vue项目当中&#xff0c;首先先创建一个文件夹utils&#xff0c;里面放request.js的文件 (1)下载 npm i axios先下载好axios请求依赖 (2)下面的这个文件&#xff0c;包括封装请求&#xff0c;以及拦截…

Docker搭建私有镜像仓库

1.Docker镜像仓库 搭建镜像仓库可以基于Docker官方提供的DockerRegistry来实现。 官网地址&#xff1a;https://hub.docker.com/_/registry 1.1.简化版镜像仓库 Docker官方的Docker Registry是一个基础版本的Docker镜像仓库&#xff0c;具备仓库管理的完整功能&#xff0c;…

c++的学习之路:6、类和对象(2)

一、 构造函数 如果一个类什么成员都没有&#xff0c;那么他是一个空类吗&#xff1f;在c的创建时&#xff0c;就规定了在类没有成员时&#xff0c;也会有六个默认的成员&#xff0c;简称6个默认成员函数&#xff0c;如下图所示 先介绍一下构造函数&#xff0c;这里就利用代码…

第四百三十六回

文章目录 1. 概念介绍2. 思路与方法2.1 实现思路2.2 实现方法 3. 示例代码4. 内容总结 我们在上一章回中介绍了"不同平台上换行的问题"相关的内容&#xff0c;本章回中将介绍如何在页面上显示蒙板层.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我…

鸿蒙OS开发实例:【ArkTS类库多线程CPU密集型任务TaskPool】

CPU密集型任务是指需要占用系统资源处理大量计算能力的任务&#xff0c;需要长时间运行&#xff0c;这段时间会阻塞线程其它事件的处理&#xff0c;不适宜放在主线程进行。例如图像处理、视频编码、数据分析等。 基于多线程并发机制处理CPU密集型任务可以提高CPU利用率&#x…

AMD GPUs - Radeon™ PRO W7900与NVIDIA 4000系列GPU性能

文心一言 RTX 4090的性能高于AMD Radeon PRO W7900。 RTX 4090具有760亿个晶体管、16384个CUDA核心和24GB高速镁光GDDR6X显存&#xff0c;在4K分辨率的游戏中持续以超过100FPS运行。RTX 4090采用全新的DLSS 3技术&#xff0c;相比3090TI&#xff0c;性能提升可达2~4倍&#x…

STM32F103 CubeMX 使用USB生成键盘设备

STM32F103 CubeMX 使用USB生成键盘设备 基础信息HID8个数组各自的功能 生成代码代码编写添加申明信息main 函数编写HID 修改1. 修改报文描述符2 修改 "usbd_hid.h" 中的申明文件 基础信息 软件版本&#xff1a; stm32cubmx&#xff1a;6.2 keil 5 硬件&#xff1a;…

超分辨率(4)--基于A2N实现图像超分辨率重建

一.项目介绍 已有研究表明&#xff0c;注意力机制对高性能超分辨率模型非常重要。然而&#xff0c;很少有工作真正讨论“为什么注意力会起作用&#xff0c;它又是如何起作用的”。 文章中尝试量化并可视化静态注意力机制并表明&#xff1a;并非所有注意力模块均有益。提出了…

vue3+threejs新手从零开发卡牌游戏(二十):添加卡牌被破坏进入墓地逻辑

在game目录下新建graveyard文件夹存放墓地相关代码&#xff1a; game/graveyard/p1.vue&#xff0c;这里主要设置了墓地group的位置&#xff1a; <template><div></div> </template><script setup lang"ts"> import { reactive, ref,…

Python入门(八)

引入 引入函数 为了减少代码的冗余&#xff0c;减轻我们的工作量&#xff0c;我们常常将代码分块编写&#xff0c;在Python中更是如此&#xff0c;那么我们怎么在一个新的程序文件中调用我们已经编写好程序文件的函数&#xff0c;我们使用import。我们先写一个first.py为例语…

WinForm_初识_事件_消息提示

文章目录 WinForm开发环境的使用软件部署的架构B/S 架构应用程序C/S 架构应用程序 创建 Windows 应用程序窗口介绍查看设计窗体 Form1.cs 后台代码窗体 Form1.cs窗体的常用属性 事件驱动机制事件的应用事件的测试测试事件的级联响应常用控件的事件事件响应的公共方法 消息提示的…

CCIE-02-PPPoE

目录 实验条件网络拓朴实验目标 开始配置R1验证效果 实验条件 网络拓朴 实验目标 R2为PPPoE Server&#xff0c;已预配了相关信息&#xff1b;R1作为PPPoE Client&#xff0c;进行PPPoE拨号 用户名为R1&#xff0c;密码为cisco &#xff0c; 采用CHAP的认证方式&#xff0c;I…