C语言 文件

一、文件

1.1 什么是文件

 磁盘文件在DOS管理中被定义为存贮在外部介质上的程序或数据的集合,是一批逻辑上有联系的数据。每个文件都有个文件名作为标识,每个文件在磁盘中的具体存放位置、格式都由操作系统中的文件系统管理,也就是说,操作系统是以文件为单位对程序或数据进行管理的。
 在C语言中文件的含义更为广泛,不仅包含以上所述的磁盘文件,还包括一切能进行输入/输出的终端设备,它们被看成是设备文件。如键盘常称为标准输入文件,显示器称为标准输出文件。即在C语言中文件是由磁盘文件和设备文件组成的,磁盘文件根据功能又分为程序文件和数据文件

1.2 文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用,文件名包含3部分:文件路径+文件名主干+文件后缀
例如Windows下: c:\code\test.txt
Linux下: /tmp/file.txt (Linux系统不识别文件后缀名,后缀名是给用户看的)

二、 文件流的打开与关闭

2.1 流

流是一个高度抽象概念
 当前计算机具有大量不同设备,很多与I/O操作有关。如:软盘、硬盘、通信端口、视屏适配器等。每种设备具有不同的特性和操作协议,操作系统负责与这些不同设备的I/O细节,并向程序员提供一个更为简单和统一的I/O接口
 C语言进一步对I/O接口进行抽象引入流的概念。根据流的方向分为输入流和输出流。程序要输入数据只需从输入流中读入;输出数据只需向输出流中写即可。特定I/O设备的细节对程序员是隐藏的。

在这里插入图片描述

说明:每个C语言程序运行时默认打开3个流,分别为标准输入流(stdin)、标准输出流(stdout)、标准错误流(stderr)。标准输入默认是键盘,标准输出默认是显示器

2.2 文件指针类型(FILE *)

每个被使用的文件都会在内存中开辟了一个对应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是FILE类型,在<stdio.h>中定义,我们也将这个结构体变量称为文件流

1
2
3
4
5
6
7
8
9
10
11
12
//VS2013 IDE中<stdio.h>头文件对FILE的定义
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;

不同的C编译器中FILE类型结构体包含的内容不完全相同,但是大同小异

每当打开一个文件的时候,系统会根据文件的信息自动创建一个FILE类型结构体变量,并填充其中的内容,
使用者不必关心细节。为了使用方便,一般都是通过一个FILE * 的指针来维护这个FILE类型结构体变量,所以FILE * 指针指向一个文件流

1
FILE* pf;//文件指针变量

pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量),通过该文件信息区中的信息就能够访问该文件
在这里插入图片描述

2.3 文件流的打开和关闭

fopen函数功能:
打开一个指定文件,并返回一个指向该文件流(文件信息区)的FILE * 指针

库函数fopen函数声明
FILE * fopen ( const char * filename, const char * mode );
返回值:打开文件成功返回指向该文件流(文件信息区)的FILE * 指针,打开失败返回NULL指针,所以应该检查fopen函数的返回值
filename :需打开文件或设备名字,可以是绝对路径或相对路径写法
mode:文件打开模式

常用模式说明:
r :以只读方式打开文件,该文件必须存在
w :以只写方式打开文件,若文件存在则文件长度清为零,即该文件内容会消失;若文件不存在则创建该文件。
a :以追加的方式打开只写文件。若文件不存在,则会创建该文件;如果文件存在,则写入的数据会被加到文件末尾
b :打开二进制文件,配合上面r、w、a使用

fclose函数功能:
刷新文件缓冲区,关闭一个指定文件(释放文件流)

库函数fclose函数声明
int fclose ( FILE * stream );
返回值:关闭成功返回0,失败返回EOF
stream:指向文件流的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main()
{
FILE* pf = fopen("file.txt", "r"); //以只读方式打开一个文件file.txt
if (NULL == pf) //打开失败
{
perror("fopen"); //打印错误信息
return -1; //退出程序
}
//读操作
fclose(pf); //关闭文件
pf = NULL;
return 0;
}

