【C】fseek、fread、ftell
·

文章目录
在C语言中,fseek 和 fread 是标准I/O库(<stdio.h>)中用于文件操作的核心函数,而 ftell(而非 ftail)是另一个常用函数,用于获取当前文件指针的位置。以下是它们的详细说明、用法示例及在深度学习部署中的潜在应用场景。
1. fseek:文件指针定位(“翻到某一页”)
就像:直接翻到第 200 页,而不是一页一页翻
函数原型
int fseek(FILE *stream, long offset, int whence);
- 功能:移动文件指针到指定位置。
- 参数:
stream:文件指针(通过fopen打开)。offset:偏移量(字节单位,可为正/负)。whence:基准位置(从哪里开始),可选:SEEK_SET:文件开头。SEEK_CUR:当前位置。SEEK_END:文件末尾。
- 返回值:成功返回
0,失败返回非零(如越界或文件未打开)。
eg:模型很大,只加载第 3 层 → 直接跳过去
示例代码
#include <stdio.h>
int main() {
FILE *file = fopen("model.bin", "rb");
if (!file) {
perror("Failed to open file");
return 1;
}
// 将文件指针移动到距开头100字节的位置
if (fseek(file, 100, SEEK_SET) != 0) {
perror("fseek failed");
fclose(file);
return 1;
}
// 从当前位置读取数据...
fclose(file);
return 0;
}
关键点
- 二进制模式:深度学习模型文件(如
.bin、.tflite)通常需以"rb"模式打开,避免换行符转换。 - 错误处理:务必检查返回值,避免后续操作因指针位置无效而失败。
- 大文件支持:在32位系统中,
long类型可能无法处理超过2GB的文件,此时可用fseeko和off_t(POSIX标准)。
2. fread:读取二进制数据(“读内容”)
把文件里的数据读到内存里
比如:把模型参数读出来
在深度学习里的作用:
- 读取模型权重(weights)
- 读取结构参数
- 加载推理所需的数据
函数原型
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
- 功能:从文件读取数据到内存缓冲区。
- 参数:
ptr:目标内存缓冲区指针。size:每个元素的大小(字节)。nmemb:要读取的元素数量。stream:文件指针。
- 返回值:成功读取的元素数量(非字节数),若小于
nmemb,可能是文件结束或错误(需用feof或ferror判断)。
示例代码
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *file = fopen("weights.bin", "rb");
if (!file) {
perror("Failed to open file");
return 1;
}
// 读取100个float类型数据(400字节)
float *buffer = (float *)malloc(100 * sizeof(float));
size_t read_count = fread(buffer, sizeof(float), 100, file);
if (read_count != 100 && !feof(file)) {
perror("fread failed");
free(buffer);
fclose(file);
return 1;
}
printf("Read %zu floats\n", read_count);
free(buffer);
fclose(file);
return 0;
}
关键点
- 返回值:返回的是元素数量(如成功读取5个
float则返回5),而非字节数。 - 内存分配:确保目标缓冲区足够大,避免溢出。
- 性能优化:在嵌入式场景中,频繁小数据读取可能影响性能,建议一次性读取大块数据(如整个模型参数)。
3. ftell:获取当前文件指针位置(“现在在第几页?”)
获取当前读到哪里,用于计算数据位置,配合 fseek 做精确读取
函数原型
long ftell(FILE *stream);
- 功能:返回当前文件指针的位置(相对于文件开头的偏移量,字节单位)。
- 参数:
stream:文件指针。
- 返回值:成功返回当前位置(
long类型),失败返回-1L。
示例代码
#include <stdio.h>
int main() {
FILE *file = fopen("data.bin", "rb");
if (!file) {
perror("Failed to open file");
return 1;
}
// 移动指针并获取位置
fseek(file, 100, SEEK_SET);
long pos = ftell(file);
printf("Current position: %ld\n", pos); // 输出: 100
fclose(file);
return 0;
}
关键点
- 用途:常用于记录文件读取位置,便于后续恢复或调试。
- 限制:在32位系统中,
long可能无法处理大文件(>2GB),此时可用ftello(POSIX标准)。
4. 深度学习部署中的典型应用
三者配合
fseek(fp, offset, SEEK_SET); // 跳到某一层的位置
fread(buffer, sizeof(float), N, fp); // 读这一层参数
pos = ftell(fp); // 记录当前位置
场景1:加载预训练模型权重
void load_model_weights(const char *filename, float *weights, int weight_count) {
FILE *file = fopen(filename, "rb");
if (!file) {
fprintf(stderr, "Error opening model file\n");
exit(1);
}
size_t read = fread(weights, sizeof(float), weight_count, file);
if (read != weight_count) {
fprintf(stderr, "Error reading weights (expected %d, got %zu)\n", weight_count, read);
fclose(file);
exit(1);
}
fclose(file);
}
场景2:跳过文件头读取特定数据
typedef struct {
int version;
int layer_count;
// ...其他元数据
} ModelHeader;
void load_model_with_header(const char *filename) {
FILE *file = fopen(filename, "rb");
if (!file) { /* 错误处理 */ }
ModelHeader header;
fread(&header, sizeof(ModelHeader), 1, file); // 读取文件头
// 记录当前位置(可选)
long header_end = ftell(file);
printf("Header ends at byte: %ld\n", header_end);
// 跳过某些部分,直接读取第3层权重
fseek(file, 1024, SEEK_CUR); // 假设跳过1024字节
float layer3_weights[256];
fread(layer3_weights, sizeof(float), 256, file);
fclose(file);
}
场景3:大文件分块读取
void load_large_file(const char *filename, size_t chunk_size) {
FILE *file = fopen(filename, "rb");
if (!file) { /* 错误处理 */ }
void *buffer = malloc(chunk_size);
if (!buffer) { /* 内存分配失败 */ }
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, chunk_size, file)) > 0) {
// 处理当前数据块(如写入内存或推理)
process_chunk(buffer, bytes_read);
}
free(buffer);
fclose(file);
}
🚀 核心流程图(嵌入式加载模型)
📦 模型文件(model.bin)
│
▼
┌───────────────┐
│ 打开文件 │ fopen
└───────────────┘
│
▼
┌───────────────┐
│ fseek │
│ 👉 跳到某一层 │
└───────────────┘
│
▼
┌───────────────┐
│ fread │
│ 👉 读取数据 │
└───────────────┘
│
▼
┌───────────────┐
│ ftell │
│ 👉 记录位置 │
└───────────────┘
│
▼
🔁 重复(读取下一层)
│
▼
✅ 模型加载完成
🚀 为什么嵌入式必须这样做?
因为设备很弱👇
| 限制 | 影响 |
|---|---|
| 内存小 | 不能一次加载整个模型 |
| CPU弱 | 不能处理太多数据 |
| 存储慢 | 读取要精准 |
👉 所以必须:“按需读取(用多少读多少)”
📦 模型文件结构图(带 offset)
model.bin(二进制模型文件)
┌──────────────────────────────┐
│ Header(文件头) │ offset = 0
│ - magic number │
│ - version │
│ - 层数 layer_num │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ Layer 1 权重 │ offset = 1024
│ (weights + bias) │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ Layer 2 权重 │ offset = 20480
│ (weights + bias) │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ Layer 3 权重 │ offset = 40960
│ (weights + bias) │
└──────────────────────────────┘
│
▼
……(更多层)
🔥 完整流程图(带函数)
📦 model.bin
│
▼
offset = 20480(Layer 2)
│
┌──────────────┐
│ fseek │
│ 👉 跳位置 │
└──────────────┘
│
▼
┌──────────────┐
│ fread │
│ 👉 读参数 │
└──────────────┘
│
▼
┌──────────────┐
│ ftell │
│ 👉 看当前指针│
└──────────────┘
👉 模型文件本质就是:
一段一段连续存储的数据块
👉 而 offset 就是:
每一段的“起始地址”
很多框架会这样做👇
Header 里存:
- 每一层的 offset
- 每一层的大小 size
比如:
Layer1: offset=1024, size=19456
Layer2: offset=20480, size=20480
👉 这样就可以:
fseek(fp, offset);
fread(..., size);
🔥完整代码
#include <stdio.h>
#include <stdlib.h>
#define MAX_LAYERS 10
// 每一层的信息(从文件头读出来)
typedef struct {
int offset; // 在文件中的位置
int size; // 数据大小(字节)
} LayerInfo;
// 模型头部结构
typedef struct {
int layer_num;
LayerInfo layers[MAX_LAYERS];
} ModelHeader;
// 👉 加载某一层(核心函数)
float* load_layer(FILE* fp, LayerInfo layer) {
// 1️⃣ 跳到该层的位置
fseek(fp, layer.offset, SEEK_SET);
// 2️⃣ 分配内存
float* buffer = (float*)malloc(layer.size);
if (buffer == NULL) {
printf("Memory allocation failed\n");
return NULL;
}
// 3️⃣ 读取数据
fread(buffer, 1, layer.size, fp);
// 4️⃣ 打印当前文件位置(调试用)
long pos = ftell(fp);
printf("Read layer at offset %d, now at %ld\n", layer.offset, pos);
return buffer;
}
int main() {
// 1️⃣ 打开模型文件
FILE* fp = fopen("model.bin", "rb");
if (fp == NULL) {
printf("Failed to open file\n");
return -1;
}
// 2️⃣ 读取 Header
ModelHeader header;
fread(&header, sizeof(ModelHeader), 1, fp);
printf("Layer num: %d\n", header.layer_num);
// 3️⃣ 按需加载每一层
for (int i = 0; i < header.layer_num; i++) {
printf("Loading layer %d...\n", i);
float* data = load_layer(fp, header.layers[i]);
if (data == NULL) {
fclose(fp);
return -1;
}
// 👉 这里可以做推理(卷积/全连接等)
// inference(data);
// 4️⃣ 用完就释放(嵌入式必须这么干)
free(data);
}
// 5️⃣ 关闭文件
fclose(fp);
return 0;
}
流程图(代码在干嘛)
📦 model.bin
│
▼
读取 Header(层数 + offset)
│
▼
for 每一层:
│
▼
fseek 👉 跳到该层
│
▼
fread 👉 读取参数
│
▼
推理(用这层数据)
│
▼
free 👉 释放内存
│
▼
下一层...
很多真实项目会这样优化👇
// 只保留一块复用内存
float* buffer = malloc(MAX_LAYER_SIZE);
for (...) {
fseek(...)
fread(buffer, ...)
inference(buffer)
}
👉 优点:
- 不反复 malloc/free
- 更快、更省内存
5. 常见问题与调试
问题1:fread 读取数量不匹配
- 原因:文件末尾、权限不足或磁盘错误。
- 解决:检查
feof(file)和ferror(file)区分原因。
问题2:fseek 后读取错误
- 原因:未以二进制模式(
"rb")打开文件,导致换行符转换破坏偏移。 - 解决:确保文件模式正确。
问题3:ftell 返回 -1
- 原因:文件未打开或指针位置无效(如管道或终端设备)。
- 解决:仅对可定位的文件(如普通文件)使用
ftell。
问题4:大文件处理
- 解决:在Linux中,使用
fseeko/ftello和off_t类型支持大文件(>2GB)。
6. 总结
fseek👉 跳位置:灵活定位文件指针,支持随机访问(如跳过模型元数据)。fread👉 读数据:高效读取二进制数据,需注意返回值和缓冲区管理。ftell👉 查位置:获取当前指针位置,便于调试或分块处理。- 优化建议:
- 大文件优先使用
mmap或fseeko/ftello。 - 嵌入式场景中,结合
fseek和块读取减少I/O次数。 - 始终检查函数返回值以确保鲁棒性。
- 大文件优先使用
在很多推理框架(比如 TensorRT / TFLite / NCNN)中:
👉模型加载本质就是:
- 文件定位(fseek)直接翻到第3章,而不是一页一页翻
- 数据读取(fread)把第3章内容读进脑子(内存)
- 偏移管理(ftell)看看现在读到哪一页了
“流式加载(streaming load)”
也就是:
👉 不一次吃完
👉 一口一口吃 🍜
| 函数 | 作用 | 类比 |
|---|---|---|
| fread | 读取数据 | 看书 |
| fseek | 跳到位置 | 翻页 |
| ftell | 当前进度 | 看页码 |
通过合理使用这三个函数,可以高效加载和处理深度学习模型文件,尤其在资源受限的环境中,精确控制文件读取行为对性能至关重要。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)