文件分类

在C程序设计中,一般将文件按功能分为程序文件和数据文件

程序文件

C程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行文

件(windows环境后缀为.exe)

数据文件

文件不一定是程序,而是程序运行时读写的数据,如程序运行需要从中读取数据或写

入数据的文件

二进制文件和文本文件

根据数据的组织形式,数据文件被称为文本文件或二进制文件

数据在内存中以二进制的形式存储,如果不加转换的输出到外存的文件中,就是二进

制文件

如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的

形式存储的文件就是文本文件

字符一律以 ASCII 码存放,数值型数据既可以用 ASCII 码形式存储,也可以用二进制

存储

文件的打开和关闭

程序的数据需要和外部设备进行频繁的操作,为了方便对各种设备进行方便的操作,

抽象出了流的概念。

简单来说,流是连接程序和外部设备的管道,屏蔽了设备差异,流提供了统一的接口,

程序只需要调用这些接口就可以了

标准流

C语言程序启动时,默认打开了三个流:

  • stdin 标准输入流,在大多数的环境中从键盘输入,scanf函数就是从标准

    输入流中读取数据

  • stdout 标准输出流,大多数的环境中输出至显示器界面,printf 函数就是

    将信息输出到标准输出流中

  • stderr 标准错误流,大多数环境中输出到显示器界面。


stdin stdout stderr 三个流的类型是: FILE* , 通常称为文件指针

C语言中,就是通过 FILE* 的文件指针来维护流的各种操作

文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用

的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如

文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变

量中的。该结构体类型是由系统声明的,取名 FILE

例如在 visual studio 2013 中 stdio.h 头文件中有如图文件声明

不同的编译器的 FLIE 类型包含的内容不完全相同,大同小异

C程序打开一个文件时,系统会根据文件情况自动创建一个 FILE 结构的变量,并填充

其中的信息,定义一个 FILE* 类型的指针能够间接找到与它关联的文件

打开和关闭文件

文件在读写前应该先打开,使用结束后应该要关闭

程序在打开文件时会返回一个 FILE* 指针指向该文件

ANSI C 规定使用 fopen 打开文件 , fclose 关闭文件

语法格式

1
2
3
4
FILE * fopen ( const char * filename, const char * mode );


int fclose ( FILE * stream );

mode 表示打开的模式,如图:

例如

1
2
3
4
5
6
7
8
9
10
11
FILE* pf = fopen("66.txt", "r");
if (!pf)
{
// 不存在就读取失败
perror("fopen");
return 1;
}

fclose(pf);
// fclose 关闭文件不会将 pf 置空,需要手动置空
pf = NULL;

文件的顺序读写

演示 fputc

语法格式:

1
int fputc ( int character, FILE * stream );

写入成功的返回值是写入的字符,写入失败返回 EOF

1
2
3
4
5
6
7
8
9
10
11
12
FILE* pf = fopen("test1.txt", "w");
if (!pf)
{
//判空
perror("fopen");
return 1;
}

fputc('a', pf);
//关闭文件
fclose(pf);
pf = NULL;

多次写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FILE* pf = fopen("test1.txt", "w");
if (!pf)
{
//判空
perror("fopen");
return 1;
}

fputc('a', pf);
// 多次写入
for (char c = 'a';c <= 'z';c++)
{
fputc(c, pf);
}
//关闭文件
fclose(pf);
pf = NULL;


有写入就有读取

1
int fgetc ( FILE * stream );

读取成功后,会将读到的字符的 ASCII 码值返回

读取失败或读取到文件末尾返回 EOF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FILE* pf = fopen("test1.txt", "r");
if (!pf)
{
//判空
perror("fopen");
return 1;
}

int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
// fgetc 读取失败或读取到文件末尾返回 EOF
// 多次读取 fgetc 会自动往后读取
printf("%c ", ch);
}
//关闭文件
fclose(pf);
pf = NULL;

如果要写入或者读取字符串的话,要用 fputs 和 fgets

1
2
3
int fputs ( const char * str, FILE * stream );

char * fgets ( char * str, int num, FILE * stream );

fputs 写入成功后返回非负值, 写入失败返回 EOF

fgets 中 str 用于存放读取到的字符串,num 用于指定最多读取多少个字符(包含

‘\0’), stream 表示从哪个文件读取

读取成功返回 str 的首元素地址,读取失败或读到文件末尾会返回 NULL

1
2
3
4
5
6
7
8
9
10
11
12
FILE* pf = fopen("test2.txt", "w");
if (!pf)
{
perror("fopen");
return 1;
}

fputs("hello world\n", pf);
fputs("I LOVE C\n", pf);
// 关闭文件
fclose(pf);
pf = NULL;

