对于不同的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” |
需要注意的是
fclose的函数原型如下
fclose接收一个流作为参数,把与这个流关联的文件关闭。
注意
和动态内存分配的malloc和free函数配对使用类似,fopen和fclose也要配对使用。
下面给出程序中常见的文件使用方式
#include
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函数以3中基本形式处理数据:单个字符、文本行和二进制数据。
对于每种形式,都有一组特定的函数对它们进行处理。
下面列出用于每种形式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 |
下面分别介绍各个函数
从流中读取单个字符由getchar函数家族实现;
向流中输出单个字符由putchar函数家族实现。
参数是需要操作的流,函数从流的当前位置读取一个字符,返回值是该字符的ASCII码。如果流中不存在更多字符,则返回常量值EOF(定义在stdio.h的一个标准I/O常量,是一个整形常量,用于标记文件末尾)
这里需要注意的是,返回值之所以是int型而不是char型,是为了处理返回值是EOF的情况。
这个函数和fgetc在使用上没有区别,区别在于getc是通过#define指令定义的宏,而fgetc是真正的函数。
getchar无需参数,因为它只从标准输入流读取字符,返回该字符的ASCII码值,在标准输入流(这里指键盘),可以通过连输三次ctrl+z来模拟EOF。
getchar也是通过#define指令定义的宏。
第一个int型参数是需要输出字符的ASCII码值,第二个参数是被写入的流。
putc在使用上和fputc相同,不同之处在于putc是宏,而fputc是函数
putchar只用于标准输出流,只需要一个参数,表示需要输出的字符的ASCII码值。
putchar也是宏。
未格式化的行I/O由fgets、gets、fputs、puts实现
功能:fgets从指定的流stream读取字符并复制到string中。
要点:
gets只用于标准输入流。从标准输入流读取一个字符串直到\n并且把\n替换成\0,返回值就是参数值。
需要注意的是,由于gets无法知道读取的字符串中字符个数是否会超出buffer的容量,很容易造成越界,因此gets在正经的程序中很少使用。
fputs第一个参数是以\0结尾的字符串,并将这个字符串输出到指定的流stream中。
如果成功输出,则返回一个非0值,否则返回EOF。
puts只用于标准输出流,参数是需要输出的字符串。
如果成功输出,则返回一个非0值,否则返回EOF。
需要注意的是,puts会在输出的字符串中添加一个换行符\n,以和gets配合使用。
格式化行I/O由scanf、printf、fscanf、fprintf、sscanf、sprintf实现。
我们已经用过不少次printf和scanf,这里主要对比以下这三组函数
不同点
区别在于
scanf和printf就不给例子了
#include
#include
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内容如下
#include
#include
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;
}
#include
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;
}
#include
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由函数fread和fwrite实现
四个参数中,
buffer是指向用于保存数据的内存位置的指针,
size读取的每个元素的大小,以字节为单位,
count是读取几个元素,
stream是数据读取的流
返回值是实际读取的元素个数
四个参数中,
buffer是指向用于保存数据的内存位置的指针,
size写入的每个元素的大小,以字节为单位,
count是写入几个元素,
stream是数据写入的流
返回值是实际写入的元素个数
注意
这两个函数,我们都可以通过其返回值是否小于count来判定是否读取、写入完毕。
正常情况下,数据以线性的方式读取或写入。C语言还支持随机访问I/O,这通过读取或写入前先定位文件的位置。
通过以下两个函数完成定位。
fseek用于改变流的当前位置到指定位置,
第一个参数说明需要改变的流,后两个参数一起确定指定位置。
第二个参数在第三个参数是SEEK_SET时才可以使用ftell的返回值,
第三个参数说明计算偏移量的相对位置,有三种选择:
from | 定位到 |
---|---|
SEEK_SET | 从流的起始位置起offset个字节,offset必须是一个非负值 |
SEEK_CUR | 从流的当前位置起offset个字节,offset的值可正可负 |
SEEK_END | 从流的尾部位置起offset个字节,offset的值可正可负,如果是正值,将定位到文件后面 |
注意
下面再介绍一个函数rewind,用于将文件指针设置为流的起始位置
rewind函数只需要一个流作为参数,把这个流的文件读写指针重新设置为文件的起始位置。
运行程序之前,先在源代码当前目录下创建一个名为text.txt的文件,保存abcdefg这几个字符。
#include
#include
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是一个定义在stdio.h中的整型常量,它的值是-1,-1在任何肯能出现的字符范围之外,所以可以用它来作为文件结束的标志,也即文件的末尾。
对于文本文件和二进制文件,有不同的方式判断它们何时读取结束
文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
前面讲过fread用于读取二进制文件,其原型如下
其返回值就是实际读取的元素个数,当其小于申请读取的个数count时,意味着读取结束,但不确定是因为遇到文件尾结束,还是因为在流遇到错误结束
判断是遇到文件尾还是在流遇到错误由feof和ferror两个函数完成
其返回值解释如下
feof以一个流为参数,如果这个流的当前读写取位置在文件末尾,则返回非0值,否则返回0
其返回值解释如下
ferror以一个流为参数,在读取或写入过程中,如果发生错误,FILE结构有相应的变量记录错误发生的位置,如果存在错误标记,则ferror返回非0值;否则返回0
运行前现在源文件当前目录下创建文件test.txt,其中保存字符串hello world。
#include
#include
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
#include
//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也会刷新缓冲区,这样我们就没有时间去观察这个写入缓冲区和从缓冲区发送到文件时间差了。