文件IO 第一天(标准IO)

一、文件与文件类型

1、文件定义

    定义:文件(File)是一个具有符号名字的一组相关联元素的有序序列。文件可以包含的内容十分广泛,操作系统和用户都可以将具有一定独立功能的一个程序模块、一组数据或一组文字命名为一个文件。

    文件名:这个数据有序序列集合(文件)的名称。

2、文件的分类

    文件由许多种,运行的方式也各有不同。在Windows中,我们是通过文件的后缀名来对文件分类的。例如.txt、.doc、.exe等。而在Linux系统中则不是,它不以文件后缀来区分文件的类型。

在Linux中,我们可以使用ls -l指令来查看文件的类型。在Linux系统中,文件主要有7种类型

    -    普通文件    指ASCII文本文件、二进制文件以及硬链接文件

    d    目录文件    包含若干文件或子目录

    l    符号链接    只保留所指向文件的地址而非文件本身

    p    管道文件    用于进程间通信

    c    字符设备    原始的I/O设备文件,每次操作仅操作1个字符(例如键盘)

    b    块设备        按块I/O设备文件(例如硬盘)

    s    套接字        套接字是方便进程间通信的特殊文件,与管道不同的是套接字能通过网络连接使不同的计算机的进程进行通信

扫描二维码关注公众号,回复: 2684756 查看本文章

3、Linux的文件目录结构

    Linux系统中文件采取树形结构,即一个根目录(/),包含下级目录或文件的信息;子目录又包含更下级的目录或文件的信息。依次类推层层延伸,最终构成一棵树。

Linux系统的每个目录都有其特定的功能,这里只简单介绍一些主要目录及其功能

目录            功能说明

/etc            存放系统配置文件

/bin            存放常用指令

/sbin        (root用户的)存放指令目录

/home        用户主目录,所有用户p的文件默认建立在此目录下(用户工作目录)

/boot          包含内核启动文件

/dev            存放设备文件(与底层驱动交互)

/usr             存放应用程序

/mnt            挂载目录

/root            root用户主目录

/proc            process的所写,存放描述系统进程的详细信息

/lib               存放常见库文件

/lost+found    可以找到一些误删除或丢失的文件并恢复它们

二、系统调用与用户编程接口(API)

    系统调用(System Call)是由操作系统实现提供的所有系统调用所构成的程序接口的集合。是应用程序与操作系统间的接口与纽带

操作系统的主要功能是为管理硬件资源和为应用程序开发人员提供良好的环境来使应用程序具有良好的兼容性。为了达到这个目的,内核提供一系列具备预定功能的函数,通过系统调用的接口呈现给用户。当用户访问系统调用,系统调用把应用程序的请求传递给内核,调用相应的内核函数完成所需处理,将处理结果返回给应用程序。

    应用程序编程接口(API,Application Programming Interface)是一些预定义的函数,目的是提供应用程序与开发人员基于软件/硬件得以访问一组例程的能力,而又无需访问源码或理解内部工作原理机制。

在实际开发应用程序的过程中,我们并不直接使用系统调用接口,而是使用用户编程接口(API)。为什么呢?

        1.系统调用功能非常简单,有时无法满足程序的需求。

        2.不同操作系统的系统调用接口不兼容,若使用系统调用接口则程序移植工作量非常大

    用户编程接口使用各种库(在C语言中最主要的是C库)中的函数。为了提高编程效率,C库中实现了很多函数。这些函数实现了许多常用功能供程序开发者调用。这样一来,程序开发者无需自己编写这些代码,直接可以调用函数就能实现功能,提高了编码效率和代码复用率。

    使用用户编程接口还有一个好处:一定程度上解决了程序的可移植性(虽然C语言的可移植性仍没有Java好)。几乎所有的操作系统上都实现了C库,因此使用C语言编写的程序只需在不同的系统下重新编译即可运行。

    通常情况下,用户编程接口API在实现时需要依赖系统调用接口。例如创建进程API函数fork()需要调用内核空间的sys_fork()系统调用。但是还有一些API无需调用任何系统调用。

在Linux中用户编程接口遵循在Unix系统中最流行的应用编程编程标准POSIX标准。

/***************POSIX简介*************************/

    POSIX表示可移植操作系统接口(Portable Operating System Interface ,缩写为 POSIX ),POSIX标准定义了操作系统应该为应用程序提供的接口标准,是IEEE为要在各种UNIX操作系统上运行的软件而定义的一系列API标准的总称,其正式称呼为IEEE 1003,而国际标准名称为ISO/IEC 9945。

    POSIX标准意在期望获得源代码级别的软件可移植性。换句话说,为一个POSIX兼容的操作系统编写的程序,应该可以在任何其它的POSIX操作系统(即使是来自另一个厂商)上编译执行。

/***************POSIX简介end**********************/

标准I/O与文件I/O的区别:

    1.文件I/O又称为低级磁盘I/O,遵循POSIX标准。任何兼容POSIX标准的操作系统都支持文件I/O。标准I/O又称为高级磁盘I/O,遵循ANSI C相关标准。只要开发环境有标准C库,标准I/O就可以使用。

