C语言标准 I/O小结 (感谢 simon_夏 的分享)

与文件I/O围绕文件描述符操作不同,标准I/O的操作是围绕流进行的。

流:

对于流,《C和指针》里有一段解释得很好:

ANSI C进一步对I/O的概念进行了抽象。就C程序而言,所有的I/O操作只是简单地从程序移进或移出字节的事情。因此,毫不惊奇的是,这种字节流便被称为流(stream)。程序只需要关心创建正确的输出字节数据,以及正确地解释从输入读取的字节数据。特定I/O设备的细节对程序员是隐藏的。

TCPL Appendix B.1中这么解释:

A stream is a source or destination of data that may be associated with a disk or other peripheral.(流是一个可能与硬盘或者其他设备关联的数据的源或者目的地)

简单地说,流是对信息的一种抽象。C系统在处理文件(文本文件和二进制文件)时,并不区分类型,都看成是字符流,按字节进行处理。
输入输出字符流的开始和结束只由程序控制而不受物理符号(如回车符)的控制。

流有最小的信息单元就是二进制位,含有最小的信息包就是字节,C标准库提供两种类型的流:二进制流(binary stream)和文本流(text stream)。二进制流是有未经处理的字节构成的序列;文本流是由文本行(每行有0个或多个字符,并以'\n'结束)组成的序列。注意在UNIX中,并不区别两种流。


当一个程序启动时,,标准输入、输出、出错三个流就已经被自动打开,并对应到默认的物理终端。这三个标准I/O流通过预定义(stdio.h)文件指针stdin,stdout,stderr加以引用当一个进程正常终止时(直接调用exit(),或从main返回)所有打开的标准I/O流都会被关闭,所有带未写缓冲数据的I/O流都会被冲洗。

PS:在main()中return(expr)等价于exit(expr),而exit则调用fclose()关闭每个文件描述符并刷洗对应缓存。

在Linux的应用程序中,通常用文件描述符0,1,2与标准输入,标准输出,标准出错输出相关联。为符合POSIX规范,在<unistd.h>中,0,1,2分别被替换成常量符号STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO。


缓冲区:

(详细内容可以参考APUE3.9和5.4,本段纯属摘抄)

标准I/O提供缓存的目的是尽可能减少使用read和write调用的数量(系统调用比普通函数调用开销大)。它也对每个I/O流自动地进行缓存管理,避免了应用程序需要考虑这一点所带来的麻烦。

标准I/O提供了三种类型的缓存:
(1) 全缓存。在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。对于驻在磁盘上的文件通常是由标准I/O库实施全缓存的。
(2) 行缓存。在这种情况下,当在输入和输出中遇到新行符时,标准I/O库执行I/O操作。这允许我们一次输出一个字符(用标准I/O fputc函数),但只有在写了一行之后才进行实际I/O操作。
(3) 不带缓存。标准I/O库不对字符进行缓存。如果用标准I/O函数写若干字符到不带缓存的流中,则相当于用write系统调用函数将这些字符写至相关联的打开文件上。


标准出错流stderr通常是不带缓存的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个新行字符。
ANSI C要求下列缓存特征:
(1) 当且仅当标准输入和标准输出并不涉及交互作用设备时,它们才是全缓存的。
(2) 标准出错决不会是全缓存的

但是,这并没有告诉我们如果标准输入和输出涉及交互作用设备时,它们是不带缓存的还是行缓存的,以及标准输出是不带缓存的,还是行缓存的。
SVR4和4.3 + BSD的系统默认使用下列类型的缓存:
• 标准出错是不带缓存的。
• 如若是涉及终端设备的其他流,则它们是行缓存的;否则是全缓存的。


可以通过下面的函数改变缓存类型(APUE5.4):

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. void setbuf(FILE *restrict fp, char *restrict buf);  
  2. int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);  
这些函数必须在流打开之后、但是未对流做任何操作之前被调用

参数buf通常指向一个长度为BUFSIZ的缓冲区,BUFSIZ在stdio.h中定义,可自行输出查看

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. stdio.h:    #ifndef BUFSIZ  
  2.         # define BUFSIZ _IO_BUFSIZ  
  3. libio.h:    #define _IO_BUFSIZ _G_BUFSIZ  
  4. _G_config.h:    #define _G_BUFSIZ 8192  
如要关闭缓冲,将buf置为NULL

Liunx上的默认情况是,当标准输入输出连接终端时是行缓冲的,缓冲区大小1024字节,重定向到普通文件时,他们变为全缓冲(APUE 5.12 程序5.3提供查看I/O相关信息的方法)


强制冲洗一个流

int fflush(FILE *fp)

使该流所有未写数据传送至内核。如fp为NULL,将使所有输出流被清洗。

应当注意的是:fflush(NULL)并不能有效地清空输入缓存。后面详细讨论


C陷阱与缺陷》5.3中讨论了一个setbuf使用中容易出现的错误

