(十二) 文件
1.流
(1)流模型
data sink:数据接收端、数据汇
优点:
①程序员读写文件时,不需要关心文件的位置
②数据源(data source) 和 数据汇(data sink) 是解耦的
(2)程序员视角的文件
存放的是一个一个字节。
EOF(end of file)指向文件末尾后一个位置,EOF是一个宏,值为-1
(3)缓冲区类型:满缓冲、行缓冲、无缓冲
①满缓冲 / 全缓冲 (Fully Buffered):数据积累到缓冲区满时才进行I/O操作,适用于大多数文件的读写操作。 [缓冲区空才从输入流读数据;缓冲区满向输出流中写入数据。]
②行缓冲 (Line Buffered):当读取或写入一行数据时触发I/O操作,常用于交互式设备,如终端。 [以行为单位进行读和写,如stdin、stdout]
③无缓冲 (Unbuffered):每次I/O操作都直接进行系统调用,不经过缓冲区,通常用于错误日志或重要数据的记录。 [没有缓冲区,立即输入输出,例如:标准错误流 stderr]
刷新输出缓冲区 (fflush),将输出缓冲区的内容输出到屏幕上
(4)标准流
①stdin:标准输入
②stdout:标准输出
③stderr:标准错误
这三个标准流,不需要程序员手动声明、创建、关闭
(5)二进制文件 与 文本文件
1.区别
(1)二进制文件:byte。(二进制文件以字节为单位,人类不可读,但体积小)
(2)文本文件:字符 + 编码。(文本文件以字符为单位,一个字符占几个字节)
2.优缺点:
文本文件:人类可读,数据量大
二进制文件:人类不可读,数据量小
(6)文件流的接口(API)
1.打开文件流 :fopen
2.读/写文件:
统计、转换、加密解密
2.5 移动文件位置
3.关闭文件流:fclose
2.打开/关闭文件
(1)fopen
1.函数原型
FILE* fopen(const char* filename, const char* mode);
(1)参数
①filename是文件的路径
②mode是打开模式
(2)返回值:
①打开文件成功,返回文件指针
②打开文件失败,返回NULL
(3)举例:
FILE* file = fopen("example.txt", "r"); //以只读模式打开文件
2.文件路径
(1)绝对路径
从根目录 (或者盘符) 开始,一直到文件所在的位置,比如:“c:/project/test.dat”。
(2)相对路径
另一种是相对路径:从当前工作目录开始,一直到文件所在的位置,比如:“in.dat”。
相对路径用的多,因为一个app各文件的相对路径一般不变,但是绝对路径,当安装到不同电脑上时一般不同。
3.打开模式(mode):文件的类型、对文件的操作(r,w)
(1)以文本文件方式打开
①“r”,读(read):要求文件存在。若不存在则返回NULL
②“w”,写(write):若文件存在,清空文件内容;若文件不存在,创建文件。
③“a”,追加(append):若文件存在,不修改原内容,每次都在文件末尾追加写入;若文件不存在,创建文件。
权限 | 文件存在 | 文件不存在 | |
---|---|---|---|
r | 只读 | 打开失败,返回NULL | |
w | 写 | 清空文件内容 | 创建文件 |
a | 追加 | 创建文件 | |
r+ | 读写 | 打开失败,返回NULL | |
w+ | 读写 | 清空文件内容 | 创建文件 |
a+ | 追加 | 创建文件 |
注意:如果以w+方式打开,写入后要读取写入的内容,需要先使用fseek将文件指针移动到要读取的位置。
(2)以二进制文件打开
(2)fclose
fclose 可以关闭程序不再使用的文件。
int fclose(FILE* stream);
如果成功关闭, fclose 返回零;否则返回 EOF
(3)示例代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void) {
//1.打开文件
FILE* stream = fopen("a.txt", "w");
if (stream == NULL) {
fprintf(stderr, "file open failed.\n");
exit(1);
}
//2.读写文件 (统计,转换,加密,解密)
//3.关闭文件
fclose(stream);
return 0;
}
3.读/写文件
(1)fgetc / fputc:读写文本文件,逐字符
fgetc 用于从文件中读取字符,返回读取的字符或 EOF。
fputc 用于向文件中写入字符,返回写入的字符或 EOF。
①fgetc
1.作用:fgetc 从指定的文件流中读取下一个字符
2.函数原型
#include <stdio.h>
int fgetc(FILE* stream);
(1)参数
stream 指向要从中读取字符的文件流指针
(2)返回值
①成功,返回读取的字符,转换为 unsigned char 后 再转换为 int,即对应的ASCII码值
②失败,返回 EOF
3.惯用法:一个字符一个字符地读取,直到文件末尾
FILE* src = fopen("src.txt", "r");
int c;
while((c = fgetc(src)) != EOF){
//操作
}
②fputc
1.作用:fputc 向指定的文件流中写入一个字符。适用于处理字符级别的文件操作。
2.函数原型
#include <stdio.h>
int fputc(int c, FILE* stream);
(1)参数
①c是要写入的字符
②stream指向要写入字符的文件流的指针
(2)返回值
①成功:返回要写入字符,转换为 unsigned char 后 再转换为 int
②失败:返回EOF
3.示例代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <ctype.h>
int main(void) {
FILE* src = fopen("src.txt", "r");
if (src == NULL) {
perror("fopen");
return -1;
}
FILE* dst = fopen("dst.txt", "w");
if (dst == NULL) {
perror("fopen");
fclose(src);
return -1;
}
int c;
while ((c = fgetc(src)) != EOF) {
//fputc(c, dst); //实现文件复制,一个字符一个字符的复制
//fputc(tolower(c), dst); //全文替换为小写
fputc(toupper(c), dst); //全文替换为大写
}
fclose(src);
fclose(dst);
return 0;
}
(2)fgets / fputs:读写文本文件,逐行
fgetc的c是character,fgets的s是string
①fgets
1.功能:
fgets 从指定的文件流中读取一行,并存储在字符串中。
读取会下列三种情况下停止:
①读取到换行符 \n
②读取到文件末尾EOF
③读取到指定的最大字符数 n-1
并且fgets会在字符串末尾自动添加一个空字符 \0。
2.函数原型
#include <stdio.h>
char* fgets(char* str, int n, FILE* stream);
(1)参数:
①str:指向一个字符数组,用于存储读取的字符串
②n:要读取的最大字符数,包括空字符 \0
(通常是str指向字符数组的长度)
③stream:指向要从中读取数据的文件流的指针
(2)返回值:
①成功,返回指向存储字符串的指针 str
②失败或到达文件末尾时,返回 NULL
3.惯用法:
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
while(fgets(sendline, MAXLINE, stdin) != NULL){ //一行一行地读取输入
}
4.fgets的特点
①fgets会读\n
②gets(str) 等价于 fgets(str, ∞, stdin) ,即gets不会检查数组越界
②fputs
1.作用:
fputs 将一个字符串写入到指定的文件流中,不会在字符串末尾添加换行符。
适用于对整行数据进行处理。
2.函数原型
int fputs(const char* str, FILE* stream);
(1)参数:
①str:指向要写入的字符串 (以’\0’结尾的字符串)
②stream:指向要写入数据的文件流的指针
(2)返回值:
①成功,返回一个非负值
②失败,返回EOF,并设置errno
3.fputs的特点
①fputs原样输出字符串,puts在字符串后多输出一个换行符’\n’
②puts(str) 等价于 fputs(str, stdout)
4.示例代码
(1)例1:实现文件的复制
①核心代码:
fgets不会清空buffer,但fgets会在字符串末尾自动添加一个 \0。
char buffer[100];
while (fgets(buffer, sizeof(buffer), src) != NULL) {
//printf("%s", buffer); //输出文件的内容
fputs(buffer, dst); //逐行读取,实现文件的原样复制
}
②完整代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <ctype.h>
int main(void) {
FILE* src = fopen("src.txt", "r");
if (src == NULL) {
perror("fopen");
return -1;
}
FILE* dst = fopen("dst.txt", "w");
if (dst == NULL) {
perror("fopen");
fclose(src);
return -1;
}
char buffer[100];
while (fgets(buffer, sizeof(buffer), src) != NULL) {
//printf("%s", buffer); //输出文件的内容
fputs(buffer, dst); //逐行读取,实现文件的原样复制
}
fclose(src);
fclose(dst);
return 0;
}
(2)示例2:实现文件的复制,并在每行的行首添加行号
①核心代码:
char buffer[100];
int line_num = 1;
char line[100];
while (fgets(buffer, sizeof(buffer), src) != NULL) {
//printf("%s", buffer); //输出文件的内容
sprintf(line, "%d %s", line_num, buffer);
fputs(line, dst);
line_num++;
}
②完整代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void) {
FILE* src = fopen("src.txt", "r");
if (src == NULL) {
perror("fopen");
return -1;
}
FILE* dst = fopen("dst.txt", "w");
if (dst == NULL) {
perror("fopen");
fclose(src);
return -1;
}
char buffer[100];
int line_num = 1;
char line[100];
while (fgets(buffer, sizeof(buffer), src) != NULL) {
//printf("%s", buffer); //输出文件的内容
sprintf(line, "%d %s", line_num, buffer);
fputs(line, dst);
line_num++;
}
fclose(src);
fclose(dst);
return 0;
}
(3)fscanf / fprintf:格式化地读写
①fscanf
1.作用
fscanf函数用于从文件中读取格式化的数据。它的功能类似于scanf,但是输入源是文件而不是标准输入stdin(通常是键盘)
2.函数原型
int fscanf(FILE* stream, const char* format, ...);
(1)参数
①FILE *stream: 文件指针,指向要读取的文件。
②const char *format: 格式控制字符串,定义了如何解析文件中的数据。
③…: 可变参数列表,指向将存储读取数据的变量。
(2)返回值
①成功,读取并格式化的项数
②读取错误或达到文件末尾,返回EOF
②fprintf
1.作用
fprintf函数用于将格式化的数据写入文件。它的功能类似于printf,但是输出目标是文件而不是标准输出stdout(通常是屏幕)
适用于生成结构化的日志文件、配置文件、数据文件等。
2.函数原型
int fprintf(FILE* stream, const char* format, ...);
(1)参数
①FILE *stream:文件指针,指向要写入的文件。
②const char *format: 格式控制字符串,定义了如何格式化后续的参数。
③… :可变参数列表,要格式化并写入文件的数据。
(2)返回值
①成功时,返回写入文件的字符数
②失败时,返回负值
3.示例代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <ctype.h>
#define MAXLINE 128
typedef struct Student {
int id;
char name[25];
char gender;
int chinese;
int math;
int english;
} Student;
int main(int argc, char* argv[]) {
//xxx.exe src dst
//0.参数校验
if (argc != 3) {
fprintf(stderr, "Usage: %s src dst\n", argv[0]);
exit(1);
}
//1.打开文件
FILE* src = fopen(argv[1], "r");
if (!src) {
fprintf(stderr, "Open %s failed.\n", argv[1]);
exit(1);
}
FILE* dst = fopen(argv[2], "w");
if (!dst) {
fprintf(stderr, "Open %s failed.\n", argv[2]);
fclose(src);
exit(1);
}
//2.读写文件 (统计,转换,加密,解密)
//(3)格式化地读写
Student students[5]; //定义 学生结构体数组
for (int i = 0; i < 5; i++) {
fscanf(src, "%d%s %c%d%d%d",
&students[i].id,
students[i].name,
&students[i].gender,
&students[i].chinese,
&students[i].math,
&students[i].english);
}
//修改成绩
for (int i = 0; i < 5; i++) {
students[i].chinese *= 10;
students[i].math *= 10;
students[i].english *= 10;
}
for (int i = 0; i < 5; i++) {
fprintf(dst, "%d %s %c %d %d %d\n",
students[i].id,
students[i].name,
students[i].gender,
students[i].chinese,
students[i].math,
students[i].english);
}
printf("成绩已修改。\n");
//3.关闭文件
fclose(src);
fclose(dst);
return 0;
}
//a.txt
1 Edward M 100 100 100
2 Amber F 99 99 99
3 Sam M 98 98 98
4 Windy F 97 97 97
5 Chole F 96 96 96
(4)fread / fwrite:读写二进制文件,逐块
①fread
1.函数原型
#include <stdio.h>
size_t fread(void* buffer, size_t size, size_t count, FILE* stream);
(1)参数:
①buffer:指向存放数据的数组
②size:每个元素的大小,以字节为单位
③count:最多可以读取的元素个数
④stream:文件指针
(2)返回值:
①返回成功读取的元素数量
②若返回值小于count,则读到文件末尾或发送错误。 [可通过feof 和 ferror 函数判断是哪种情况]
(3)举例:
fread(png_file_buff, 1, filesize, png_file);
②fwrite
1.函数原型
#include <stdio.h>
size_t fwrite(const void *buffer, size_t size, size_t count, FILE *stream);
(1)参数
①buffer:指向存放数据的数组
②size:每个元素的大小,以字节为单位
③count:要写入元素的个数
④stream:文件指针
(2)返回值
返回成功写入的元素的个数。若返回值<count,则可能发生了写入错误。
(3)举例
fwrite(buffer, 1, BUFSIZE, dst);
③代码
1.核心代码:
//读写二进制文件 (复制)
char buffer[BUFSIZE];
int bytes;
while ((bytes = fread(buffer, 1, BUFSIZE, src)) > 0) {
fwrite(buffer, 1, BUFSIZE, dst);
}
2.完整代码:复制二进制文件
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define BUFSIZE 4096
int main(int argc, char* argv[]) {
//xxx.exe src
//0.参数校验
if (argc != 3) {
fprintf(stderr, "Usage: %s src dst\n", argv[0]);
exit(1);
}
//1.打开文件
FILE* src = fopen(argv[1], "rb");
if (!src) {
fprintf(stderr, "Open %s failed.\n", argv[1]);
exit(1);
}
FILE* dst = fopen(argv[2], "wb");
if (!dst) {
fprintf(stderr, "Open %s failed.\n", argv[2]);
fclose(src);
exit(1);
}
//2.读写文件
//(4)读写二进制文件 (复制)
char buffer[BUFSIZE];
int bytes;
while ((bytes = fread(buffer, 1, BUFSIZE, src)) > 0) {
fwrite(buffer, 1, BUFSIZE, dst);
}
//3.关闭文件
fclose(src);
fclose(dst);
return 0;
}
4.文件定位、移动文件位置
(1)fseek:移动文件位置
int fseek(FILE* stream, long int offset, int whence);
whence的三个宏:
①SEEK_SET
:文件的起始位置,0
②SEEK_CUR
:文件的当前位置,pos
③SEEK_END
:文件的末尾位置,EOF
(2)ftell:返回当前文件位置
long int ftell(FILE* stream);
(3)rewind:将文件位置移回开头
void rewind(FILE* fp);
rewind
会将文件位置设置为起始位置,等价于调用:
举例:
//获取文件大小:获取图片长度
fseek(png_file, 0, SEEK_END);
long filesize = ftell(png_file);
rewind(png_file);
fseek(fp, 0, SEEK_SET);
(4)代码实战
难点:
①如何确定文件的大小
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
char* readFile(const char* path) {
//1.打开文件
FILE* stream = fopen(path, "rb");
if (stream == NULL) { //!stream
fprintf(stderr, "Open %s failed\n", path);
exit(1);
}
//2.确定文件大小
fseek(stream, 0, SEEK_END);
long n = ftell(stream);
char* content = malloc((n + 1) * sizeof(char)); // 1 for '\0'
//3.读取文件
rewind(stream); //回到文件开头
int bytes = fread(content, 1, n, stream);
content[bytes] = '\0';
//4.关闭文件
fclose(stream);
return content;
}
int main(int argc, char* argv[]) {
//0.参数校验
if (argc != 2) {
fprintf(stderr, "Usage: %s filename\n", argv[0]);
exit(1);
}
char* content = readFile(argv[1]);
//操作
printf("%s\n", content);
free(content);
return 0;
}
5.两种文件指针:FILE* 与 fd 的区别
1.FILE* 文件流指针
FILE* 是一个文件流指针,定义在标准 I/O 库 <stdio.h> 中。它提供了高级别的文件操作函数,如 fopen、fclose、fread、fwrite、fprintf、fscanf 等。这些函数通常更易于使用,并且提供了缓冲机制,多次调用直至缓冲区满了才进行一次系统调用,提高了 I/O 操作的效率。
2.fd 文件描述符
fd 是文件描述符(file descriptor),通常是一个整数,定义在 POSIX 标准中。文件描述符是一个低级别的文件指针,定义在 <unistd.h> 中。文件描述符的操作函数包括 open、close、read、write、lseek 等。这些函数更接近系统调用,提供了更细粒度的控制,但没有缓冲机制,每次调用会直接进行系统调用。
3.区别
(1)缓冲机制:
①FILE* 默认是全缓冲,系统自动管理缓冲区,多次调用直至缓冲区满才进行一次系统调用,减少了系统调用的次数,提高了I/O效率。[使用fopen打开文件时,标准库会自动为文件流动态分配一个缓冲区,称为 FILE *的自动缓冲]
②fd 是无缓冲,会直接进行系统调用,需要用户手动管理缓冲区。fd频繁的系统调用,导致效率较低。
(2)级别不同:
①FILE* 是高级文件指针,带有缓冲机制,适用于一般文件读写操作;
②fd 是低级文件描述符,适用于需要精细控制的场景,例如设备文件、管道、套接字等。
(3)跨平台性:
①FILE* 更加跨平台,其内部实现适配各种操作系统
②fd 是 POSIX 标准,主要用于 UNIX 和类 UNIX 系统。
(4)灵活性/复杂性:
①FILE* 封装了复杂的缓冲操作,使得编程更加简洁。
②fd 提供了更灵活的文件操作,但代码相对复杂,需要手动管理缓冲区;
4.使用场景
①FILE*:适用于大多数文件的读写操作,如文本文件读写、日志记录等。提供更高的抽象层次和缓冲机制,简化编程并提高效率。
②fd:适用于需要精细控制的场景,如多线程环境下的文件操作、系统编程、网络编程、设备驱动开发等。
6.错误处理
1.如何检测错误:①②
2.如何打印错误信息:③④⑤
(1)返回值
用int ret 接收函数的返回值,判断是否执行成功
(2)errno
全局变量errno,定义在 <errno.h>
头文件中 (每个线程都有自己的errno,线程特定变量)
errno为0表示没有错误,非0表示有错误
#include <errno.h>
FILE* fp = fopen("not_exist.txt", "r");
printf("%d\n", errno);
(3)strerror (errno)
strerror将errno从数字转化为对应的字符串错误,定义在 <string.h>
头文件中
#include <error.h>
#include <string.h>
FILE* fp = fopen("not_exist.txt", "r");
printf("%s\n", strerror(errno)); //将errno对应的错误信息,转化为人类可读的字符串
#define ARGS_CHECK(argc, num){ \
if(argc != num){ \
fprintf(stderr, "ARGS ERROR!\n");\
return -1; \
} \
} \
(4)perror (“前缀信息”)
1.函数原型
#include <stdio.h>
void perror(const char *s);
2.功能
打印: s、冒号、空格、换行符。
perror("前缀信息")
,错误的前缀信息,后面自动添加了 :
和\n
相当于调用了
fprintf(stderr, "%s: %s\n", "prefix", strerror(errno));
3.举例
#include <error.h>
#include <string.h>
FILE* fp = fopen("not_exist.txt", "r");
fprintf(stderr, "%s: %s\n", "prefix", strerror(errno));
perror("prefix"); //添加错误的前缀信息
exit(1);
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(void) {
//errno==0 代表 没有错误
printf("errno = %d\n", errno);
FILE* fp = fopen("a.txt","r+"); //实际a.txt不存在
//errno
printf("errno = %d\n", errno);
//strerror(errno)
printf("strerror(errno) = %s\n",strerror(errno));
//perror
fprintf(stderr, "%s: %s\n", "prefix", strerror(errno));
perror("prefix");
return 0;
}
(5)error()
仅linux可用,自动添加‘\n’
#include <error.h>
#include <errno.h>
error(0, error, "prefix"); //不退出程序,设置errno
error(1, 0, "prefix"); //相当于exit(1),不设置errno
error(1, errno, "prefix"); //相当于exit(1),设置errno
输出:
./可执行程序名: prefix