在Linux系统中使用GLIBC标准,它是标准C库的超集,既支持ANSI C中定义的函数又支持POSIX中定义的函数。因此Linux下既可以使用标准I/O,也可以使用文件I/O。

    2.通过文件I/O读写文件时,每次操作都会执行相关系统调用。这样的好处是直接读写实际文件,坏处是频繁的系统调用会增加系统开销。标准I/O在文件I/O的基础上封装了缓冲机制,每次先操作缓冲区,必要时再访问文件,从而减少了系统调用的次数

    3.文件I/O使用文件描述符打开操作一个文件,可以访问不同类型的文件(例如普通文件、设备文件和管道文件等)。而标准I/O使用FILE指针来表示一个打开的文件,通常只能访问普通文件

三、Linux标准I/O

1、标准I/O定义

    标准I/O指的是ANSI C中定义的用于I/O操作的一系列函数。只要操作系统安装了C库,就可以调用标准I/O。换句话说,若程序使用标准I/O函数,那么源代码无需进行任何修改就可以在其他操作系统上编译,具有更好的可移植性。

    除此之外,由于标准I/O封装了缓冲区,使得在读写文件的时候减少了系统调用的次数,提高了效率。在执行系统调用的时候,Linux必须从用户态切换到内核态,在内核中处理相应的请求,然后再返回用户态。如果频繁地执行系统调用则会增加这种开销。标准I/O为了减少这种开销,采取缓冲机制,为用户空间创建缓冲区,读写时优先操作缓冲区,在必须访问文件时(例如缓冲区满、强制刷新、文件读写结束等情况)再通过系统调用将缓冲区的数据读写实际文件中,从而避免了系统调用的次数。

2、流的定义

    标准I/O的核心对象是流。当用标准I/O打开一个文件时,就会创建一个FILE结构体描述该文件。我们把这个FILE结构体称为“流”。标准I/O函数都是基于流进行各种操作的。

/**********************流的分类***********************/

流的分类分为文本流和二进制流两种

    文本流:文本流是由字符文件组成的序列,每一行包含0个或多个字符并以'\n'结尾。在流处理过程中所有数据以字符形式出现,'\n'被当做回车符CR和换行符LF两个字符处理,即'\n'ASCII码存储形式是0x0D和0x0A。当输出时,0x0D和0x0A转换成'\n'

    二进制流:二进制流是未经处理的字节组成的序列,在流处理过程中把数据当做二进制序列,若流中有字符则把字符当做ASCII码的二进制数表示。'\n'不进行变换。

例如:2016在文本流中和二进制流中的数据类型不同:

文本流:2016---->'2''0''1''6'---->50 48 49 54

二进制流:2016-->数字2016--->0000011111010001

在Linux/Unix系统中,文本流与二进制流没有差异,但是在Windows中稍有差异,所以标准C库定义了两种流。

/**********************流的分类end********************/

在使用标准I/O操作文件的时候,每个被程序使用的文件都会在内存中开辟一块区域,用来存放与文件相关的属性信息,这些信息存放在一个FILE类型的结构体中,FILE类型的结构体是由系统定义的一个结构体

typedef struct

{

    short level;                //缓冲区满/空的状态

    unsigned flags;             //文件状态标志

    char fd;                    //文件描述符

    unsigned char hold;         //如缓冲区无内容则不读取字符

    short bsize;                //缓冲区的大小

    unsigned char *buffer;      //数据缓冲区的位置

    unsigned char *curp;        //指针当前的指向

    unsigned istemp;            //临时文件指示器

    short token;                //用于有效性检查

}FILE;

在标准I/O中,常用FILE类型的结构体指针FILE*来操作文件。

/***************对“流”与“文件”的关系的讨论************/

    在初学C语言文件I/O相关知识点时,经常会陷入“什么是流?”“什么是文件?”“流和文件有什么关系(区别)?”等问题。在这里对“流”与“文件”进行简单的讨论,基础好的同学可跳过该部分。

《C Primer Plus》上说,C程序处理一个流而不是直接处理文件。但是后面的解释十分抽象:『流(stream)是一个理想化的数据流,实际输入或输出映射到这个数据流』。

    本质上来说,文件本身就是数据的有序序列,因此我们操作文件时是按顺序依次操作该文件的数据。我们可以想象一个传送带,传送带上的产品就是待操作数据。当我们对文件内的数据进行操作时,已操作的数据从当前位置离开,待操作的数据不断流向当前位置,这样文件内的数据就产生了流动的感觉,这个“传送带”就是C语言内“流”的原型。

    我们打开一个流,就意味着将该流与该文件进行了连接(即让文件内的“产品”放上“传送带”),关闭流将断开流与该文件的连接。此时我们对流进行操作就是对文件进行操作。通常在不产生歧义的情况下,“文件”与“流”可以不予区分。

    在程序开始执行的时候,操作系统会默认开启stdin、stdout、stderr三个文件,这三个文件作为输入、输出、输出错误的流,这样我们使用诸如scanf()、printf()等就无需手动加载流,方便使用。

当我们使用fopen()打开一个文件的时候,该文件会返回一个FILE*类型的指针,例如:

FILE *fp = fopen("hello.txt","w")

此时文件hello.txt与流指针fp就关联了起来,对fp的操作就相当于对文件hello.txt进行操作。

