文章目录
文件操作
一、基本概念
Linux的哲学是“Linux下一切皆文件”。从上面我们也可看出,除了常规的ASCALL码和二进制文件,其他的如字符设备,套接字等在Linux下都被看作文件。因此学习Linux文件操作不可为不重要。
Linux的文件I/O编程可以分为两类:标准I/O和文件I/O。
-
标准I/O
标准I/O指的是ANSI C中定义的用于I/O操作的一系列函数
。只要操作系统中安装了C库就可以使用标准I/O(如:fopen(),fclose(),fwirte()等)。所以使用标准I/O的程序具有更好的可移植性。除此之外,使用标准I/O可以减少系统调用的次数,提高系统运行的效率
。标准I/O函数在执行的时候也会用到系统调用。标准I/O在使用的时候在用户空间创建缓冲区,读写时先操作缓冲区,在合适的时机再通过系统调用访问实际的文件,从而减少使用系统调用的次数。 -
文件I/O
基于系统调用的文件操作I/O接口(open(), read(), write(), lseek(), close()等)。是操作系统直接提供的编程接口API。Linux操作系统是基于文件概念的。文件是以字符序列构成的信息载体。由此,我们可以将I/O设备当作文件来处理。
下面会先介绍标准I/O编程,学习标准I/O前我们先一起来看几个概念。
1. 系统调用
用户程序通过系统调用访问内核的的方式如下:
Linux将用户空间和内核空间进行了隔离,用户空间的程序不能直接访问内核空间,只能通过系统调用或者C库函数来访问内核空间。
系统调用:用户空间进程访问内核的接口把用户空间从底层的硬件编程中解放出来,极大的提高了系统的安全性是用户程序具有可移植性,是操作系统的一部分。在执行系统调用的时候Linux必须从用户态切换到内核态,处理相应的请求,然后再返回用户态
。
库函数:库函数是为了实现某个功能而封装起来的API接口集合提供统一的编程接口,更加便于应用程序的移植
,是语言或者应用程序的一部分。
2. 用户程序编程接口
利用系统调用接口可以访问各种资源,但是在实际开发过程中并不能直接用系统调用接口,而是直接使用用不程序编程接口(API)。主要有以下两个原因:
1. 系统调用接口功能非常简单,无法满足程序的需求。
2. 不同操作系统的系统调用解耦不兼容,程序移植时工作量大。
用户程序编程接口通俗的解释就是各种库中的函数。用户编程接口(API)在实现时,通常都要依赖系统调用接口。例如,创建进程的API函数fork()对应于内核空间的sys_fork()系统调用。很多API函数需要多个系统调用来完成其功能。还有一些不需要调用任何系统调用。
3. 流
标准I/O的核心对象就是流。
当标准I/O打开一个文件的时候,就会创建一个FILE结构体描述该文件(可以理解为创建一个FILE结构体和实际打开的文件关联起来)。我们把这个FILE结构体形象的称为流。标准I/O都是基于流进行各种操作的。
通常,一个进程启动时,都会打开三个流:标准输入、标准输出和标准错误。这三个流分别对应文件描述符0,1,2。分别对应着宏:STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO。
标准I/O中的流的缓冲根据缓冲类型的不同,又可以分为以下三种。
-
全缓冲
在这种情况下,当填满标准I/O缓冲区后才进行实际的I/O操作。对于存放在磁盘上的普通文件,用标准I/O打开时默认是全缓冲的。当缓冲区已满或执行flush操作是才会进行磁盘操作。 -
行缓冲
在这种情况下,当在输入和输出中遇到换行符时执行I/O操作。标准输入流(stdin)和标准输出流(stdout)就是使用行缓冲的典型例子。 -
无缓冲
不对I/O操作进行缓冲,即在对流的读写时会立即操作实际文件。标准出错流是不带缓冲的,这就使得出错信息可以立即显示在终端上,而不管输出的内容是否包含换行符。
通过上面的描述我们会发现,有时候我们不想输出或输出的数据被缓冲,想立即刷新缓存。那该怎么办呢?主要由一下三种办法:
- 使用fflush()函数立即刷新缓存(主意这个仅针对stdin有效,函数原型为
int fflush(FILE *stream)
) - setbuf()函数关闭缓存区(关闭缓冲机制,函数原型
void setbuf(FILE *stream,char *buf);
😉 - 一个小技巧,
while((ch = getchar()) != ‘\n’ && ch != EOF);
,遇到’\n’或达到文件结尾,就会刷新缓存。这个是从编程角度来预防的。
二、标准I/O编程
了解了标准I/O编程中的的一些特点和基本概念后,这里我们来继续了解标准I/O编程。
1. 流的打开
使用标准I/O打开文件的函数有fopen()、fdopen()、freopen()。他们可以以不同的模式打开文件,都返回一个执行FILE的指针来进行。
其中fopen()除可以指定打开的文件和模式外,还可指定特定的I/O流。
所需头文件
#include <stdio.h>
函数原型
FILE *fopen(const hcar *path,const char *mode);
函数参数
path:包含要打开的文件路径及文件名
mode:文件打开方式,下面会详细说明
函数返回值
成功:指向FILE的指针
失败:NULL
其中,mode用于指定打开文件的方式。下面详细看下mode取不同值时的用法:
r或rb
打开只读文件,该文件必须存在
r+或r+b
打开可读写的文件,该文件必须存在
w或wb
打开只写文件,若该文件存在则文件长度为0,即会擦写以前的内容;
若文件不存在则建立该文件
w+或w+b
打开可读写文件,若该文件存在存在则文件长度为0,机会擦写文件以前的内容;
若文件不存在则会建立该文件
a或ab
以附加的方式打开只写文件。若该文件不存在,则会建立该文件;
如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留
a+或a+b
以附加的方式打开可读写的文件。若文件不存在,则会建立该文件;
如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留
在每个文件的选项中加入b字符从来告诉函数库打开的文件尾二进制文件,而非纯文本文件。不过在Linux系统中会忽略该符号。
另外,前面提到的当操作系统起来的时候回默认打开三个流:标准输入(0)、标准输出(1)、标准出错(2)。所以我们可以直接用这三个标准流。
2. 流的关闭
关闭流的函数为fclose(),该函数将流的缓冲区内的数据全部写入文件中,并释放相关资源。
所需头文件
#include <stdio.h>
函数原型
int fclose(FILE *stream);
函数参数
stream:已打开的流指针
函数返回值
成功:0
失败:EOF
程序结束时会自动关闭所有打开的流。
3. 出错处理
标准I/O函数执行时如果出现错误,会把错误码保存在全局变量errno中。程序员可以通过相应的函数打印错误信息。全局错误码定义在<errno.h>中,全局可见,错误值定义为"EXXX"形式,如EACCESS。
处理规则:如果没有出错,则errno值不会被一个例程清除,即只有出错时才需要检查errno值。任何函数都不会讲errno的值设置为0,errno.h中定义了所有常数都不为0。
错误处理函数主要为,perror()和strerror()。下面分别来进行介绍。
- perror():输出用户信息及errno对应的错误信息
- strerror:映射errno对应的错误信息
下面先来看,perror()函数:
所需头文件
#include <stdio.h>
函数原型
void perror(const char* s);
函数参数
s:在标准错误流上输出的信息
函数返回值
无
下面来看一个小例子:
#include <stdio.h>
int main()
{
FILE *fp; //定义流指针
if( (fp = fopen("a.c","r") = NULL )
{
perror("fail to fopen");
return -1;
}
fclose(fp);
return 0;
}
strerror()函数:
所需头文件
#include <string.h>
#include <errno.h>
函数原型
char *strerror(int errnum);
函数参数
errnum:错误码
函数返回值
错误码对应的错误信息
同样的,我们来看一个小例子。
#include <stdio.h>
int main()
{
FILE *fp;
if ((fp = fopen("a.c","r")) == NULL )
{
printf("fail to fopen: %s\n",strerro(errno));
return -1;
}
fclose(fp);
return 0;
}
4. 流的读写
1. 字符输入/输出
字符输入/输出函数一次仅读写一个字符。字符输入/输出函数语法要点。
字符输入函数
所需头文件
#include <stdio.h>
函数原型
int getc(FILE *stream);
int fgetc(FILE *stream);
int getchar(void);
函数参数
stream:要输入的文件流
函数返回值
成功:读取的字符
失败:EOF
getc()
和fgetc()
从指定的流中读取一个字符(字节),getchar()
从stdin
(标准输入流)中读取一个字符(字节)。
字符输出函数
所需头文件
#include <stdio.h>
函数原型
int putc(int c, FILE *stream);
int fputc(int c, FILE *stream);
int putchar(void);
函数返回值
成功:输出字符c
失败:EOF
putc()
和fputc()
向指定的流输出一个字符(节),putchar()
函数向stdout
(标准输出流)输出一个字符(节)。
在来看一个例子,从标准输入中读入数字并将其输出到标准输出:
#include <stdio.h>
int main()
{
int c;
while(1)
{
c = fgetc(stdin); //从键盘中读取一个字符
if( (c >= '0' ) && (c <= '9') )
fputc(); //若输入的位数字则输出
if(c == '\n')
break;
}
return 0;
}
2. 行输入/输出
行输入/输出函数一次操作一行。
行输入函数
所需头文件
#include <stdio.h>
函数原型
char *gets(char *s);
char *fgets(char *s,int size,FILE *stream);
函数参数
s:存放输入字符串的缓冲区首地址
size:输入的字符串长度
stream:对应的流
函数返回值
成功:返回字符串s
失败或达到文件末尾:NULL
gets函数容易造成缓冲区溢出。原因是该函数不会进行缓冲区长度的检测。
fgets()从指定的流中读取一个字符串,当遇到\n时,会读取\n或读取size-1个字符后返回。
注意:fgets()不能保证每次都能读取一行。比如,当size小于当前行的长度时就会提前返回。
行输出函数
所需头文件
#include <stdio.h>
函数原型
int puts(const char *s);
int fputs(const char *s, FILE *stream);
函数参数
s:存放输出字符串的缓冲区首地址
stream:对应的流
函数返回值
成功:输出字符串s
失败:NULL
下面还是来看一个例子:
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int line = 0;
char buf[128];
FILE *fp;
if (argc < 2)
{
printf("Usage : %s <file>\n",argv[0]);
return -1;
}
if ((fp = fopen(argv[1] ,"r"))== NULL)
{
perror("fail to fopen");
return -1;
}
while(fgets(buf,128,fp) != NULL)
{
if (buf[strlen(buf) - 1] == '\n')
line++;
}
printf("The line of %s is %d\n",argv[1],line);
return 0;
}
3. 指定大小读取文件
在文件流被打开之后,可对文件流按照指定大小为单位进行读写操作。fread对文件流进行读,fwrite()对文件流进行写。
**fread()函数:**
所需头文件
#include <stdio.h>
函数原型
size_t fread(void *ptr,size_t size,size_t nmemb,FILE *stream);
函数参数
ptr:存放读入记录的缓冲区
size:读取的每个记录的大小
nmemb:读取的记录数
stream:要读取的文件数
函数返回值
成功:返回实际读取到的nmemb数目
失败:EOF
fwrite()函数
所需头文件
#include <stdio.h>
函数原型
size_t fwrite(const void *ptr,size_t size,size_t nmemb,FILE *stream);
函数参数
ptr:存放写入记录的缓冲区
size:写入每个记录的大小
nmemb:读取的记录数
stream:要读取的文件流
函数返回值
成功:返回实际写入到的nmemb数目
失败:EOF
4. 流的定位
每个打开的流内部都有一个当前读写位置。流打开时,当前读写位置为0,表示文件的开始位置。每读写一次后,当前读写位置自动增加实际读写的大小。在读写流之间可先对比流进行定位,即移动到指定的位置在操作。
常用的API有两个:ftell()、fseek()。
ftell()用于得到当前的文件位置,调用成功则为当前文件位置指示,若出错则返回-1L。
ftell()
所需头文件
#include <stdio.h>
函数原型
int ftell(FILE *stream);
函数参数
stream:要定位的文件流
函数返回值
成功:返回当前读写位置
失败:EOF
fseek()
所需头文件
#include <stdio.h>
函数原型
int fseek(FILE *stream,long offset,int whence);
函数参数
stream:要定位的文件流
offset:相对基准值的偏移量
whence:基准值
SEEK_SET 代表文件的起始位置
SEEK_END 代表文件的结束位置
SEEK_CUR 代表文件当前读写位置
函数返回值
成功:0
失败:EOF
5. 格式化输入输出
格式化输入/输出函数可以指定输入/输出的具体形式。
格式化输入函数语法要点:
所需头文件
#include <stdio.h>
函数原型
int scanf(const char *format,...);
int fscanf(FILE *fp, const char *format,...);
int sscanf(char *buf, cont char *format,...);
函数参数
format():输入的格式
fp:作为输入的流
buf:作为输入的缓冲区
函数返回值
成功:输出字符输数
失败:EOF
格式化输出函数语法要点
所需头文件
#include <stdio.h>
函数原型
int printf(const char *format, ...);
int fprintf(FILE *fp, const char *format, ...);
int sprintf(char *buf, cont char *format, ...);
函数参数
format():输出的格式
fp:接收输出的流
buf:接收输出的缓冲区
函数返回值
成功:输出字符输数(sprintf返回存入的数组中的字符数)
失败:EOF
到这里我们已经把Linux下标准I/O接口梳理了一遍,这里只梳理了常用的接口,介绍了其函数定义和用法。
下一篇,我们将介绍一下Linux文件I/O的用法。