文件操作
# 文件操作
# C 文件概述
文件是指存储在外部介质(如磁盘、磁带)上的数据集合。操作系统(Windows、Linux、Mac 等)是以文件为单位对数据进行管理的。如下图所示。

文件的分类如下:
- 从用户的角度来看,可分为特殊文件(标准输入 / 输出文件或标准设备文件)和普通文件(磁盘文件)。
- 从操作系统的角度来看,每个与主机相连的输入 / 输出设备都可视为一个文件。 例如,终端键盘可视为输入文件,显示屏和打印机可视为输出文件。
- 按照数据的组织形式,可分为 ASCII 文件和二进制文件。在 ASCII 文件(文本文件)中,每个字节放一个 ASCII 码。二进制文件把内存中的数据按其在内存中的存储形式原样输出到磁盘上存放。
例如,整数 10000 在内存中的存储形式及分别按 ASCII 码形式和二进制形式输出的。如下图所示。

# 文件的打开、读写、关闭
# 文件指针介绍
打开一个文件后,我们会得到一个 FILE * 类型的文件指针,然后通过该文件指针对文件进行操作。FILE 是一个结构体类型,其具体内容如下所示:
struct _iobuf {
char *_ptr; //下一个要被读取的字符地址
int _cnt; //剩余的字符,若是输入缓冲区,则表示缓冲区中还有多少个字符未被读取
char *_base; //缓冲区基地址
int _flag; //读写状态标志位
int _file; //文件描述符
int _charbuf;
int _bufsiz; //缓冲区大小
char *_tmpfname;
};
typedef struct _iobuf FILE;
FILE *fp;
2
3
4
5
6
7
8
9
10
11
12
fp 是一个指向 FILE 类型结构体的指针变量。可以使 fp 指向某个文件的结构体变量,从而通过该结构体变量中的文件信息来访问该文件。 Windows 操作系统下的 FILE 结构体与 Linux 操作系统下的 FILE 结构体中的变量名是不一致 的,但是其原理可以互相参考。
# 文件的打开与关闭
fopen 函数用于打开由 fname(文件名)指定的文件,并返回一个关联该文件的流。如果发生错误,那么 fopen 返回 NULL。mode(方式)用于决定文件的用途(如输入、输出等),具体形式如下所示:
FILE *fopen(const char *fname, const char *mode);
常用的 mode 参数及其各自的意义如下所示。
| mode(方式) | 意义 |
|---|---|
| r | 打开一个用于读取的文本文件 |
| w | 创建一个用于写入的文本文件 |
| a | 附加到一个文本文件 |
| rb | 打开一个用于读取的二进制文件 |
| wb | 创建一个用于写入的二进制文件 |
| ab | 附加到一个二进制文件 |
| r+ | 打开一个用于读 / 写的文本文件 |
| w+ | 创建一个用于读 / 写的文本文件 |
| a+ | 以附加方式打开一个用于读 / 写的文本文件 |
| rb+ | 打开一个用于读 / 写的二进制文件 |
| wb+ | 创建一个用于读 / 写的二进制文件 |
| ab+ | 打开一个用于读 / 写的二进制文件 |
fclose 函数用于关闭给出的文件流,并释放已关联到流的所有缓冲区。fclose 执行成功时返回 0,否则返回 EOF。具体形式如下所示:
int fclose(FILE *stream);
fputc 函数用于将字符 ch 的值输出到 fp 指向的文件中,如果输出成功,那么返回输出的字符;如果输出失败,那么返回 EOF。具体形式如下所示:
int fputc(int ch, FILE *stream);
fgetc 函数用于从指定的文件中读入一个字符,该文件必须是以读或读写方式打开的。如果读取一个字符成功,那么赋给 ch。如果遇到文件结束符,那么返回文件结束标志 EOF。具体形式 如下所示:
int fgetc(FILE *stream);
文件打开、读写、关闭案例
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[]) {
FILE* fp; //文件类型指针
int i;
char c;
// 循环输出main函数的传参字符数组 argv[0]是编译后exe的存储路径
// printf("argc=%d\n", argc);
// for (i = 0; i < argc; i++) {
// puts(argv[i]);
// }
fp = fopen(argv[1], "r+"); //打开由运行参数传递过来的文件路径
if (NULL == fp) {
perror("fopen");
goto error;
}
// while ((c = fgetc(fp)) != EOF) { // 循环读取文件内容
// putchar(c);
// }
i = fputc('H', fp); //将H字符写入到文件中
if (EOF == i) {
perror("fputc");
}
fclose(fp); //关闭文件
error:
system("pause");
}
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
# fread 函数、fwrite 函数与 fseek 函数
fread 函数与 fwrite 函数的具体形式如下:
int fread(void *buffer, size_t size, size_t num, FILE *stream);
int fwrite(const void *buffer, size_t size, size_t count, FILE *stream);
2
- buffer 是一个指针,对 fread 来说它是读入数据的存放地址,对 fwrite 来说它是输出数 据的地址(均指起始地址)
- size 是要读写的字节数
- count 是要进行读写多少 size 字节的数据项
- fp 是文件型指针
- fread 函数的返回值是读取的内容数量,fwrite 写成功后的返回值是已写对象的数量
fseek 函数的功能是改变文件的位置指针,其具体调用形式如下:
int fseek(FILE *stream, long offset, int origin);
其中 fseek 的说明如下:
fseek(文件类型指针,位移量,起始点)
- FILE 为文件类型指针,也就是被移动的文件指针
- offset 为偏移量,也就是要移动的字节数。之所以为 long 类型,是希望移动的范围更大,能处理的文件更大。offset 为正时,向后移动;offset 为负时,向前移动
- origin 为起始位置,也就是从何处开始计算偏移量。C 语言规定的起始位置有三种,分别为文件开头、当前位置和文件末尾,每个位置都用对应的常量来表示
起始点的说明如下:
| 描述 | 常量名 | 常量值 |
|---|---|---|
| 文件开头 | SEEK_SET | 0 |
| 文件当前位置 | SEEK_CUR | 1 |
| 文件末尾 | SEEK_END | 2 |
位移量是指以起始点为基点,向前移动的字节数。一般要求为 long 型。
fseek 函数调用成功时返回零,调用失败时返回非零。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char** argv) {
char buf[20] = "hello\nworld";
FILE* fp;
int i = 12345;
int ret;
if (argc != 2) {
printf("error args\n");
return -1;
system("pause");
}
fp = fopen(argv[1], "r+");
if (NULL == fp) {
perror("fopen");
return -1;
}
//向文件中写入整型数
//ret = fwrite(&i, sizeof(int), 1, fp);
//i = 0;
//ret = fread(&i, sizeof(int), 1, fp);
ret = fwrite(buf, sizeof(char), strlen(buf), fp); //把 buf 中的字符串写入文件
memset(buf, 0, sizeof(buf)); //清空 buf
ret = fseek(fp, -12, SEEK_CUR); //由于上面写入文件 光标/文件指针位置已经发生改变 需要往前偏移 12 字节 再进行读取 否则读取的是当前文件指针位置后的文本
ret = fread(buf, sizeof(char), sizeof(buf) - 1, fp);
puts(buf);//打印 buf 的内容
fclose(fp);
system("pause");
}
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
fread 和 fwrite 函数既可以以文本方式对文件进行读写,又可以以二进制方式对文件进行读写。以 "r+" 即文本方式打开文件进行读写时,向文件内写入的是字符串,写入完毕后会将 buf 清空,这时文件位置指针指向 12 字节的位置,如果要从文件头读取,那么就必须通过 fseek 函 数偏移到文件头。向前偏移 12 字节,接着通过 fread 函数读取文件, 读取内容后进行打印。
为什么写入的是 "hello\nworld" 共 11 字节,而偏移到文件开头却需要 偏移 12 字节呢?
这是因为在 Windows 默认文本方式下,向文本文件中写入 "\n" 时实际存入磁盘的是 "\r\n", 所有的接口调用都是 Windows 的系统调用,这是 Windows 的底层实现所决定的。当然,以文本 方式写入,以文本方式读出,遇到 "\r\n" 时底层接口会自动转换为 "\n",因此用 fread 函数再次读取数据时,得到的依然是 "hello\nworld",共 11 字节。
如果把 fopen 函数中的 "r+" 改为 "rb+",也就是改为二进制方式,那么当我们向磁盘写入 11 字节时,磁盘实际存储的就是 11 字节,这时向前偏移时, fseek(fp,-12,SEEK_CUR); 需要修改为 fseek(fp,-11,SEEK_CUR); ,因为实际磁盘存储只有 11 字节。在二进制方式下,文件大小是 11 字节,如果这时双击以 Windows 默认的记事本打开该文件,那么会发现没有换行,即 helloworld 是连在一 起的,中间没有换行符,原因是记事本文本编辑器必须遇到 "\r\n" 时才进行换行操作。
如果是以文本方式写入的内容,那 么一定要以文本方式读取;如果是以二进制方式写入的内容,那么一定要以二进制方式读取,不能混用!
# fgets 函数与 fputs 函数
函数 fgets 从给出的文件流中读取 [num-1] 个字符,并且把它们转储到 str(字符串)中。fgets 在到达行末时停止,fgets 成功时返回 str(字符串),失败时返回 NULL,读到文件结尾时 返回 NULL。其具体形式如下:
char *fgets(char *str, int num, FILE *stream);
fputs 函数把 str(字符串)指向的字符写到给出的输出流。成功时返回非负值,失败时返回 EOF。其具体形式如下:
int fputs(const char *str, FILE *stream);
下面案例将指定文件的每行首字符进行小写转大写
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char** argv) {
FILE* fp;
char buf[1024];
if (argc != 2) {
printf("error args\n");
return -1;
system("pause");
}
fp = fopen(argv[1], "r+");
if (NULL == fp) {
perror("fopen");
return -1;
}
while (fgets(buf, sizeof(buf), fp) != NULL) {
buf[0] -= 32; //首字母小写转大写
if (buf[strlen(buf) - 1] == '\n') { //字符串末尾包含\n,就要往前多偏移一个字符
fseek(fp, -strlen(buf) - 1, SEEK_CUR);
} else { //末尾不包含\n直接偏移
fseek(fp, -strlen(buf), SEEK_CUR);
}
fputs(buf, fp);// 将字符串写入到文件中
fseek(fp, 0, SEEK_CUR); //刷新当前文件位置指针 不能省略否则会出现无限写入文件
}
fclose(fp);
system("pause");
}
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
用于 fgets 函数的 buf 不能过小,否则可能无法读取 "\n",导致行数统计出错。fputs 函数向文件中写一个字符串,不会额外写入一个 "\n"。
# ftell 函数
ftell 函数返回 stream(流)当前的文件位置,发生错误时返回 - 1。当我们想知道位置指针距离文件开头的位置时,就需要用到 ftell 函数,其具体形式如下所示:
long ftell(FILE *stream);
ftell 与 fseek 的使用。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
FILE *fp;
char str[20] = "hello\nworld";
int val = 0;
long pos;
int ret;
fp = fopen("file.txt", "r+");
if (NULL == fp) {
perror("fopen");
goto error;
}
val = strlen(str);
ret = fwrite(str, sizeof(char), val, fp);
ret = fseek(fp, -5, SEEK_CUR);
if (ret != 0) {
perror("fseek");
goto error;
}
pos = ftell(fp); //获取位置指针距离文件开头的位置
printf("Now pos=%ld\n", pos);
memset(str, 0, sizeof(str));
ret = fread(str, sizeof(char), sizeof(str), fp);
printf("%s\n", str);
fclose(fp);
error:
system("pause");
}
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
我们向文件中写入了 "hello\nworld",因为是文本方式,所以总计为 12 字节,通过 fseek 函数向前偏移 5 字节后,用 ftell 函数得到的位置指针距离文件开头的位置即为 7,这时再用 fread 函数读取文件内容,得到的是 "worl"。
# fprintf 函数与 fscanf 函数
fprintf 函数根据指定的 format(格式)发送信息(参数)到由 stream(流)指定的文件。
fprintf 只能和 printf 一样工作(除了第一个形参,fprintf 的其他参数与 printf 的一样)。
printf 将不同类型的数据以字符串的形式输出到屏幕上,fprintf 将这些数据写入对应的文件, fprintf 的返回值是输出的字符数,发生错误时返回一个负值,其具体形式如下:
int fprintf(FILE *stream, const char *format, ...);
scanf 函数将屏幕上输入的字符串数据依次格式化到各个变量中,函数 fscanf 以 scanf 函数的执行方式从给出的文件流中读取数据。fscanf 的返回值是事实上已赋值的变量的数量,与 scanf 等价,未进行任何分配时返回 EOF,其具体形式如下:
int fscanf(FILE *stream, const char *format, ...);
fprintf 与 fscanf 的使用。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int num;
float score;
char name[20];
} Student_t;
int main(int argc, char** argv) {
Student_t s = {1001, 98.5, "zhangsan"};
FILE* fp;
if (argc != 2) {
printf("error args\n");
return -1;
system("pause");
}
fp = fopen(argv[1], "r+");
if (NULL == fp) {
perror("fopen");
return -1;
}
fprintf(fp, "%d %5.2f %s\n", s.num, s.score, s.name); //以打印方式写入到文件当中
fseek(fp,0,SEEK_SET); //位置指针重新定位到文件开头
// 将控制台输入的字符串 以打印方式写入到文件当中
while(memset(&s,0,sizeof(Student_t)),fscanf(fp,"%d%f%s",&s.num,&s.score,s.name)!=EOF){
fprintf(fp,"%d %5.2f %s\n",s.num,s.score,s.name);
}
fclose(fp);
system("pause");
}
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
33
用 fprintf 函数和 fscanf 函数对磁盘文件进行读写虽然使用方便且容易理解,但由于在读取时要将 ASCII 码转换为二进制形式,写入文件时又要将二进制形式转换为字符,所以花 费的时间较多。因此,在内存与磁盘频繁交换数据的情况下,最好不用 fprintf 函数和 fscanf 函数,而用 fread 函数和 fwrite 函数。