/***************对“流”与“文件”的关系的讨论end*********/

在标准I/O中预定义了三块缓冲区:stdin、stdout、stderr,分别代表标准输入流、标准输出流、标准输出错误流。见下:

流的名称        程序中使用

标准输入           stdin    

标准输出           stdout    

标准错误输出    stderr    

标准I/O中的流的缓冲类型有三种

1.全缓冲:这种情况下,当缓冲区被填满后才进行实际的I/O操作。对于存放在磁盘上的普通文件用标准I/O打开时默认是全缓冲的。当缓冲区满或者执行刷新缓冲区(fflush)操作才会进行磁盘操作。

2.行缓冲:这种情况下,当在输入/输出中遇到换行符时执行I/O操。-->标准输入/输出流(stdin/stdout)就是使用行缓冲。

3.无缓冲:不使用缓冲区进行I/O操作,即对流的读写操作会立即操作实际文件。标准出错流(stderr)是不带缓冲的,这就使得当发生错误时,错误信息能第一时间显示在终端上,而不管错误信息是否包含换行符。

示例1.1:stdout使用行缓冲形式

效果:不会立即打印内容,而是等待'\n'或者缓冲区满才输出。

#include<stdio.h>

int main()

{

    while(1)

    {

        printf("Hello World");

        sleep(1);//延时1秒

    }

    return 0;

}

当缓冲区满的时候才输出到屏幕

示例1.2:stdout使用行缓冲形式

效果:当添加了'\n'之后,会正确地输出

#include<stdio.h>

int main()

{

    while(1)

    {

        printf("Hello World\n");

        sleep(1);//延时1秒

    }

    return 0;

}

每1S输出一次

示例1.3:stderr使用无缓冲形式

效果:stderr使用无缓冲,即使不使用'\n'仍能立即输出

#include<stdio.h>

int main()

{

    while(1)

    {

        perror("Hello World");   //无缓冲

        sleep(1);//延时1秒

    }

    return 0;

}

perror会自动打印系统的提示信息 (:Success)

示例2:编写程序实现以下功能:

    ⒈向标准输出流输出HelloWorld

    ⒉向标准错误流输出HelloWorld

    ⒊控制输出重定向,使程序仅能输出标准输出流的字符

    ⒋控制输出重定向,使程序仅能输出标准错误流的字符

#include<stdio.h>

int main()

{

    fprintf(stdout,"%s","This is stdout:HelloWorld!\n");

    fprintf(stderr,"%s","This is stderr:HelloWorld!\n");

    //fprintf的作用是向某个流(文件)中按格式输出指定内容

    return 0;

}

实现第三步的功能:在执行时:./a.out 2> /dev/null

仅能显示标准输出的内容

实现第四步的功能:在执行时:./a.out 1> /dev/null

仅能显示标准出错的内容

四、标准I/O编程

1、打开文件(流)

使用fopen()/fdopen()/freopen()函数可以打开一个文件。其中fopen()是最常用的函数,fdopen()可以指定打开文件的文件描述符和模式,freopen()除可以指定打开的文件与模式外,还可以指定特定的I/O流

函数fopen()

             fopen做的事:找到文件,打开文件,并将文件内的数据加载到流内

    需要头文件:#include<stdio.h>

    函数原型:FILE *fopen(const char *path,const char *mode)

    函数参数:path:要打开的文件的路径及文件名

                   mode:文件打开方式,见下

    函数返回值:成功:指向文件的FILE类型指针

                        失败:NULL

以下是mode参数允许使用的取值及说明:

    r或rb               以只读的方式打开文件,该文件必须存在

    r+或r+b          以可读可写的方式打开文件,该文件必须存在

    w或wb            以只写的方式打开文件,若文件不存在则创建该文件;若文件存在则擦除文件原始内容,从文件开头开始操作文件

    w+或w+b       以可读可写的方式打开文件,若文件不存在则创建该文件若文件存在则擦除文件原始内容,从文件开头开始操作文件

    a或ab              以附加的方式打开只写文件,若文件不存在则创建该文件;若文件存在,写入的数据追加在文件尾,即文件的原始内容会被保留

    a+或a+b         以附加的方式打开可读可写文件,若文件不存在则创建该文件;若文件存在,写入的数据追加在文件尾,即文件的原始内容会被保留

注意:

    ⒈+的作用代表操作文件的方式是只读/写/附加(无+)还是同时读写(有+)

    ⒉b的作用代表操作的文件是ASCII文本文件(无b)还是二进制文件(有b)

2、关闭文件(流)

使用fclose()函数可以关闭一个文件,该函数将缓冲区内的所有内容写入相关文件中并回收相应的系统资源

函数fclose()

    需要头文件:#include<stdio.h>

    函数原型:int fclose(FILE *stream)

    函数参数:stream:已打开的流指针

    函数返回值:成功:0

                        失败:EOF

示例:打开一个文件然后关闭

写一个程序,打开一个文件,然后关闭该文件。

#include<stdio.h>

#include<stdlib.h>

int main()

{

    FILE *fp;

    if((fp = fopen("hello.txt","w"))==NULL)//打开文件,之后判断是否打开成功

    {

        perror("cannot open file");

        exit(0);

    }

    //对文件的操作

    fclose(fp);//关闭文件

    return 0;

}