[cpp]  view plain copy
  1. #include<stdio.h>  
  2.   
  3. int main()  
  4. {  
  5.     int c;  
  6.   
  7.     char buf[BUFSIZ];  
  8.     setbuf(stdout, buf);  
  9.   
  10.     while ((c = getchar()) != EOF)  
  11.         putchar(c);  
  12.   
  13.     return 0;  
  14. }  
在setbuf之后,每次执行putchar,就把该字符写到buf里。只有当缓冲区满或者显式或隐式地调用fflush时,buf中的内容才实际写入到stdout中。

在本程序中,main()返回时,buf字符数组已被释放。

解决方法是:把buf声明为静态数组,或者定义到main之外。也可以动态分配buf的缓存,程序并不主动释放动态分配的缓存。

也可以直接在return前调用fflush。

常用I/O函数:

流打开:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. FILE * fopen ( const char * filename, const char * mode );  
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. FILE * freopen ( const char * filename, const char * mode, FILE * stream );  
常用freopen进行输入输出重定向。

单字符读写:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. int getc(FILE *fp)  
  2. int fgetc(FILE *fp)  
  3. int getchar(void)  
getchar等价于getc(stdin)。前两个函数区别在于,getc可被实现为宏,意味着fgetc调用时间略长。

不管是出错还是到达文件尾端,三个函数都返回-1.

在大多数实现中,FILE维护了两个标志:出错标志和文件结束标志。可用下面三个函数判断流是出错还是结束,最后一个函数是清除两标志:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. int ferror(FILE *fp)  
  2. int foef(FILE *fp)  
  3. void clearerr(FILE *fp)  

还有一个神奇的函数,可以把字符压送回流中:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. int ungetc(int c, FILE *fp)  
注意不能回送EOF

类似地,输出函数:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. int putc(int c, FILE *fp)  
  2. int fputc(int c, FILE *fp)  
  3. int putchar(int c)  
putchar(c)等价于putc(c,stdout),putc可被实现为宏。

通常为了避免过多的函数调用开销,putchar和getchar都被实现为宏。


行读写函数:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. char * fgets ( char * str, int num, FILE * stream );  
它会读取不超过num-1个字符,然后在末尾加上结束符 '\0' ,或者遇到换行符结束输入,同时换行符也被传入。

另一个函数:

char * gets(char* buf);

由于存在缓冲区溢出漏洞,不推荐使用。

相应地,输出

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. int fputs(const char * str, FILE * fp);  
  2. int puts(const char *char);  
fputs()将一个以NULL终止的字符串写到指定的流,终止符NULL不写出。
puts()虽然安全,但是他的不方便在于,每次将换行符写到输出。

TCPL里用getc和putc实现了一个简易的行读取,但这样增加了调用函数的开销,效率低下。通常做法是用汇编写的memccpy(3)实现。

因此,我们贯彻这样的方针,坚持使用fgets和fputs,并自己处理换行符。


各I/O函数的效率对比可参看APUE5.9


格式化I/O:

格式化输出:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. int printf ( const char * format, ... );  
  2. int fprintf (FILE *fp, const char * format, ... );  
  3. int snprintf (char *buf, size_t n, const char * format, ... );  

注意参数转换说明中 %[flags][fldwidth][lenmodifier]convtype,宽度和精度字段可被置为*,而后用一个整形参数指定其值。

printf的实现:

printf是C中为数不多的变参函数之一,主要通过stdarg.h中的一系列宏来对参数列表进行处理。其源码实现可以参看:点击打开链接点击打开链接

使用变参函数机制,简单模拟printf

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include<stdio.h>  
  2. #include<stdarg.h>  
  3. #include<string.h>  
  4. #include<unistd.h>  
  5.   
  6. void simon_printf(char *fmt, ...)  
  7. {  
  8.     char buf[10];  
  9.     char *p = fmt;  
  10.     char c_tmp, *s_tmp;  
  11.     int i_tmp;  
  12.     double f_tmp;  
  13.   
  14.     va_list  ap;  
  15.     va_start(ap, fmt);  
  16.   
  17.     while (*p)  
  18.     {  
  19.         if (*p != '%')  
  20.         {  
  21.             putchar(*p++);  
  22.             continue;  
  23.         }  
  24.         else   
  25.         {  
  26.             switch (*++p)  
  27.             {  
  28.                 case 'd':  
  29.                     {  
  30.                         i_tmp = va_arg(ap, int);  
  31.                 //      sprintf(buf, "%d", i_tmp);  
  32.                 //          write(STDOUT_FILENO, buf, strlen(buf));  
  33.                         printf("%d", i_tmp);  
  34.                         break;  
  35.                     }  
  36.                 case 'f'://float在内部被提升为double  
  37.                     {  
  38.                         f_tmp = va_arg(ap, double);  
  39.                         printf("%f", f_tmp);  
  40.                         break;  
  41.                     }  
  42.                 case 'c'://char在内部被提升为int  
  43.                     {  
  44.                         i_tmp = va_arg(ap, int);  
  45.                         printf("%c", i_tmp);  
  46.                         break;  
  47.                     }  
  48.                 case 's':  
  49.                     {  
  50.                         for(s_tmp = va_arg(ap, char*); *s_tmp; s_tmp++)  
  51.                             printf("%c", *s_tmp);  
  52.                         break;  
  53.                     }  
  54.             }  
  55.             p++;  
  56.         }  
  57.     }  
  58.   
  59.     va_end(ap);  
  60. }  
  61.   
  62. int main()  
  63. {  
  64.     int a = 1;  
  65.     float b = 2.0;  
  66.     char c = 'a';  
  67.     char *str = {"test"};  
  68.   
  69.     simon_printf("This is a test Message:\n int:%d\n float:%f\n string: %s\n char:%c\n ", a, b, str, c);  
  70.     return 0;  
  71. }  


