文章目录
一、流
对于不同的I/O设备,通常具有不同的特性和操作协议,操作系统负责这些不同设备的通信细节,并向程序员提供一个更为简单和统一的I/O接口。
ANSI C进一步对I/O的概念进行了抽象。就C程序而言,所有的I/O操作只是简单地从程序移进或移出字节,这种字节流就被称为流(stream)。
流分为两种类型:文本(text)流和二进制(binary)流。
文本流与文本文件
文本流中的字节以ASCII码值的形式写入到文件或设备中,适用于文本数据。与文本流相关联的文件就是文本文件。
二进制流和二进制文件
二进制流中的字节将完全根据程序编写它们的形式写入到文件或设备中,而且完全根据它们从文件或设备读取的形式读入到程序中,适用于非文本数据和文本数据。
二、文件
文件就是保存在外存上的一种数据类型。把数据存到文件中可以实现数据的持久化。
文件分类
C程序设计中,讨论的文件主要分为程序文件和数据文件(按使用功能角度分类)
程序文件
包括源文件(后缀为.c)、目标文件(windows下为.obj)、可执行程序(windows下为.exe)。
数据文件
用于程序读取、写入数据的文件。
文件标识
文件标识由三个部分组成:文件路径+文件主干名+文件后缀
例如:c:\code\test.txt
文件名通常指文件主干名+文件后缀
三、文件的打开和关闭
文件指针
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.
一般情况下,都是用指向FILE的指针来维护这个FILE结构的变量。比如下面创建的pf变量就是一个指向FILE结构的指针,我们把这种变量叫做文件指针变量。
FILE* PF;
通过文件指针变量,就能找到与之相关联的文件。
文件的打开和关闭
文件读写之前要打开文件,读写结束之后应关闭文件。
在C语言中,使用fopen函数来打开文件,使用fclose关闭文件。
fopen的函数原型如下
fopen第一个参数为用一个字符串,是所要打开文件的文件名。
需要注意的是:
- 如果该文件在源文件当前目录下,只要写文件主干名和文件后缀就可以了;
- 如果该文件不再源文件当前目录下,需要写文件的绝对路径。
fopen的第二个参数也是一个字符串,指定打开流的模式(只能在规定的把模式中选择)。决定打开的流适用于只读、只写还是既读又写,以及它是文本流还是二进制流。
下面列出常用模式
读取 | 写入 | 添加 | |
---|---|---|---|
文本 | “r” | “w” | “a” |
二进制 | “rb” | “wb” | “ab” |
需要注意的是
- 如果要打开的文件原先不存在,以读取的方式打开会打开失败,fopen返回NULL
- 如果要打开的文件原先不存在,以写入的方式打开,则会在源代码当前路径下新创建一个以第一个参数为文件名的文件。
- 如果要打开的文件原先不存在,以添加的方式打开,也会创建一个新的文件。
- 如果要打开的文件存在,以写入的方式打开,则源文件会被销毁,新创建一个同名文件。
- 如果要打开的文件存在,以添加的方式打开,则源文件会被销毁,新创建一个同名文件。
fclose的函数原型如下
fclose接收一个流作为参数,把与这个流关联的文件关闭。
注意
- fclose的参数不能是NULL,否则程序会崩溃,因此使用fopen后有必要检查返回值是否为null。
- 对于输出流,fclose在关闭文件之前会刷新缓冲区。
和动态内存分配的malloc和free函数配对使用类似,fopen和fclose也要配对使用。
下面给出程序中常见的文件使用方式
#include<stdio.h>
int main()
{
FILE* pf;
//打开文件
pf = fopen("test.txt","r");//以只读的方式打开一个文本流
if(NULL==pf)//检查文件是否打开成功,若失败则结束程序
{
perror("fail");
exit(EXIT_FAILURE);
}
//读文件,这里略
//关闭文件
fclose(pf);
pf = NULL;//避免野指针
return 0;
}
四、文件读写
文件的读写通过ANSI C中规定的I/O函数实现,下面先介绍I/O函数
I/O函数
I/O函数以3中基本形式处理数据:单个字符、文本行和二进制数据。
对于每种形式,都有一组特定的函数对它们进行处理。
下面列出用于每种形式I/O形式的函数,及函数家族。
执行字符、文本行和二进制的I/O函数
数据类型 | 输入 | 输出 | 描述 |
---|---|---|---|
字符 | getchar | putchar | 读取(写入)单个字符 |
文本行 | gets、scanf | puts、printf | 文本行未格式化的输入(输出)、格式化的输入(输出) |
二进制数据 | fread | fwrite | 读取(写入)二进制数据 |
输入输出函数家族
家族名 | 目的 | 可用于所有的流 | 只用于stdin和stdout | 用于内存中的字符串 |
---|---|---|---|---|
getchar | 字符输入 | fgetc、getc | getchar | 无 |
putchar | 字符输出 | fputc、putc | putchar | 无 |
gets | 文本行输入 | fgets | gets | 无 |
puts | 文本行输出 | fputs | puts | 无 |
scanf | 格式化输入 | fscanf | scanf | sscanf |
printf | 格式化输出 | fprintf | printf | sprintf |
下面分别介绍各个函数
字符I/O
从流中读取单个字符由getchar函数家族实现;
向流中输出单个字符由putchar函数家族实现。
fgetc
参数是需要操作的流,函数从流的当前位置读取一个字符,返回值是该字符的ASCII码。如果流中不存在更多字符,则返回常量值EOF(定义在stdio.h的一个标准I/O常量,是一个整形常量,用于标记文件末尾)
这里需要注意的是,返回值之所以是int型而不是char型,是为了处理返回值是EOF的情况。
getc
这个函数和fgetc在使用上没有区别,区别在于getc是通过#define指令定义的宏,而fgetc是真正的函数。
getchar
getchar无需参数,因为它只从标准输入流读取字符,返回该字符的ASCII码值,在标准输入流(这里指键盘),可以通过连输三次ctrl+z来模拟EOF。
getchar也是通过#define指令定义的宏。
fputc
第一个int型参数是需要输出字符的ASCII码值,第二个参数是被写入的流。
putc
putc在使用上和fputc相同,不同之处在于putc是宏,而fputc是函数
putchar
putchar只用于标准输出流,只需要一个参数,表示需要输出的字符的ASCII码值。
putchar也是宏。
未格式化的行I/O
未格式化的行I/O由fgets、gets、fputs、puts实现
fgets
功能:fgets从指定的流stream读取字符并复制到string中。
要点:
- 在读取到第一个换行符(fgets保留换行符)或读取字符数到达n-1(因为fgets会在读取的字符串末尾添加一个‘\0‘)时停止读取。
- 下一次调用时fgets将从流的下一个字符开始读取。
- 如果在读取任何字符之前就到达了文件尾,fgets返回NULL;否则fgets返回它的第一个参数。这个返回值一般只用于检测是否到达了文件尾。
gets
gets只用于标准输入流。从标准输入流读取一个字符串直到\n并且把\n替换成\0,返回值就是参数值。
需要注意的是,由于gets无法知道读取的字符串中字符个数是否会超出buffer的容量,很容易造成越界,因此gets在正经的程序中很少使用。
fputs
fputs第一个参数是以\0结尾的字符串,并将这个字符串输出到指定的流stream中。
如果成功输出,则返回一个非0值,否则返回EOF。
puts
puts只用于标准输出流,参数是需要输出的字符串。
如果成功输出,则返回一个非0值,否则返回EOF。
需要注意的是,puts会在输出的字符串中添加一个换行符\n,以和gets配合使用。
格式化的行I/O
格式化行I/O由scanf、printf、fscanf、fprintf、sscanf、sprintf实现。
我们已经用过不少次printf和scanf,这里主要对比以下这三组函数
scanf家族
相同点
- 每个原型中的省略号表示一个可变长度的指针列表。从输入转换来的值逐个存储到这些指针所指向的内存位置。
- 都从输入源读取字符并根据format字符串给出的格式代码进行转换。
- 当格式化字符串到达末尾或者读取的输入不再匹配格式字符串指定的类型时输入停止。
- 如果在任何输入被转换之前文件就已经到达文件尾部,函数返回EOF。
不同点
- fscanf的输入源是作为参数的流stream
- scanf的输入源只能是标准输入
- sscanf从第一个参数所给的字符串中读取字符
printf家族
三个函数的参数中后两个参数都相同。
相同点在于
- 都是根据参数format决定如何对参数列表中的值进行格式化。
- 返回值都是实际打印或存储的字符数。
区别在于
- printf把结果输出到标准输出。
- fprintf把结果输出到流参数stream中,适用于任何输出流。
- sprintf把格式化结果作为一个以\0为结尾的字符串存储到指定的buffer缓冲区(在内存)中
例子
scanf和printf就不给例子了
- fprintf
#include<stdio.h>
#include<stdlib.h>
int main()
{
FILE* pf;
int a = 10;
double b = 3.1415;
pf = fopen("data.txt", "w");
if (NULL == pf)
{
perror("fail");
exit(EXIT_FAILURE);//这个宏定义于stdlib.h
}
fprintf(pf, "%d %f", a, b);
fclose(pf);
pf = NULL;
return 0;
}
运行后在源文件当前目录下可以看到创建了data.txt文件,点开后data.txt内容如下
- fscanf
#include<stdio.h>
#include<stdlib.h>
int main()
{
FILE* pf;
int a;
double b;
pf = fopen("data.txt", "r");//读的方式打开文本流
if (NULL == pf)
{
perror("fail");
exit(EXIT_FAILURE);//这个宏定义于stdlib.h
}
fscanf(pf, "%d %lf", &a, &b);//注意和printf的格式码不同,scanf对于double需要%lf
//打印验证
printf("%d %f", a, b);
fclose(pf);
pf = NULL;
return 0;
}
运行结果如下
- sprintf
#include<stdio.h>
int main()
{
char arr[100] = { 0 };
int a = 10;
double b = 3.145;
char* c = "hello world";
sprintf(arr, "%d %f %s", a, b, c);
printf("%s\n", arr);
return 0;
}
运行结果如下
- sscanf
#include<stdio.h>
int main()
{
char arr[100] = { 0 };
int a = 10;
double b = 3.145;
char* c = "hello world";
sprintf(arr, "%d %f %s", a, b, c);
printf("%s\n", arr);
int d;
double e;
char f[20] = { 0 };
sscanf(arr, "%d %lf %s", &d, &e, f);
printf("%d %f %s\n", d, e, f);//注意scanf的读取特点,以%s的格式读取,遇到空格停止
return 0;
}
运行结果如下
二进制I/O
把数据写到文件中时,效率最高的是用二进制形式写入。
二进制输出避免了在数值转换为字符串的过程中所涉及的开销和精度损失。
二进制I/O由函数fread和fwrite实现
fread
四个参数中,
buffer是指向用于保存数据的内存位置的指针,
size读取的每个元素的大小,以字节为单位,
count是读取几个元素,
stream是数据读取的流
返回值是实际读取的元素个数
fwrite
四个参数中,
buffer是指向用于保存数据的内存位置的指针,
size写入的每个元素的大小,以字节为单位,
count是写入几个元素,
stream是数据写入的流
返回值是实际写入的元素个数
注意
这两个函数,我们都可以通过其返回值是否小于count来判定是否读取、写入完毕。
五、文件随机读写
正常情况下,数据以线性的方式读取或写入。C语言还支持随机访问I/O,这通过读取或写入前先定位文件的位置。
通过以下两个函数完成定位。
ftell
ftell函数的参数是一个流,返回流的当前位置。
- 所谓当前位置,就是下一个读取或写入将要开始的位置距离文件起始位置的偏移量。
- 偏移量以字节为单位。
fseek
fseek用于改变流的当前位置到指定位置,
第一个参数说明需要改变的流,后两个参数一起确定指定位置。
第二个参数在第三个参数是SEEK_SET时才可以使用ftell的返回值,
第三个参数说明计算偏移量的相对位置,有三种选择:
from | 定位到 |
---|---|
SEEK_SET | 从流的起始位置起offset个字节,offset必须是一个非负值 |
SEEK_CUR | 从流的当前位置起offset个字节,offset的值可正可负 |
SEEK_END | 从流的尾部位置起offset个字节,offset的值可正可负,如果是正值,将定位到文件后面 |
注意
- 不能定位到文件起始位置之前
- 定位到文件尾之后并写入将扩展这个文件。
下面再介绍一个函数rewind,用于将文件指针设置为流的起始位置
rewind
rewind函数只需要一个流作为参数,把这个流的文件读写指针重新设置为文件的起始位置。
例子
运行程序之前,先在源代码当前目录下创建一个名为text.txt的文件,保存abcdefg这几个字符。
#include<stdio.h>
#include<stdlib.h>
int main()
{
FILE* pf;
pf = fopen("text.txt", "r");
if (NULL == pf)
{
perror("fail");
exit(1);
}
//定位文件指针
fseek(pf, -3, SEEK_END);
int ch = fgetc(pf);
printf("%c\n", ch);
//计算偏移量
int pos = ftell(pf);
printf("%d\n", pos);
//rewind
rewind(pf);
ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
六、文件结束的判定
将文件结束判定前,先介绍EOF预定义常量
EOF
EOF是一个定义在stdio.h中的整型常量,它的值是-1,-1在任何肯能出现的字符范围之外,所以可以用它来作为文件结束的标志,也即文件的末尾。
对于文本文件和二进制文件,有不同的方式判断它们何时读取结束
文本文件读取结束
文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
- fgetc的返回值为EOF时读取结束,但这个EOF并不意味着到达了文件末尾;读取结束可能是因为遇到了文件末尾,也可能是在流遇到了错误。
- fgets的返回值为NULL时读取结束,读取结束可能是因为遇到了文件末尾,也可能是在流遇到了错误。
二进制文件读取结束
二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
前面讲过fread用于读取二进制文件,其原型如下
其返回值就是实际读取的元素个数,当其小于申请读取的个数count时,意味着读取结束,但不确定是因为遇到文件尾结束,还是因为在流遇到错误结束
判断是遇到文件尾还是在流遇到错误
判断是遇到文件尾还是在流遇到错误由feof和ferror两个函数完成
feof
其返回值解释如下
feof以一个流为参数,如果这个流的当前读写取位置在文件末尾,则返回非0值,否则返回0
ferror
其返回值解释如下
ferror以一个流为参数,在读取或写入过程中,如果发生错误,FILE结构有相应的变量记录错误发生的位置,如果存在错误标记,则ferror返回非0值;否则返回0
文本文件例子
运行前现在源文件当前目录下创建文件test.txt,其中保存字符串hello world。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int c;//注意必须为int型,因为要处理EOF
FILE* pf = fopen("test.txt", "r");
if (!pf)
{
perror("fail");
exit(EXIT_FAILURE);
}
//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while ((c = fgetc(pf))!= EOF)
{
putchar(c);
}
putchar('\n');
//判断原因
if (ferror(pf))
{
puts("I/O error when reading");
}
else if (feof(pf))
{
puts("End of file reached successfully");
}
fclose(pf);
pf = NULL;
return 0;
}
二进制文件例子
#define SIZE 5
int main()
{
double a[SIZE] = { 1.,2.,3.,4.,5. };
FILE* pf = fopen("test.bin", "wb");
fwrite(a, sizeof * a, SIZE, pf);
fclose(pf);
double b[SIZE];
pf = fopen("test.bin", "rb");
size_t ret_code = fread(b, sizeof * b, SIZE, pf);
if (SIZE == ret_code)
{
puts("Array read successfully, contents:");
int i;
for (i = 0; i < SIZE; i++)
{
printf("%f ", b[i]);
}
putchar('\n');
}
else//错误或遇到文件尾
{
if (ferror)
{
perror("Error reading test.bin");
}
else if (feof)
{
printf("Error reading test.bin: unexpected end of file\n");
}
}
fclose(pf);
pf = NULL;
return 0;
}
七、文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
测试例子
#include <stdio.h>
#include <windows.h>
//WIN10环境测试
int main()
{
FILE*pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
//注:fflush 在高版本的VS上不能使用了
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0; }
该程序先创建一个test.txt的文件,接着把字符串abcdef发送到输出缓冲区,此时还没有写入文件,此时让程序睡眠1000毫秒,在这10秒里,可以打开这个test.txt文件,可以看到里面什么也没有;10秒后刷新缓冲区,则把缓冲区的内容发送到文件,可以看到文件中出现了abcdef。
这个程序的关键在于让程序睡眠10秒以便观察,因为fclose也会刷新缓冲区,这样我们就没有时间去观察这个写入缓冲区和从缓冲区发送到文件时间差了。