注意:由于打开文件fopen()函数可能会失败,所以我们打开文件后通常需要判断fopen()函数是否打开文件成功。判断的方法是将fopen的结果放入if()表达式中并判断该表达式得到的结果是否为NULL(空指针)。

练习:若示例程序的hello.txt文件不存在,以"r"的模式打开该文件会出现什么效果?

答案:

#include<stdio.h>

#include<stdlib.h>

int main()

{

    FILE *fp;

    if((fp = fopen("hello.txt","r"))==NULL)//打开文件,之后判断是否打开成功

    {

        perror("cannot open file");

        exit(0);

    }

    //对文件的操作

    fclose(fp);//关闭文件

}

若hello.txt文件不存在,则在执行程序时会报错:

示例:打开文件hello.txt,使用fprintf()向hello.txt中写入"HelloWorld"。其中打开文件部分使用命令行传参。

#include<stdio.h>

#include<stdlib.h>

int main(int argc,char *argv[])

{

    FILE *fp;

    if((fp = fopen(argv[1],"w"))==NULL)//打开文件,之后判断是否打开成功

    {

        perror("cannot open file");

        exit(0);

    }

    fprintf(fp,"%s","HelloWorld\n");

    fclose(fp);//关闭文件

}

练习1:打开文件hello.txt,使用fprintf()在刚才的"HelloWorld"之后添加一行"NanJing"

答案:

#include<stdio.h>

#include<stdlib.h>

int main(int argc, char *argv[])

{

    FILE *fp;

    if((fp = fopen(argv[1],"a"))==NULL)//打开文件,之后判断是否打开成功

    {

        perror("cannot open file");

        exit(0);

    }

    fprintf(fp,"%s","NanJing\n");

    fclose(fp);//关闭文件

}

练习2:打开文件hello.txt,将该文件原始内容清空并添加"Zhen Mei!"

3、错误输出

在刚才的示例程序与练习程序中使用了perror这个函数。perror函数可以在程序出错的时候将错误信息输出到标准错误流stderr中。由于标准错误流不使用缓冲所以可以及时显示错误信息。

函数perror()

    需要头文件:#include<stdio.h>

    函数原型:void perror(const char *s)

    函数参数:s:在标准错误流上输出的错误信息

    函数返回值:无

在标准I/O函数执行的时候,若发生了错误(例如以r或r+打开一个不存在的文件),会将错误码保存在系统变量errno中。使用perror函数可以检测errno的错误码信息并输出对应的错误信息。

注意:errno的变量声明不在stdio.h中,而是在头文件errno.h中

除了使用perror来输出错误信息,也可以使用strerror函数手动获得错误码对应的错误信息

函数strerror()

    需要头文件:#include<string.h>、#include<errno.h>

    函数原型:char *strerror(int errno)

    函数参数:errno:返回的错误码

    函数返回值:错误码对应的信息

示例:不使用perror,使用strerror输出错误信息

#include<stdio.h>

#include<stdlib.h>

#include<string.h>

#include<errno.h>

int main(int argc, char *argv[])

{

    FILE *fp;

    fp = fopen(argv[1],"r");

    if(NULL == fp)

    {

        printf("Open %s was error:%s\n",argv[1],strerror(errno));

        return 1;

    }

    printf("Open %s wasn't error:%s\n",argv[1],strerror(errno));

    fclose(fp);

    return 0;

}

4、按字符输入/输出

函数getc()、fgetc()、getchar()

    需要头文件:#include<stdio.h>

    函数原型:int getc(FILE *stream)

                     int fgetc(FILE *stream)

                     int getchar(void)

    函数参数:stream:输入文件流

    函数返回值:成功:读到的字符

                        失败:EOF

/*********************有关EOF************************/

在C语言(或者更精确的说是在C标准库中)EOF表示一个文本文件的结束符(end of file),这个宏定义在头文件stdio.h中,其值为-1,因为在ASCII表的编码(0~255)中没有-1编码。EOF通常被当做文件结束的标志,还有很多的文件处理相关的函数使用EOF作为函数出错的返回值。

但是要注意的是,EOF只能作为文本文件(流)的结束符,因为若该文件(流)是二进制形式文件则会有-1的出现,因此无法使用EOF来表征文件结束。为解决这个问题,在C语言中提供了一个feof()函数,若遇到文件结尾,函数feof()返回1,否则返回0。这个函数既可以判断二进制文件也可以判断文本文件。

/*********************有关EOFend*********************/

getc()函数和fgetc()函数是从一个指定的流中读取一个字符,getchar()函数是从stdin中读取一个字符。

   

 函数putc()、fputc()、putchar()

    需要头文件:#include<stdio.h>

    函数原型:int putc(int c,FILE *stream)

                     int fputc(int c,FILE *stream)

                     int putchar(int c)

    函数参数:c:待输出的字符(的ASCII码)

            stream:输入文件流

    函数返回值:成功:输出字符c

                        失败:EOF

putc()函数和fputc()函数是从一个指定的流中输出一个字符,putchar()函数是从stdout中输出一个字符。

示例:从文件hello.txt中读取字符然后输出到显示器上

#include<stdio.h>

