文本文件操作
在 C 语言中创建或打开文件使用的函数是 fopen(),函数原型是:
c
FILE* fopen(char const* _FileName, char const* _Mode);
访问模式有以下六种,简单了解一下:
| 模式 | 描述 |
|---|---|
r |
只读方式打开一个已有的文本文件。 |
w |
写入方式打开一个文本文件。如果文件不存在,会创建一个新的;如果文件存在,页面的内容会被清空。 |
a |
与 w 不同的是,它会以追加的形式写入文件:如果文件已存在,会在之前的文件内容后追加内容。 |
r+ |
可读可写方式打开一个已有的文本文件。 |
w+ |
可读可写打开文本文件。文件不存在则会创建一个新文件;如果文件存在,会清空文件内容。 |
a+ |
与 w+ 不同的是,读取会从文件的开头开始,写入永远是追加到文件末尾。(执行写入操作时,指针会自动移动到文件的最末尾(越界位置),然后开始写入) |
注意: 这里所说的文件指的都是文本文件,二进制文件会在后面提到。
关闭文件使用的是 fclose(),函数原型是 int fclose( FILE *fp );。如果关闭成功返回的是 0,关闭失败返回的是 EOF 常量。
fclose() 的背后会执行这些步骤:
- 将缓冲区的数据强制写入磁盘(刷新缓冲区)。
- 释放用于该文件的内存与系统资源(如文件描述符)。
文本读写与指针移动示例
c
#include <stdio.h>
#include <string.h>
int main() {
const char* file_name = "./temp.txt";
const char* wr_open_mode = "w+";
FILE* file = fopen(file_name, wr_open_mode);
// 文件打开失败,退出
if (file == NULL) {
return -1;
}
// 写入文本
fputs("Hello World", file);
fputc('!', file);
fputc('\n', file);
// 移动文件位置指针到开头
// 否则后续的读取会读到文件末尾的空白
fseek(file, 0, SEEK_SET);
// 循环读取文件内容
char buffer[1024];
while (fgets(buffer, sizeof(buffer), file)) { // 类似的函数有 fgetc,用于读取单个字符
printf("%s", buffer);
}
// 关闭文件资源
fclose(file);
return 0;
}
注意: 上述的读取函数在遇到文件结尾符
EOF时都会停止。其中fgets在遇到换行符\n时也会停止, 并且它会在读取的字符串尾部补充\0。
其中我们用到了一个移动文件位置指针的函数 fseek(),来实现同一个文件指针能读取刚刚写入的内容,它的函数原型是 int fseek(FILE *stream, long offset, int whence),其中 offset 表示相对基准位置的偏移字节数,where 则是移动的基准点:
| 基准点 | 描述 |
|---|---|
SEEK_SET |
文件开头 |
SEEK_CURRENT |
文件当前的位置 |
SEEK_END |
文件末尾 |
格式化读写函数:fprintf 与 fscanf
类似读写文件的函数还有 fprintf、fscanf,专门用于写入和读取模板字符串,例如,保存或解析结构化的文本数据,可以这样:
c
int main() {
const char* file_name = "./temp.txt";
const char* wr_open_mode = "w+";
FILE* file = fopen(file_name, wr_open_mode);
if (file == NULL) {
return -1;
}
// 格式化写入
const char format_str_write[] = "Name: %s, Age: %d\n";
fprintf(file, format_str_write, "Alice", 20);
// 读写切换时,刷新缓冲区
fflush(file);
// 回到开头
fseek(file, 0, SEEK_SET);
// 格式化读取
const char format_str_read[] = "Name: %[^,], Age: %d\n";
char name[20];
int age = 0;
int ret = fscanf_s(
file,
format_str_read,
name,
(unsigned int)sizeof(name),
&age
);
if (ret == 2) { // 两个字段都读取成功
printf("Read from file -> Name: %s, Age: %d\n", name, age);
}
else if (ret == EOF) { // 读取完毕
printf("File read complete\n");
}
else { // 格式匹配失败
printf("File format doesn't match, failed to read\n");
}
fclose(file);
return 0;
}
注意: 读取时,%s 遇到空白符 才会终止,遇到逗号不会停。所以用 %s 会读到 "Alice,",后面模板的 , 对不上,导致匹配失败,所以要换成 %[^,],它遇到逗号就停。
二进制文件操作与文件大小获取
二进制文件的模式与文本文件的模式有点差异,模式需要替换为:"rb", "wb", "ab", "rb+"(等价 "r+b"), "wb+"(等价 "w+b"), "ab+"(等价 "a+b")。
读写二进制文件
处理二进制文件时,我们使用到的核心函数分别是 fread(读)、fwrite(写),以一个图片的复制为例:
c
int main() {
const char* file_name = "./avatar.jpg";
const char* copy_file_name = "./avatar_copy.jpg";
FILE* file_r = fopen(file_name, "rb");
FILE* file_w = fopen(copy_file_name, "wb");
// 检查打开状态
if (file_r == NULL || file_w == NULL) {
if (file_r != NULL) fclose(file_r);
if (file_w != NULL) fclose(file_w);
return -1;
}
char buffer[1024];
int buffer_len = sizeof(buffer) / sizeof(buffer[0]);
int len = 0;
// 采用块读写的方式,高效复制文件
while ((len = fread(buffer, sizeof(char), buffer_len, file_r)) != 0) {
fwrite(buffer, sizeof(char), len, file_w);
}
fclose(file_r);
fclose(file_w);
return 0;
}
注意: 在 C 语言标准库中没有直接叫字节(byte)的类型,但是我们可以使用标准 1 字节的
char或unsigned char来替换。
获取文件的总大小
获取文件的总大小,可以在上述的遍历代码上进行改造来实现:在不断读取文件内容的同时,累加每次读取到的有效字节数,像这样:
c
char buffer[1024];
long total_size = 0;
int len = 0;
while ((len = fread(buffer, sizeof(char), sizeof(buffer), file_r)) != 0) {
total_size += len; // 累加每次读取的字节数
fwrite(buffer, sizeof(char), len, file_w);
}
printf("The total size of the file is %ld bytes\n", total_size);
我们也可以通过 fseek + ftell 的组合获取。其中 ftell 可以获取文件位置指针当前相对于文件开头的偏移字节数。
注意:操作二进制文件时,模式一定要使用 b,否则在部分操作系统上指针的定位会受到换行符转换的影响,导致计算出错误的大小。
c
int main() {
const char* file_name = "avatar.jpg";
FILE* r_file = fopen(file_name, "rb"); // 这里一定要是 rb
if (r_file == NULL) {
return -1;
}
// 将指针移动到文件末尾
fseek(r_file, 0, SEEK_END);
// 获取当前指针位置,即文件总大小
long file_size = ftell(r_file);
printf("The total size of the file is %ld bytes\n", file_size);
fclose(r_file); // 关闭文件
return 0;
}
大文件的顺序切割
学完了上面的基础后,我们来实现一个进阶功能------文件的顺序切割:将一个大文件拆分为指定数量的子文件。
c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 获取文件大小的辅助函数
long get_file_size(const char* file_name) {
FILE* r_file = fopen(file_name, "rb"); // 使用二进制模式
if (r_file == NULL) {
return -1L;
}
fseek(r_file, 0, SEEK_END);
long file_size = ftell(r_file);
fclose(r_file);
return file_size;
}
const int SPLIT_NUMBER = 3; // 拆分份数
const int MAX_PATH_LEN = 256;
int main() {
const char* file_name = "avatar.jpg";
FILE* origin_file = fopen(file_name, "rb");
if (origin_file == NULL) {
return -1;
}
const char* file_name_placeholder = "avatar_%d.jpg";
// 计算拆分的每块大小
long file_size = get_file_size(file_name);
if (file_size < 0) {
fclose(origin_file);
return -1;
}
long split_size = file_size / SPLIT_NUMBER;
// 动态分配二维数组,用于存放每个子文件的文件路径
char** split_file_names = (char**)malloc(sizeof(char*) * SPLIT_NUMBER);
for (int i = 0; i < SPLIT_NUMBER; i++) {
split_file_names[i] = (char*)malloc(sizeof(char) * MAX_PATH_LEN);
snprintf(split_file_names[i], MAX_PATH_LEN, file_name_placeholder, i); // 生成如 avatar_0.jpg
}
// 写入每个子文件
for (int i = 0; i < SPLIT_NUMBER; i++) {
FILE* split_file = fopen(split_file_names[i], "wb");
if (split_file == NULL) {
continue;
}
// 计算当前子文件的字节区间
long start = i * split_size;
long end = start + split_size;
// 如果是最后一块,需要将文件剩余的所有字节都划分进来
if (i == SPLIT_NUMBER - 1) {
end = file_size;
}
// 准确定位原始文件的读取起点
fseek(origin_file, start, SEEK_SET);
// 块读写
char block_buffer[1024];
long bytes_to_write = end - start;
while (bytes_to_write > 0) {
long read_len = (bytes_to_write > (long)sizeof(block_buffer)) ? (long)sizeof(block_buffer) : bytes_to_write;
fread(block_buffer, sizeof(char), read_len, origin_file);
fwrite(block_buffer, sizeof(char), read_len, split_file);
bytes_to_write -= read_len;
}
fclose(split_file);
}
// 资源清理
fclose(origin_file);
for (int i = 0; i < SPLIT_NUMBER; i++) {
free(split_file_names[i]);
}
free(split_file_names);
return 0;
}
拆分出的第一张图片(后面两张因为识别不到文件格式,所以无法显示):