三、 顺序读写

顺序读写:对文件的输入和输出只能按照顺序读写

3.1 字符函数

3.1.1 字符输入 fgetc函数

fgetc函数功能:
返回指定流的位置指示器指向的字符,然后位置指示器向前移动一个字符,简单理解就是从指定流读取1个字符

库函数fgetc函数声明
int fgetc ( FILE * stream );
返回值:读取成功返回对应字符的ascii码值。读取到文件结束标志时,则函数返回EOF并设置流的文件结束指示符(feof)。如果发生读错误,该函数返回EOF并设置流的错误指示符(ferror)
stream:指向输入流(文件流、标准输入流)的指针

测试1:从文件中读取字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main()
{
FILE* pf = fopen("file.txt", "r"); //file.txt存在,内容为hello
if (NULL == pf)
{
printf("%s\n", strerror(errno));
return 1;
}
printf("%c\n", fgetc(pf)); //输出读取第1个字符
printf("%c\n", fgetc(pf)); //输出读取第2个字符
fclose(pf);
pf = NULL;
return 0;
}

输出

1
2
h
e

测试2:从标准输入中读取字符

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
char c = fgetc(stdin);
printf("%c\n", c);
return 0;
}

输入

1
a

输出

1
a

3.1.2 字符输出 fputc函数

fputc函数功能:
将一个字符写入流并使位置指示器向前移动,即字符被写入到流的位置指示器所指示的位置,然后该位置指示器自动前进1,简单理解就是写1个字符到流中

库函数fputc函数声明
int fputc ( int character, FILE * stream );
返回值:如果成功,则返回所写的字符。如果发生写错误,则返回EOF并设置错误指示符(ferror)。
character :待写入字符
stream:指向输出流(文件流、标准输出流)的指针

测试1:输出字符到文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
int main()
{
//将字符'a' 'b' 'c' 写入到文件
FILE* pf = fopen("file.txt", "w");
if (NULL == pf)
{
printf("%s\n", strerror(errno));
return 1;
}
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
fclose(pf);
pf = NULL;
return 0;
}

测试2:输出字符到标准输出

1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
int c = fputc('h', stdout);
c = fputc('e', stdout);
c = fputc('l', stdout);
return 0;
}

屏幕上显示

1
hel

3.2 未格式化文本行函数

3.2.1 未格式化文本行输入 fgets函数

fgets函数功能:
从流中读取num-1个字符到字符数组str中,或者遇到换行符或文件结束符停止(以最先发生的为准)。换行符会使fgets停止读取,但函数会认为它是一个有效的字符读取到str,并添加’\0’使成为一个字符串

库函数fgets函数声明
char * fgets ( char * str, int num, FILE * stream );
返回值:读取成功,函数返回str。如果在读取中时遇到文件结束符,返回NULL指针并设置流的文件结束指示符(feof)。 如果发生读错误,返回NULL指针则设置错误指示符(ferror)
str :字符数组的数组名
num:最多复制num个字符到str数组
stream:指向输入流(文件流、标准输入流)的指针

测试1:从文件中读取未格式文本行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int main()
{
FILE* pf = fopen("file.txt", "r"); //文件file.txt内容为:Hello World
if (NULL == pf)
{
printf("%s\n", strerror(errno));
return 1;
}
char arr[6] = {0};
fgets(arr, 5, pf); //读取(5-1)个字符到arr数组,并添加'\0'到数组,即"Hell"
printf("%s", arr); //打印Hell
fgets(arr, 5, pf); ////读取(5-1)个字符到arr数组,并添加'\0'到数组,即"o Wo"
printf("%s", arr); //打印o Wo
fclose(pf);
pf = NULL;
return 0;
}

输出

1
Hello Wo

测试2:从标准输入中读取未格式化文本行

1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
char arr[] = "##########";
fgets(arr, 6, stdin);
printf("%s\n", arr);
return 0;
}

输入

1
abcdef

输出

1
abcde

3.2.2 未格式化文本行输出 fputs函数