#include<stdlib.h>

int main(int argc, char *argv[])

{

    int c;

    FILE *fp;

    fp = fopen(argv[1],"r+");

    if(NULL == fp)     //判断文件是否打开成功

    {

        perror("Can't open this file");

        return 1;

    }

    c = fgetc(fp);

    while(c != EOF)

    {

        putchar(c);

        c = fgetc(fp);

    }

    fclose(fp);

    return 0;

}

练习:文件hello.txt中存放了各种字符(大写字母、小写字母、数字、特殊符号等),将该文件中的字母输出,非字母不输出

答案:

#include<stdio.h>

#include<stdlib.h>

int main(int argc, char *argv[])

{

    int c;

    FILE *fp;

    fp = fopen(argv[1],"r+");

    if(NULL == fp)

    {

        perror("Can't open this file");

        return 1;

    }

    c = fgetc(fp);

    while(EOF != c)

    {

        if((c >= 'A'&&c <= 'Z')||(c >= 'a'&&c <= 'z'))

        putchar(c);

        c = fgetc(fp);

    }

    printf("\n");

    fclose(fp);

    return 0;

}

5、按行输入/输出

当然,如果我们每次都按字符一个一个字符操作的话,程序执行效率会大大降低。因此标准I/O中提供了按行输入/输出的操作函数。

    函数gets()、fgets()

    需要头文件:#include<stdio.h>

    函数原型:char *gets(char *s)

                     char *fgets(char *s,int size,FILE *stream)

    函数参数:s:存放输入字符的缓冲区地址

                 size:输入的字符串长度

            stream:输入文件流

    函数返回值:成功:s

                        失败或读到文件尾:NULL

在Linux的内核man手册中,对gets()函数的评价是:"Never use gets().  Because it is impossible to tell without knowing the data in advance how many  characters  gets()  will  read,  and  because gets() will continue to store characters past the end of the buffer, it is extremely dangerous to use.  It has  been  used  to  break  computer security.  Use fgets() instead."简单来说gets()的执行逻辑是寻找该输入流的'\n'并将'\n'作为输入结束符,但是若输入流数据超过存储空间大小的话会覆盖掉超出部分的内存数据,因此gets()函数十分容易造成缓冲区的溢出,不推荐使用。而fgets()函数的第二个参数指定了一次读取的最大字符数量。当fgets()读取到'\n'或已经读取了size-1个字符后就会返回,并在整个读到的数据后面添加'\0'作为字符串结束符。因此fgets()的读取大小保证了不会造成缓冲区溢出,但是也意味着fgets()函数可能不会读取到完整的一行(即可能无法读取该行的结束符'\n')。

//学习过fgets()之后,输入字符串尽可能多使用fgets(),而尽量避免使用gets()和scanf("%[^\n]")

示例:使用fgets()函数,依次读取文件内的内容并输出。

#include<stdio.h>

#include<stdlib.h>



#define MAX 128



int main(int argc, char *argv[])

{

    int c;

    char buf[MAX] = {0};  //初始化数组:清零

    FILE *fp;

    if(argc < 2)

    {

        printf("Arguments are too few\n");

        return 1;

    }

    fp = fopen(argv[1],"r+");

    if(NULL == fp)

    {

        perror("Can't OPen file");

        return 1;

    }

    while(fgets(buf,MAX,fp) != NULL)

    {

        printf("%s",buf);

    }

    fclose(fp);

    return 0;

}

思考:由于fgets()函数不一定会读取到'\n',那么如何使用fgets()函数来统计一个文件有多少行呢?

练习:使用fgets()函数统计某个文本文件有多少行

答案:

#include<stdio.h>

#include<stdlib.h>

#include<string.h>

#define MAX 128

int main(int argc,char *argv[])

{

    int c;

    char buf[MAX]={0};

    FILE *fp;

    int line=0;

    if(argc<2)

    {

        perror("argument is too few");

        exit(0);

    }

    if((fp = fopen(argv[1],"r+"))==NULL)//打开文件,之后判断是否打开成功

    {

        perror("cannot open file");

        exit(0);

    }

    while(fgets(buf,MAX,fp)!=NULL)

    {

        if(buf[strlen(buf)-1]=='\n')//若这次读取读到了'\n',则证明该行结束

            line++;//行数+1

    }

    printf("This file %s has %d line(s)\n",argv[1],line);

    fclose(fp);

    return 0;

}

   函数puts()、fputs()

    需要头文件:#include<stdio.h>

    函数原型:int puts(const char *s)

              int fputs(conse char *s,FILE *stream)

    函数参数:s:存放输出字符的缓冲区地址

            stream:输出文件流

    函数返回值:成功:非负数

                        失败:EOF

6、使用格式化输入/输出操作文件

    刚才学习的fgetc()/fputc()/fgets()/fputs()以及相关的函数都是将数据作为字符类型进行操作,但是如果我们想将数字相关类型(int,float类型等)读/写文件显然是不可以的。例如我们想在某文件中写入float类型3.5,则不能使用fputc()/fputs()。这时我们可以使用我们熟悉的printf()/scanf()函数以及它们的同族函数fprintf()/fscanf()实现数据的格式化读/写。