格式化输入:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. int scanf(const char *format, ...);  
  2. int fscanf(FILE *fp, const char *format, ...);  
  3. int sscanf(const char *buf, const char *format, ...);  

scanf中*表示抑制,不把该输入赋值给对应变量,即跳过。

scanf()还有一些正则用法:[]表示输入字符集,可以使用连字符表示范围,scanf() 连续吃进集合中的字符并放入对应的字符数组,直到发现不在集合中的字符为止。用字符 ^ 可以说明补集。把 ^ 字符放为扫描集的第一字符时,构成其它字符组成的命令的补集合。

通常并不推荐使用scanf()的正则用法,用法复杂, 容易出错。编译器作语法分析时会很困难, 从而影响目标代码的质量和执行效率。


关于输入缓冲区清空:

1)fflush(NULL)

fflush的定义说得很清楚了,这种用法导致的结果不确定

If the given stream was open for writing (or if it was open for updating and the last i/o operation was an output operation) any unwritten data in its output buffer is written to the file.
If stream is a null pointer, all such streams are flushed.
In all other cases, the behavior depends on the specific library implementation. In some implementations, flushing a stream open for reading causes its input buffer to be cleared (but this is not portable expected behavior).
The stream remains open after this call.
When a file is closed, either because of a call to fclose or because the program terminates, all the buffers associated with it are automatically flushed.

如果stream指向输出流或者更新流(update stream),并且这个更新流最近执行的操作不是输入,那么fflush函数将把任何未被写入的数据写入stream指向的文件(如标准输出文件stdout)。
fflush(NULL)清空所有输出流和上面提到的更新流。
否则,fflush函数的行为是不确定的。取决于编译器,某些编译器(如VC6)支持用 fflush(stdin) 来清空输入缓冲,而gcc就不支持。

2)setbuf(stdin, NULL);
setbuf(stdin, NULL);是使stdin输入流由默认缓冲区转为无缓冲区,在没有特殊要求的情况下还是适用的


3)int c;
while((c = getchar()) != '\n' && c != EOF);
由代码知,不停地使用getchar()获取缓冲区中字符,直到获取的字符c是换行符’\n’或者是文件结尾符EOF为止。这个方法可以完美清除输入缓冲区,并且具备可移植性。


4)scanf("%[^\n]%*c");
这里用到了scanf格式化符中的“*”,即赋值屏蔽;“%[^集合]”,匹配不在集合中的任意字符序列。这也带来个问题,缓冲区中的换行符’\n’会留下来,需要额外操作来单独丢弃换行符。


关于EOF

EOF并非字符,只是表示文件结束。

CSAPP 10.1里这么描述:
一个读操作就是从文件拷贝n>0个字节到存储器,从当前文件位置k开始,增加k到k+n,给定文件大小m字节,当k>=m时执行读操作会触发一个EOF条件,应用程序能检测到这个条件。文件结尾处并没有明确的“EOF符号”

如果是终端键盘输入作为标准输入,linux里用ctrl+d,win下用ctrl+z。

要注意的是:getchar是以行为单位进行存取,在getchar提示新的一次输入时,EOF的输入方式如下:
1)输入一行字符后,回车,再接ctrl+D
2)输入一行字符后,连按两次ctrl+D(如果在一行的中间按下Ctrl-D,则表示输出"标准输入"的缓存区,所以这时必须按两次Ctrl-D)
3)没有任何输入的情况下,直接ctrl+D

可以总结为:当终端有字符输入时,Ctrl+D产生的EOF相当于结束本行的输入。EOF作为行结束符时的情况,这时候输入Ctrl+D并不能结束getchar(),而只能引发getchar()提示下一轮的输入。


可以参考下面博文

EOF是什么---阮一峰
EOF

Definition of EOF and how to use it effectively


关于freopen

打开后想恢复,在linux里可以这样

freopen ("/dev/tty", "a", stdout)
跨平台的方法暂时没有

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值