连同换行符也成功写入


1
2
3
4
5
6
7
8
9
10
11
12
FILE* pf = fopen("test2.txt", "r");
if (!pf)
{
perror("fopen");
return 1;
}
char arr[20] = { 0 };
fgets(arr, 10, pf);
printf("%s\n", arr);
// 关闭文件
fclose(pf);
pf = NULL;


可以看到 fgets 读取的时候是会给 ‘\0’ 留一个位置

如果这一行不够 fgets 读取会如何呢

1
2
3
4
5
6
7
8
9
10
11
12
13
FILE* pf = fopen("test2.txt", "r");
if (!pf)
{
perror("fopen");
return 1;
}
char arr[20] = { 0 };
// 如果这一行长度小于 fgets 想要读取的长度
fgets(arr, 20, pf);
printf("%s\n", arr);
// 关闭文件
fclose(pf);
pf = NULL;

fgets 不会换行读取


fgets 读取到文件尾会返回 NULL, 那想要完整读取这个文件可以这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FILE* pf = fopen("test2.txt", "r");
if (!pf)
{
perror("fopen");
return 1;
}
char arr[20] = { 0 };
while (fgets(arr, 20, pf) != NULL)
{
// 不用担心长度的问题, fgets 会自行判断
printf("%s", arr);
}
// 关闭文件
fclose(pf);
pf = NULL;



fprintf / fscanf

fprintf 用于将数据按格式化形式打印到指定流

fscanf 用于从指定流上读入格式化数据

1
2
3
int fprintf ( FILE * stream, const char * format, ... );

int fscanf ( FILE * stream, const char * format, ... );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FILE* pf = fopen("test3.txt", "w");
if (!pf)
{
// 判空
perror("fopen");
return 1;
}

int a = 10;
int b = 20;
char* str = "法外狂徒张三";
// 将数据格式化输出到指定数据流
fprintf(pf, "%d\n%d\n%s\n", a, b, str);

// 关闭文件
fclose(pf);
pf = NULL;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FILE* pf = fopen("test3.txt", "r");
if (!pf)
{
// 判空
perror("fopen");
return 1;
}

int a = 0;
int b = 0;
// 这里的 str 指向的必须是一块可以修改的空间
// 且该空间要足够大
char str[20] = { 0 };
// 从指定数据流中格式化读取数据
fscanf(pf, "%d %d %s", &a, &b, str);
printf("%d\n%d\n%s", a, b,str);
// 关闭文件
fclose(pf);
pf = NULL;


fprintf 和 fscanf 都是对指定数据流进行操作,stdout 是标准输入输出流, 就是说

fprintf 和 fscanf 能完成 printf 和 scanf 的功能

1
2
3
4
5
6
7
//fscanf fprintf 都是对指定流进行操作
// stdin 是标准输入流
// stdout 是标准输出流
// fscanf fprintf 也可以完成 scanf 和 printf 的功能
int a = 0;
fscanf(stdin, "%d", &a);
fprintf(stdout, "%d\n", a);

sprintf / sscanf

1
2
3
int sprintf ( char * str, const char * format, ... );

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

sprintf 用于将格式化的数据写入字符串 str 中, sscanf 用于从字符串 s 中读取格式化的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct S
{
char name[20];
int age;
char sex[5];
};

struct S s = { "张三",18,"male" };
char str[50] = { 0 };
sprintf(str, "%s %d %s", s.name, s.age, s.sex);
printf("%s\n", str);

struct S t = { 0 };
sscanf(str, "%s %d %s", t.name, &(t.age), t.sex);
printf("%s %d %s", t.name, t.age, t.sex);

fwrite / fread

1
2
3
4
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );


size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );

fwrite 会将 ptr 指向的内存空间中的数据写入 count 个大小是 size 个字节到指定

数据流
fread 会从指定流中读取 count 个大小为 size 个字节的数据到 ptr 指向的

内存空间中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//size_t fwrite(const void* ptr, size_t size, size_t count, FILE * stream);
// fwrite 会将 ptr 指向的内存空间中的数据写入 count 个大小是 size 个字节到指定数据流
FILE* pf = fopen("test.txt", "w");
if (!pf)
{
// 判空
perror("fopen");
return 1;
}
int arr[] = { 1,2,3,4,5 };
int sz = sizeof(arr) / sizeof(arr[0]);
fwrite(arr, sizeof(int), sz, pf);// fwrite 以二进制方式写入

fclose(pf);
pf = NULL;

fread 读取成功时,会将成功读取的元素总个数返回,可以利用这个来完成数据流数