函数scanf()、fscanf()、sscanf()

    需要头文件:#include<stdio.h>

    函数原型:int scanf(const char *format,...);

              int fscanf(FILE *fp,const char *format,...);

              int sscanf(char *buf,const char *format,...);

    函数参数:format:输入的格式

                             fp:待输入的流

                           buf:待输入的缓冲区

    函数返回值:成功:读到的数据个数

                        失败:EOF

函数printf()、fprintf()、sprintf()

    需要头文件:#include<stdio.h>

    函数原型:int printf(const char *format,...);

              int fprintf(FILE *fp,const char *format,...);

              int sprintf(char *buf,const char *format,...);

    函数参数:format:输出的格式

                            fp:待输出的流

                          buf:待输出的缓冲区

    函数返回值:成功:输出的字符数

                        失败:EOF

示例:使用fprintf()函数向文件中写入一些数据,然后使用fscanf()函数读出这些数据

#include<stdio.h>

#include<stdlib.h>

int main(int argc, const char *argv[])

{

    FILE *fp1,*fp2;

    if((fp1=fopen("scanftest.txt","w+"))==NULL)

    {

        perror("cannot open file");

        exit(0);

    }

    fprintf(fp1,"%d %d %d %d %s",1,2,3,4,"HelloWorld\n");

    fclose(fp1);

    /*文件写入数据完毕*/

    /*开始读取文件数据*/

    int a,b,c,d;

    char str[32];

    if((fp2=fopen("scanftest.txt","r+"))==NULL)

    {

        perror("cannot open file");

        exit(0);

    }

    fscanf(fp2,"%d%d%d%d%s",&a,&b,&c,&d,str);

    printf("a is %d\nb is %d\nc is %d\nd is %d\nstr:%s\n",a,b,c,d,str);

    fclose(fp2);

    return 0;

}

/****************关于sscanf()与sprintf()****************/

sscanf()与sprintf()函数的第一个参数都是字符型指针。sscanf()函数可以在字符串中读出指定格式的数据,sprintf()函数可以将数据按指定格式写入到某字符数组中。

示例:使用sscanf()函数在一个字符串中读出指定的数据

#include<stdio.h>

#include<stdlib.h>

int main(int argc, const char *argv[])

{

    FILE *fp;

    int a,b,c,d;

    if((fp=fopen("scanftest.txt","w+b"))==NULL)

    {

        perror("cannot open file");

        exit(0);

    }

    sscanf(argv[1],"%d.%d.%d.%d",&a,&b,&c,&d);

    printf("a is %d\nb is %d\nc is %d\nd is %d\n",a,b,c,d);

    fclose(fp);

    return 0;

}

编译后执行a.out 192.168.1.20,则可将ip地址(字符串)内的数据读出并写入变量a,b,c,d(int型)中。

/****************关于sscanf()与sprintf()end*************/

7、指定大小读/写文件

格式化输入/输出函数fscanf()/fprintf()使用比较方便,程序也简单易懂,但是fscanf()/fprintf()的读/写效率低下。一般在程序开发过程中,更多的使用fread()/fwrite()函数来一次读/写几个数据。

    函数fread()

    需要头文件:#include<stdio.h>

    函数原型:size_t fread(void *ptr,size_t size,size_t nmemb,FILE *stream);

    函数参数:ptr:存放读入数据的缓冲区

                   size:读取的每个数据项的大小(单位字节)

             nmemb:读取的数据个数

              stream:要读取的流

    函数返回值:成功:实际读到的nmemb数目

                        失败:0

函数fwrite()

    需要头文件:#include<stdio.h>

    函数原型:size_t fwrite(void *ptr,size_t size,size_t nmemb,FILE *stream);

    函数参数:ptr:存放写入数据的缓冲区

              size:写入的每个数据项的大小(单位字节)

              nmemb:写入的数据个数

              stream:要写入的流

    函数返回值:成功:实际写入的nmemb数目

                        失败:0

注意:

    ⒈fread()函数和fwrite()函数会将流当做二进制流的形式进行读/写,因此使用fread()/fwrite()操作的文件使用vim打开可能会出现乱码情况。

    ⒉fread()函数结束时,无法自动判断导致fread()函数结束的原因是读取到了文件末尾还是发生了读写错误。这时需要手动判断发生的情况。可以观察最后一次fread()的返回值,或使用feof()/ferror()函数判断。

示例1:使用fread()函数一次性读取1000个字节的数据

#include<stdio.h>

#include<stdlib.h>

int main(int argc, const char *argv[])

{

    FILE *fp;

    char buf[1000];

    int i;

    if((fp=fopen(argv[1],"r+"))==NULL)

    {

        perror("cannot open file");

        exit(0);

    }

    fread(buf,sizeof(char),1000,fp);//buf:读取后存放的位置;sizeof(char):每次读的大小;1000读的次数;fp:从什么地方读

    for(i=0;i<1000;i++)

    {

        putchar(buf[i]);

    }

    fclose(fp);

    return 0;

}

示例2:将一个int型数组{0,1,2,3,4,5,6,7,8,9}写入一个文件中

#include<stdio.h>

#include<stdlib.h>

int main(int argc, const char *argv[])