fputs函数功能:
将str指向的字符串写入流。函数从指定的地址(str)开始复制,遇到’\0’停止。’\0’不会复制到流中

库函数fputs函数声明
int fputs ( const char * str, FILE * stream );
返回值:如果成功,将返回一个非负值。出错时函数返回EOF并设置流的错误指示符(ferror)
str :需写入的字符串起始地址
stream:指向输出流(文件流、标准输出流)的指针

测试1 :输出未格式化文本行到文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main()
{
FILE* pf = fopen("file.txt", "w");
if (NULL == pf)
{
printf("%s\n", strerror(errno));
return 1;
}
fputs("Hello World", pf);
fputs("Hello WuHan", pf);
fclose(pf);
pf = NULL;
return 0;
}

打开file.txt文件发现内容为:

1
Hello WorldHello WuHan

所以当要写入多行时需要在字符串中加入换行符,即 fputs(“Hello World\n”, pf);

测试2 :输出未格式化文本行到标准输出

1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
char arr1[] = "Hello World\n";
fputs(arr1, stdout);
fputs("Hello Wuhan\n", stdout);
return 0;
}

屏幕上显示

1
2
Hello World
Hello Wuhan

3.3 格式化文本行函数

3.3.1 格式化文本行输入函数 fscanf

fscanf函数功能:
功能与scanf一样,区别:scanf是从标准输入流读取格式化数据,fscanf从输入流读取格式化数据。
从流中读取数据,并根据参数格式将其存储到附加参数所指向的位置。附加的参数应该指向已经分配的变量,其类型由格式字符串中相应的格式说明符指定

库函数fscanf声明
int fscanf ( FILE * stream, const char * format, ... );
返回值:返回参数列表中成功填充的个数。如果填充任何数据之前遇到文件结束或读错误返回EOF。读取时遇到文件结束符设置流的文件结束指示符(feof),读取时发生错误设置流的错误指示符(ferror)
stream:指向输入流(文件流、标准输入流)的指针
format:格式化字符串
… :待填充变量的地址

测试1:从文件中读取格式文本行
file.txt文件内容

1
2
张三 18
李芳 16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
struct student
{
char name[20];
int age;
char sex[4];
};
int main()
{
FILE* pf = fopen("file.txt", "r");
if (NULL == pf)
{
printf("%s\n", strerror(errno));
return 1;
}
struct student s1 = {0}; //创建结构体变量s1
struct student s2 = {0}; //创建结构体变量s2
fscanf(pf, "%s %d %s", s1.name, &s1.age, s1.sex); //读取文件中格式化数据并填充到结构体变量中,name、sex是数组名,所以s1.name、s1.sex没有用&运算符
fscanf(pf, "%s %d %s", s2.name, &s2.age, s2.sex);
printf("姓名:%s 年龄:%d 性别:%s\n", s1.name, s1.age, s1.sex);
printf("姓名:%s 年龄:%d 性别:%s\n", s2.name, s2.age, s2.sex);
fclose(pf);
pf = NULL;
return 0;
}

输出

1
2
姓名:张三 年龄:18 性别:男
姓名:李芳 年龄:16 性别:女

测试2 :输出格式化文本行到标准输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
struct student
{
char name[20];
int age;
char sex[4];
};
int main()
{
struct student s1 = { 0 };
fscanf(stdin, "姓名:%s 年龄:%d 性别:%s", s1.name, &s1.age, s1.sex); //从标准输入流读取格式化数据并填充到变量s1中
printf("姓名:%s 年龄:%d 性别:%s\n", s1.name, s1.age, s1.sex);
return 0;
}

输入

1
姓名:小明 年龄:18 性别:男

输出

1
姓名:小明 年龄:18 性别:男

3.3.2 格式化文本行输出函数 fprintf

fprintf函数功能:
功能与printf一样,区别:printf将格式化数据输出到标准输出流,fprintf将格式化数据输出到输出流

库函数fprintf声明
int fprintf ( FILE * stream, const char * format, ... );
返回值:如果成功返回写入的字符总数。如果发生写错误,返回一个负数并设置流的错误指示符(ferror)
stream:指向输出流(文件流、标准输出流)的指针
format:格式化字符串
… :变量