据的完整读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//size_t fread(void* ptr, size_t size, size_t count, FILE * stream);
// fread 会从指定流中读取 count 个大小为 size 个字节的数据到 ptr 指向的内存空间中
FILE* pf = fopen("test.txt", "rb");
if (!pf)
{
perror("fopen");
return 1;
}
int arr[5] = { 0 };
int i = 0;
while (fread(&arr[i], sizeof(int), 1, pf))
{
printf("%d ", arr[i]);
i++;
}
// 关闭文件
fclose(pf);
pf = NULL;

文件的随机读写

fseek

1
int fseek ( FILE * stream, long int offset, int origin );

将文件指针从 origin 的位置移动 offset 个数据的位置, 正数是向后移动,负数是向前移动

这是 <stdio.h> 这个头文件中定义的 origin 的三个参数

现将文件 “test1.txt” 中放图中内容

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
29
30
31
32
FILE* pf = fopen("test1.txt", "r");
if (!pf)
{
// 判空
perror("fopen");
return 1;
}
// int fseek ( FILE * stream, long int offset, int origin );
// 将文件指针从 origin 的位置移动 offset 个数据的位置, 正数是向后移动,负数是向前移动
// origin 参数的三个写法包含在头文件 stdio.h 中
// SEEK_SET 文件起始
// SEEK_CUR 当前文件指针位置
// SEEK_END 文件末尾
char c = fgetc(pf);
printf("%c\n", c); // a

fseek(pf, 5, SEEK_CUR); // 从当前指针位置向后移动5个数据
c = fgetc(pf); // g
printf("%c\n", c); // g

fseek(pf, 1, SEEK_SET); // 从文件开始位置向后移动1个数据
c = fgetc(pf); // b
printf("%c\n", c); // b


fseek(pf, -1, SEEK_END);// 将文件指针移动到文件末尾,偏移量为 -1
c = fgetc(pf); // g
printf("%c\n", c); // g

// 关闭文件
fclose(pf);
pf = NULL;

ftell

1
long int ftell ( FILE * stream )

返回当前文件指针相对于当前文件起始位置的偏移量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FILE* pf = fopen("test1.txt", "r");
if (!pf)
{
// 判空
perror("fopen");
return 1;
}

// long int ftell ( FILE * stream )
//返回当前文件指针相对于当前文件起始位置的偏移量

fseek(pf, 0, SEEK_END); // 设置文件指针到文件末尾处
printf("%d\n", ftell(pf));
// 关闭文件
fclose(pf);
pf = NULL;

rewind

1
void rewind ( FILE * stream );

设置文件指针到文件起始位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FILE* pf = fopen("test1.txt", "r");
if (!pf)
{
// 判空
perror("fopen");
return 1;
}

// void rewind ( FILE * stream )
// 设置文件指针到文件起始位置

fseek(pf, 0, SEEK_END); // 设置文件指针到文件末尾处
rewind(pf);
printf("%c\n", fgetc(pf)); // a
// 关闭文件
fclose(pf);
pf = NULL;

文件结束的判定

feof

1
int feof ( FILE * stream );

用于判断当文件读取结束时,是否是读到了文件末尾再结束

如果是,则返回非零值,否则返回 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FILE* pf = fopen("test1.txt", "r");
if (!pf)
{
// 判空
perror("fopen");
return 1;
}
while (fgetc(pf) != EOF)
{
;
}
if (!feof(pf))
{
// 判断是否是读到了文件尾
perror("fgetc");
return 1;
}
printf("%d\n", feof(pf));
// 关闭文件
fclose(pf);
pf = NULL;


需要注意的是,不能用 feof 直接来判断文件是否读取结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FILE* pf = fopen("test1.txt", "r");
if (!pf)
{
// 判空
perror("fopen");
return 1;
}
// 不能用 feof 来直接判断文件是否读取结束
// feof 具有滞后性
// 直到最后一次成功读取后,文件指针恰好指向末尾。下一次循环时,feof 还是 0(因为上次读取成功),所以再次进入循环体
while (!feof(pf))
{
char c = fgetc(pf);
printf("%c\n", c);
}
// 关闭文件
fclose(pf);
pf = NULL;

ferror

1
int ferror ( FILE * stream );

如果文件操作发生了错误,返回非零值

如果没有发生错误,返回 0

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
FILE* fp = fopen("test.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}

int n = 0;
while (fscanf(fp, "%d", &n) == 1)
{
// 处理正常读取的数据
}

// 循环结束,可能是 EOF 或 错误
if (feof(fp))
{
printf("正常到达文件末尾\n");
}
else if (ferror(fp))
{
printf("读取过程中发生错误\n");
perror("file error"); // 打印具体错误原因
}

fclose(fp);
fp = NULL;