{

    FILE *fp;

    int a[10]={1,2,3,4,5,6,7,8,9,10};

    int i;

    if((fp=fopen(argv[1],"w+"))==NULL)

    {

        perror("cannot open file");

        exit(0);

    }
   fwrite(a,sizeof(a[0]),sizeof(a)/sizeof(a[0]),fp);
//a:要写入的数据存放的位置;sizeof(a[]):每次写的大小;sizeof(a)/sizeof(a[0])写的次数;fp:写到什么地方去

    fclose(fp);

    return 0;

}

 不过查看该文件会发现是乱码

练习1:读以下程序,猜想该程序会向文件中输入什么数据,运行程序证明猜想

#include<stdio.h>

#include<stdlib.h>

int main(int argc, const char *argv[])

{

    if(argc<2)

    {

        printf("too few arguments\n");

        exit(0);

    }

    FILE *fp;

    int a[]={1632397644,1768444783,1852139610,1635084371,663913};

    if((fp=fopen(argv[1],"w+"))==NULL)

    {

        perror("cannot open file");

        exit(0);

    }

    fwrite(a,sizeof(a[0]),sizeof(a)/sizeof(a[0]),fp);

    fclose(fp);

    return 0;

}

练习2:使用标准I/O的fread()/fwrite()函数实现文件的复制

提示:分别打开两个文件,一个源文件一个目标文件,循环从源文件中使用fread()取出数据,然后使用fwrite()函数写入目标文件中。注意fread()函数的循环结束条件。

#include<stdio.h>

#include<stdlib.h>

#define MAX 128

int main(int argc, const char *argv[])

{

    FILE *fp1,*fp2;

    char buf[MAX];

    int n;

    if(argc<3)

    {

        printf("arguments are too few, Usage:%s <src_file> <dst_file>\n",argv[0]);

        exit(0);

    }

    if((fp1=fopen(argv[1],"r"))==NULL)

    {

        perror("cannot open file1");

        fclose(fp1);

        exit(0);

    }

    if((fp2=fopen(argv[2],"w"))==NULL)

    {

        perror("cannot open file2");

        fclose(fp2);

        exit(0);

    }

    while((n=fread(buf,sizeof(buf[0]),sizeof(buf),fp1))>0)

    {

        fwrite(buf,sizeof(buf[0]),n,fp2);

    }

    fclose(fp1);

    fclose(fp2);

    return 0;

}

8、流的定位

    每次使用流打开文件并对文件进行操作后,都会让操作文件数据的位置发生偏移。在打开流的时候,偏移位置为0(即文件开头),随着读写的进行,偏移位置会不断向后,每次偏移量自动增加实际读写的大小。可以使用fseek()函数和ftell()函数对当前流的偏移量进行定位操作

函数fseek()

    需要头文件:#include<stdio.h>

    函数原型:int fseek(FILE *stream,long offset,int whence);

    函数参数:stream:要定位的流

                       offset:相对于基准点whence的偏移量,正数表示向前(向文件尾方向)移动,负数表示向后(向文件头方向)移动,0表示不移动

                    whence:基准点(取值见下)

    函数返回值:成功:0,改变读写位置

                        失败:EOF,不改变读写位置

    其中第三个参数whence的取值如下:

        SEEK_SET:代表文件起始位置,数字表示为0

        SEEK_CUR:代表文件当前的读写位置,数字表示为1

        SEEK_END:代表文件结束位置,数字表示为2

    使用fseek()函数可以定位流的读写位置,通过偏移量+基准值的计算将读写位置移动到指定位置,其中第二个参数offset的值为正时表示向后移动,为负时表示向前移动,0表示不动。

示例:读取一个文件的最后10个字节

#include<stdio.h>

#include<stdlib.h>

#define MAX 10

int main(int argc, const char *argv[])

{

    FILE *fp;

    char buf[MAX];

    int n;

    if(argc<2)

    {

        printf("arguments are too few\n");

        exit(0);

    }

    if((fp=fopen(argv[1],"r"))==NULL)

    {

        perror("cannot open file1");

        exit(0);

    }

    fseek(fp,-10,SEEK_END);

    fread(buf,sizeof(buf[0]),MAX,fp);

    for(n=0;n<MAX;n++)

    {

        putchar(buf[n]);

    }

    fclose(fp);

    return 0;

}

注意实际输出只能输出9个字符,因为最后一个字符为'\n'。

若想知道当前的读写位置的偏移量,则可以使用ftell()函数

函数ftell()

    需要头文件:#include<stdio.h>

    函数原型:int ftell(FILE *stream);

    函数参数:stream:要定位的流

    函数返回值:成功:返回当前的读写位置

                        失败:EOF

示例:在上一个示例程序中添加ftell()函数

#include<stdio.h>

#include<stdlib.h>

#define MAX 10

int main(int argc, const char *argv[])

{

    FILE *fp;

    char buf[MAX];

    int n;

    if(argc<2)

    {

        printf("arguments are too few\n");

        exit(0);

    }

    if((fp=fopen(argv[1],"r"))==NULL)

    {

        perror("cannot open file1");

        exit(0);

    }

    fseek(fp,-10,SEEK_END);

    printf("This location is %ld\n",ftell(fp));

    fread(buf,sizeof(buf[0]),MAX,fp);

    for(n=0;n<MAX;n++)

    {

        putchar(buf[n]);

    }

    fclose(fp);

    return 0;

}