测试1 :输出格式化文本行到文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
struct student
{
char name[20];
int age;
char sex[4];
};

int main()
{
FILE* pf = fopen("file.txt", "w");
if (NULL == pf)
{
printf("%s\n", strerror(errno));
return 1;
}
struct student s1 = { "小明",18,"男" };
struct student s2 = { "李芳",20,"女" };
fprintf(pf, "%s %d %s\n", s1.name, s1.age, s1.sex);
fprintf(pf, "%s %d %s", s2.name, s2.age, s2.sex);
fclose(pf);
pf = NULL;
return 0;
}

file.txt文件内容

1
2
小明 18
李芳 20

测试2 :输出格式化文本行到标准输出流

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
struct student
{
char name[20];
int age;
char sex[4];
};

int main()
{
struct student s1 = { "小明",18,"男" };
fprintf(stdout, "姓名:%s 年龄:%d 性别:%s\n", s1.name, s1.age, s1.sex);
}

输出

1
姓名:小明 年龄:18 性别:男

四、随机读写

随机读写:以任意顺序读写文件的不同位置,文件的读写位置是由文件信息区(文件流)中位置指示器决定。可以理解成记事本中的光标

4.1 获取文件当前位置 ftell函数

ftell函数功能:
返回流的位置指示器的当前值(文件当前位置)

库函数ftell声明
long int ftell ( FILE * stream );
返回值:对于二进制流,这是当前位置距离文件起始位置之间字节数。对于文本流,数值可能没有意义(因为不同系统换行符不一样,如:windwos换行符为\r\n  linux换行符为\n)。失败返回-1L
stream:指向流的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int main()
{
FILE* pf = fopen("file.txt", "r");
if (NULL == pf)
{
printf("%s\n", strerror(errno));
return 1;
}
long position = ftell(pf); //获取文件当前位置
printf("文件当前位置:%d\n", position);
fgetc(pf); //读取位置指示器的字符,读完后位置指示器向前移动
position = ftell(pf);
printf("文件当前位置:%d\n", position);
fclose(pf);
pf = NULL;
return 0;
}

输出

1
2
文件当前位置:0
文件当前位置:1

4.2 改变文件当前位置 fseek函数

fseek函数功能:
将流的位置指示器(当前位置)设置为指定位置,成功调用该函数后,清除流的文件结束指示符(feof)

库函数fseek声明
int fseek ( FILE * stream, long int offset, int origin );
返回值:如果成功返回0。否则返回非0。如果发生读或写错误,则设置流的错误指示符(ferror)。
stream:指向流的指针
offset:距origin的偏移量
origin:有3个值。SEEK_SET :表示文件开头位置 SEEK_CUR :文件当前位置 SEEK_END:文件结束位置

测试
file.txt文件内容

1
hello
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main()
{
FILE* pf = fopen("file.txt", "r");
if (NULL == pf)
{
printf("%s\n", strerror(errno));
return 1;
}
char c = fgetc(pf);
printf("%c\n", c); //读取第一个字符'h',读完后位置指示器向前移动1字符
c = fgetc(pf); //读取第二个字符'e',读完后位置指示器向前移动1字符
printf("%c\n", c);
fseek(pf, -1, SEEK_CUR); //将位置指示器从当前位置向后移动1字符,即倒退1个字符,位置指示器指向字符'e'
c = fgetc(pf);
printf("%c\n", c);
fclose(pf);
pf = NULL;
return 0;
}

输出

1
2
3
h
e
e

4.3 设置文件当前位置为起始位置 rewind函数

rewind函数功能:
将流的位置指示器(当前位置)设置为文件的起始位置,成功调用该函数后,清除流的文件结束指示符(feof)和错误指示符(ferror)

