Linux标准I/O概述
文章目录
1.0 标准I/O 的由来
标准IO是指ANSI C(ANSI C是美国国家标准协会(ANSI)对C语言发布的标准)中定义的用于I/O操作的一系列函数。
只要操作系统中安装了C库,标准I/O函数就可以调用,换句话说,如果程序中使用的是标准I/O函数,那么源代码不需要修改就可以在其它操作系统下编译运行,具有更好的可移植性
在执行系统调用时,Linux必须从用户态切换到内核态,处理相应的请求,然后再返回到用户态。如果频繁执行必定增加系统开销,为避免这种情况,标准I/O使用时在用户空间创建缓冲区,读写实先操作缓冲区,再通过系统调用访问世纪的文件,而减少系统调用的次数。
1.2 流的含义
标准I/O的核心对象就是流。那流到底是什么呢?
当使用标准I/O打开一个文件时,就会创建一个FILE结构体,这个结构体用来描述改文件(可以理解为创建一个FILE结构体和实际打开的文件进行关联起来),我们把这个FILE结构体就称为流,标准I/O函数都基于流进行所有的操作。
1.3 标准I/O的缓冲机制
在前面我们提到了标准I./O是有缓冲机制的,缓冲区创建于用户空间,那么缓冲的具体类型有哪些呢?
其实,标准I/O中流的缓冲类型有3种:
1、全缓冲: 当填满标准I/O缓冲区后再进行实际的I/O操作。
对于存放在磁盘上的普通文件,标准I/O打开的默认就是全缓冲。当缓冲区已满或者执行FULSH操作室才会进行磁盘操作。
2、行缓冲:当在输入和输出中遇到换行符时执行I/O操作。
标准输入流和输出流就是使用行缓冲,想想printf();
的格式,就是遇到\n
后进行输出,行缓冲。
3、无缓冲: 不对I/O操作进行缓冲。
即对流的读写时会立刻操作实际的文件。标准出错流是不带缓冲的,理论上所有出错输出都不应该是缓冲的,因为出错就应该立刻输出。
1.4 标准I/O编程
本节只介绍最常用的函数。本就所讨论的I/O操作都是基于流的,它包含ANSI C的标准。
首先扔一个问题,想想 open() 与 fopen()的区别?
1.4.1 流的打开
使用标准I/O打开的函数有fopen(),fdopen(),freopen()。它们可以用不同的模式打开文件,每一个都返回一个指向FILE的指针。 此后,对文件的读写都通过这个FILE指针来进行。
fopen():可以指定打开文件的路径和模式;
fdopen():可以指定打开文件的描述符和模式;
freopen():可以指定打开的文件和模式,还可以指定特定的 I/O 流;
注意:给环境变量赋值需要用 export
, 而不是直接path = "********"
有些人可能会疑惑 “b”
是干嘛的,其实在每个选项中加入 b
字符是用来告诉函数库打开的文件是二进制文件,不是纯文本文件,不过在linux系统中会忽略该符号。
注意: 当用户进程运行时,系统会自动打开 3 个流: 标准输入流stdin;标准输出流stdout;标准错误流 stderr。
标准输入流stdin:用来从标准输入设备(默认键盘)中读取输入内容;
标准输出流stdout:用来从标准输出设备(默认当前终端)中输出内容;
标准错误流 stderr:用来向标准错误设备(默认当前终端)中输出错误信息;
1.4.2 流的关闭
关闭流的函数为 fclose()
;该函数将流的缓冲区的数据全部写入文件中,并释放相关资源。
注意:程序结束时,会自动关闭所有的打开的流
1.4.3 错误信息
标准 I/O 函数执行时如果出现错误,会把错误码保存在全局变量 errno
中,程序员可以打印错误信息,处理错误的相关函数:1. perror(); 2. strerror()
perror():
其实,perror()
函数的使用还是比较简单的,在之前的错误信息输出中很多同学采用的是标记性输出,printf("************");
这种方式,这样可以利用输出的信息来断点在某个区间,但无法获取准确的错误信息,采用perror()
可以得到详细的错误原因。
来,咱不止说,拿个例子试试看。
#include <stdio.h>
int main(int argc, char *argv[])
{
FILE *fp; //指定流指针
if ( NULL == ( fp = fopen( "a.txt","r"))) // NULL为系统宏,用流指针接收 fopen()的返回值;
{
perror(" fail to open");
return -1;
}
fclose(fp);
return 0;
}
接下来,我们看看实际的效果
错误信息分析:
fail to open: No such file or directory
错误处理相关函数 :
2. strerror()
同样的,我们拿个例子试试手,看看怎么操作,什么结果。
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[])
{
FILE *fp; //指定流指针
if ( NULL == ( fp = fopen( "AA.txt", "r")))
{
printf(" fail to fopen: %s\n", strerror(errno));
return -1;
}
fclose(fp);
return 0;
}
在当前路径下,并没有“AA.txt”
文件。看看结果:
根据这两个错误信息的打印,能够知道这俩个都可以将具体的错误原因进行罗列输出。
1.4.4 流的写入
- 按照字符(字节)输入
字符输入 / 输出函数一次仅读写一个字符。要点总结一下,如下:
字符输入函数原型:
1. int getc ( FILE * stream);
2. int fgetc ( FILE * stream);
3. int getchar ( void );
getc()
和fgetc()
从指定的流中读取一个字符(节),getchar()
从stdin
中读取一个字符(节)。 - 按照字符(字节)输出
字符输入函数原型:
1. int putc ( FILE * stream);
2. int fputc ( FILE * stream);
3. int putchar ( void );
putc()
和fputc()
从指定的流中读取一个字符(节),putchar()
向stdout
中输出一个字符(节)。
同样,我们还是简单的实验一下,结合fputc() 和 fgetc()尝试一下。
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[])
{
int c;
while( 1 ) //循环
{
c = fgetc (stdin); // 从键盘读取一个字符
if (( '0' < c) && ( '9' >= c) ) // 字符判断是否为数字
{
fputc( c,stdout); // 满足标准输出
}
else if( '\n' == c) // 当遇到‘\n’时跳出
{
break;
}
}
return 0;
}
运行后输入1321rrerf
,最终结果:1321
。
-
按照行输入
行输入/输出函数一次操作一行。
行输入函数总结一波如下:
1. char * gets( char * s);
2. char * fgets( char *s, int size, FILE* stream);
注意:gets()函数不安全,容易造成缓冲区溢出 ,不建议使用。(原因:由于gets()不检查字符串string的大小,必须遇到换行符或文件结尾才会结束输入,因此容易造成缓存溢出的安全性问题,导致程序崩溃,可以使用fgets()代替。)
fgets()从指定的流中读取一个字符,当遇到换行符 \n 时,会读取 \n 或者读取sizee -1个字符后返回。
fgets()不能保证每次都能读取到一行。 -
行输出函数
1. int puts( const char * s);
2. int fputs( const char * s, FILE * stream);
惯例哈,思考:怎么获取一个文本文件的行数。文后提供代码 -
指定大小为单位读写文件
在文件流被打开以后,可对文件流按照指定大小为单位进行读写操作。这里简单介绍一下,毕竟其他都差不多,只是函数参数和返回值的问题
函数:1. fread() ;2. fwrite();
fread():
fwrite();
1.4.5 流的定位
每个打开的流内部都有一个当前读写位置。留在打开时,当前读写位置为 0,表示文件的开始位置,每读写一次后,当前读写位置自动增加实际读写的大小,在读写流之间可先对流进行定位,即移动到当前指定的位置再进行操作。
函数1. fseek()
, 2. ftell();
fseek()
:
ftell():
惯例哈,思考:怎么获取一个文件的大小。文后提供代码
好了,今天我们就将Linux标准I/O编程进行了简单的介绍,其中很多东西还需要朋友们自己去消化,比如偏移量是什么,基准值是什么,缓冲区在用户空间什么位置,怎么确定缓冲区到底多大等等,篇幅问题就不过多介绍。
1.6 总结
接下来,我们进行两个问题的解答:
1、 怎么计算一个文本文件的行数,比如算一下当前路径下 1.txt
有多少行。
首先拿到题目,看目标,计算文本文件的行数,那么,回忆一下用什么行数可以来实现呢?
——行输入函数
可是行输入函数有 gets()
和 fgets()
,到底用什么呢?
——fgets()
那为什么要选择fgets()
而不用gets()
呢?
前面已经都介绍过了,自己回忆回忆,就不再重复介绍了。这道题我们用fgets()
来实现一下。
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[])
{
int line = 0; // 保存具体的行数
char buf[128]; // 用数组来装输入的字符串,128字节大小
FILE *fp; // 指定流
if( argc < 2) // 参数个数,其实就是要求文件和 a.out一起执行
{
printf( "Usage: %s<file>\n", argv[0]);
return -1;
}
if ( NULL == ( fp = fopen( argv[1], "r"))) // 判断是否正常打开文件
{
perror( "fail to open");
return -1;
}
while( NULL != fgets( buf, 128, fp)) // 循环读取,buf是数组名,也是首地址
{
if ( '\n' == buf[ strlen(buf) -1]) // 换行符即代表着新的一行了嘛,这就是行数增加的依据
{
line++;
}
}
printf(" The lines of %s is %d \n",argv[1],line);
return 0;
}
来来来,运行一下试试。在当前路径下创建一个 1. txt
,然后联合a.out
运营一下看看实际效果
2、怎么计算一个文件的大小。
思路理一理:
想想,文件的大小,这是什么玩意?
文件大小是什么意思?
大小,在计算机中指的是什么?
单位是什么?
这个单位是最小的单位吗?
如果不是,那和最小的单位换算关系是什么?
最小的单位又是什么?
最小的单位怎么计算呢?
来来来,继续哈,想清楚了就继续搞起来。
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[])
{
FILE *fp;
if ( argc <2)
{
printf( "Usage: %s<file>\n ", argv[0]);
return -1;
}
if ( NULL == (fp = fopen( argv[1], "r")))
{
perror(" fail to open");
return -1;
}
fseek(fp, 0, SEEK_END);
printf( " The size of %s is %d k \n", argv[1],ftell(fp)) ;
return 0;
}
fseek()函数在前面流的定位中已经讲过相关参数,请理解偏移量,基准值。