练习:使用fseek()函数和ftell()函数求一个文件的大小。

提示:先使用fseek()定位到文件末尾,再使用ftell()得到值。

#include<stdio.h>

#include<stdlib.h>

#define MAX 10

int main(int argc, const char *argv[])

{

    FILE *fp;

    if(argc<2)

    {

        printf("arguments are too few\n");

        exit(0);

    }

    if((fp=fopen(argv[1],"r"))==NULL)

    {

        perror("cannot open file1");

        exit(0);

    }

    fseek(fp,0,SEEK_END);

    printf("File is %ld\n",ftell(fp));

    fclose(fp);

    return 0;

}

可以使用ls -l指令与该程序的执行结果进行对比。

9、其他常用标准I/O操作函数

①刷新缓冲区fflush()

函数fflush()

    需要头文件:#include<stdio.h>

    函数原型:int fflush(FILE *stream);

    函数参数:stream:操作的流

    函数返回值:成功:0

                        失败:EOF

    fflush()函数会清除(原意是“冲刷”)该流内的输出缓冲区并立即将输出缓冲区的数据写回,即强迫将缓冲区内的数据写回流stream指定的文件中。若fflush()的参数为0(或NULL)则会刷新所有已经打开的流的输出缓冲区。

注意:

    ⒈fflush()函数可能会执行失败,当fflush()函数执行失败时会返回EOF,这可能由于缓冲区数据意外丢失或其他未知原因。因此当某些重要文件需要设置时,若使用fflush()刷新缓冲区失败,则应考虑使用文件I/O相关操作(open()、close()、read()、write()等)来代替标准I/O操作。

    ⒉!!!请不要试图使用fflush()刷新stdin!!!

    在C标准和POSIX标准中,fflush()仅定义了刷新输出流的行为,对于输入流stdin的fflush()操作是“未定义”(undefined),因此不同操作系统不同编译器对fflush(stdin)的操作都不会相同。fflush(stdin)操作只对部分编译器(例如VC++)等有效,而现在的大多数编译器(gcc等)是无效的。按C99标准规定的fflush()函数定义来说,fflush()函数是不允许刷新stdin的。

(原文:For input streams, fflush() discards any buffered data that has been fetched from the underlying file, but has not been consumed by the application. 大意是说如果对fflush传入一个输入流,会清除已经从输入流中取出但还没有交给程序的数据,而对输入流中剩余的未被程序处理的数据没有提及,可能不受影响,也可能直接丢弃)

对于fflush(stdin)操作,该操作未被标准定义,行为不确定,不同系统不同编译器操作不相同(可移植性不好),因此极为不推荐使用fflush()刷新标准输入流stdin。

②判断文件是否已经结束feof()

函数feof()

    需要头文件:#include<stdio.h>

    函数原型:int feof(FILE *stream);

    函数参数:stream:操作的流

    函数返回值:文件结束:非0的值

                     文件未结束:0

    feof()函数可以检测流上的文件结束符,若文件结束则返回一个非0值,否则返回0

    文件结束符EOF的数值是0xFF(十进制为-1),在文本文件中我们可以通过fgetc()是否读取到EOF来判断文件是否结尾(见fgetc()的示例程序)。而在二进制文件中可以有数值-1,因此就无法使用fgetc()读取EOF的方法来判断文件是否结尾。这时我们可以使用feof()函数来判断,feof()函数既可以判断文本文件又可以判断二进制文件。

feof()的常用用法有

if(feof(stream))//判断该流是否已结尾

或者

while(!feof(stream))//循环操作该流直至文件结尾

等。

③回到文件开头rewind()

函数rewind()

    需要头文件:#include<stdio.h>

    函数原型:void rewind(FILE *stream);

    函数参数:stream:操作的流

    函数返回值:无

rewind()函数会将当前读写位置返回至文件开头(rewind原意为“(磁带等)回滚,倒带”),其等价于

(void)fseek(stream, 0L, SEEK_SET)

综合练习:循环记录系统的时间

每过1s,读取一次当前系统时间,之后写入到文件中。再次操作该文件不会删除文件内的原始数据而是继续向后书写数据。

提示:打开文件--->获取系统时间--->写入文件--->延时1s

                       ↑                        ↓

                 ----------死循环---------

答案:

#include<stdio.h>

#include<stdlib.h>

#include<time.h>

#define MAX 64

int main(int argc, const char *argv[])

{

    FILE *fp;

    char buf[MAX];

    int n;

    time_t t;

    if(argc<2)

    {

        printf("arguments are too few\n");

        exit(0);

    }

    if((fp=fopen(argv[1],"a+"))==NULL)

    {

        perror("cannot open file");

        exit(0);

    }

    while(1)

    {

        t = time(NULL);

        fprintf(fp,"%s",asctime(localtime(&t)));

        fflush(NULL);//刷新缓冲区

        printf("%s",asctime(localtime(&t)));

        sleep(1);

    }

    fclose(fp);

    return 0;

}

文件IO 第一天(标准IO)

猜你喜欢

转载自blog.csdn.net/nan_lei/article/details/81460798