库函数rewind声明
void rewind ( FILE * stream );
stream:指向流的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
int main()
{
FILE* pf = fopen("file.txt", "r");
if (NULL == pf)
{
printf("%s\n", strerror(errno));
return 1;
}
long position = ftell(pf); //获取文件当前位置
printf("文件当前位置:%d\n", position);
fseek(pf, 3, SEEK_SET);
position = ftell(pf);
printf("文件当前位置:%d\n", position);
rewind(pf);
position = ftell(pf);
printf("文件当前位置:%d\n", position);
fclose(pf);
pf = NULL;
return 0;
}

输出

1
2
3
文件当前位置:0
文件当前位置:3
文件当前位置:0

五、二进制读写

上面的顺序读写和随机读写都是对字符进行操作,在C语言中称为文本流(JAVA中称为字符流),但把数据写入到文件中效率最高的方法是用二进制写入,二进制写入可以避免在数值转换为字符串过程中所涉及的开销和精度丢失,但二进制并非人眼可以阅读。这种二进制读写在C语言称为二进制流(JAVA中称为字节流)

5.1 二进制写 fwrite函数

fwrite函数功能:
将包含count个元素的数组从ptr指向的内存块写入到流中的当前位置,每个元素的大小为size字节

库函数fread声明
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
返回值:返回成功写入的元素总数。如果此数字与count值不同,则写入错误并设置流的错误指示符( ferror ) 。 如果size或count为零,则该函数返回零并且错误指示符保持不变。
ptr: 指向要写入的元素数组起始地址
size :元素的大小(单位为字节)
count:元素的数量
stream:指向输出流(文件流、标准输出流)的指针

测试1:将数据以二进制方式写入到文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <errno.h>
struct student
{
char name[20];
int age;
char sex[4];
};

int main()
{
FILE* pf = fopen("file.txt", "wb"); //w为写模式,b为二进制
if (NULL == pf)
{
printf("%s\n", strerror(errno));
return 1;
}
struct student s1 = { "小明",18,"男" };
struct student s2 = { "张三",20,"男" };
fwrite(&s1, sizeof(struct student), 1, pf); //将结构体变量s1起始内存地址,元素个数为1,大小为sizeof(struct student)数据写入到文件
fwrite(&s2, sizeof(struct student), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}

测试2:将数据以二进制方式写入标准输出流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
struct student
{
char name[20];
int age;
char sex[4];
};

int main()
{
struct student s1 = { "小明",18,"男" };
struct student s2 = { "张三",20,"男" };
fwrite(&s1, sizeof(struct student), 1, stdout); //输出到显示器上内容看不懂,因为是二进制数据
fwrite(&s2, sizeof(struct student), 1, stdout);
return 0;
}

5.2 二进制读 fread函数

fread函数功能:
从流中读取count个元素,每个元素的大小为size字节,并将它们存储在ptr指定的内存块中

库函数fread声明
size_t fread ( const void * ptr, size_t size, size_t count, FILE * stream );
返回值:返回成功读取的元素总数。如果size或count为零,则函数返回0。如果返回值与size值不同,则在读取时发生了读取错误或遇到文件结束符,在这两种情况下,设置流的错误指示符(ferror)或文件结束指示符(feof)
ptr: 存放读入数据的内存块地址
size :元素的大小(单位为字节)
count:元素的个数
stream:指向输入流(文件流、标准输入流)的指针

测试:从文件读取二进制数据(该file.txt文件为5.1 测试1写入的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <errno.h>

struct student
{
char name[20];
int age;
char sex[4];
};

int main()
{
FILE * pf = fopen("file.txt", "rb"); //r以只读模式打开,b 二进制
if (NULL == pf)
{
printf("%s\n", strerror(errno));
return 1;
}
struct student s1 = { 0 };
struct student s2 = { 0};
fread(&s1, sizeof(struct student), 1, pf);
fread(&s2, sizeof(struct student), 1, pf);
printf("姓名:%s 年龄:%d 性别:%s\n", s1.name, s1.age, s1.sex);
printf("姓名:%s 年龄:%d 性别:%s\n", s2.name, s2.age, s2.sex);
fclose(pf);
pf = NULL;
return 0;
}

输出

1
2
姓名:小明 年龄:18 性别:男
姓名:张三 年龄:20 性别:男