1. 项目概述:为什么文件操作是C程序员的必修课
如果你写过C语言程序,大概率遇到过这样的场景:程序运行得很好,但一关掉,所有数据都清零了。或者,你需要读取一个配置文件,或者把程序的计算结果保存下来,下次启动时还能接着用。这时候,你就得跟文件打交道了。文件操作,说白了,就是让程序学会“读写”和“记忆”,是连接程序内部世界和外部持久化存储(比如硬盘、SD卡)的桥梁。对于嵌入式开发来说,这个“外部存储”可能是一块Flash芯片或者通过某种协议连接的传感器,但逻辑是相通的。
C语言本身没有内置的文件操作关键字,这套能力是由标准输入输出库,也就是我们熟悉的
stdio.h
提供的。它通过一个叫
FILE
的结构体指针来抽象化各种不同的数据源和目标,无论是磁盘文件、终端屏幕,还是网络套接字(在支持的情况下),在程序眼里都是一个统一的“流”(stream)。
fputc
、
fread
、
fseek
这些函数,就是我们操作这些流的工具。理解它们,不仅是学会几个API调用,更是理解C语言I/O模型的核心——缓冲、定位、格式转换以及错误处理。尤其在资源受限的嵌入式或RTOS环境里,哪些函数能用、怎么高效地用、有哪些坑,都是实打实的经验。接下来,我会结合我这些年调试过的代码和踩过的坑,把这些函数的里里外外讲清楚。
2. 核心思路:理解“流”与缓冲区的游戏规则
在深入每个函数之前,我们必须建立起两个核心认知: “流”的抽象 和 缓冲区的机制 。这是理解所有文件操作函数行为差异的基石。
2.1 “流”(Stream)到底是什么?
你可以把“流”想象成一条连接程序和某个数据源/目标的单向水管。
FILE *
这个指针,就是你这个“水管工”操作这个水管的把手。标准库预定义了三个已经打开的水管:
stdin
(标准输入,通常对应键盘)、
stdout
(标准输出,通常对应屏幕)、
stderr
(标准错误输出,通常也对应屏幕,但独立缓冲)。当你用
fopen()
打开一个磁盘文件时,你就创建了一条通往该文件的新水管。
这个抽象的美妙之处在于
统一性
。无论底层是文本文件、二进制文件、设备还是内存块,程序都可以用几乎相同的一套函数(
fread
,
fwrite
,
fprintf
等)来读写。这极大地提高了代码的可移植性。
2.2 缓冲区的双刃剑:效率与同步的权衡
为什么直接操作硬件(如磁盘)很慢?因为涉及机械寻道、电路延迟。为了缓解这个速度鸿沟,标准I/O库引入了
缓冲区
。当你调用
fputc(‘A‘, fp)
时,字符 ‘A‘ 绝大多数情况下并不会立刻被写入磁盘文件。它先被放入一个属于
fp
这个流的内存缓冲区里。只有当缓冲区满了、你主动调用
fflush(fp)
、或者你关闭文件
fclose(fp)
时,缓冲区里的数据才会被一次性“刷”到磁盘。
缓冲的三种模式:
- 全缓冲(Fully Buffered) :通常用于磁盘文件。缓冲区满才进行实际I/O。效率最高。
-
行缓冲(Line Buffered)
:通常用于
stdout(当指向终端时)。遇到换行符\n或缓冲区满时刷新。为什么printf的内容有时不立刻显示?因为它还在行缓冲区里等着换行符呢。 -
无缓冲(Unbuffered)
:通常用于
stderr。数据立刻输出。这是为了确保错误信息能第一时间被看到,即便程序下一刻就崩溃了。
注意 :缓冲区的存在是很多初学者困惑的根源。比如,在文件里写入了数据,但用其他程序(或另一个文件指针)却读不到,很可能就是因为数据还在缓冲区里,没有真正落盘。记住,
fclose()会隐式执行fflush()。
2.3 文本流与二进制流的本质区别
用
fopen(“file.txt“, “r“)
和
fopen(“file.bin“, “rb“)
打开同一个文件,行为可能天差地别。关键就在这个
b
模式。
-
文本流(Text Stream)
:在读写时,可能会执行
平台相关的字符转换
。最经典的就是在Windows上,换行符
\n(0x0A) 在写入文件时会被转换成\r\n(0x0D, 0x0A),读入时又会转换回来。在类Unix系统(Linux, macOS)上,通常没有这种转换。这会导致一个严重问题: 在文本模式下,你用fseek和ftell得到的文件位置,可能不等于实际的字节偏移量 ,因为它计算的是转换后的“逻辑位置”。 -
二进制流(Binary Stream)
:数据会
原封不动
、一个字节不差地进行读写。没有转换,
fseek和ftell的数值就是真实的字节偏移。处理图片、音频、结构体数据等非文本内容时, 必须使用二进制模式 。
// 一个常见的坑:在Windows上用文本模式读写结构体
struct Data {
int id;
char name[20];
};
struct Data d = {10, “Tom\nJerry“};
FILE *fp = fopen(“data.dat“, “w“); // 错误!应该是 “wb“
fwrite(&d, sizeof(struct Data), 1, fp); // 如果结构体数据里碰巧有0x0A(\n),在Windows上会被错误转换
fclose(fp);
3. 逐字符与字符串操作:fputc、fputs及其家族
我们先从最基础的写入操作开始。写入分为按字符写和按字符串写。
3.1 fputc:一个字符一个字符地雕刻
int fputc(int c, FILE *stream);
这个函数如其名:
f
(file)
put
(放)
c
(character字符)。它将一个字符写入指定的流。
-
参数解析
:
c虽然是int类型,但实际写入的是其低8位(转换为unsigned char)。EOF(通常是-1)是一个特殊的int值,用于表示文件结束或错误,所以返回值需要int来容纳它和所有可能的字符值(0-255)。 -
返回值
:成功时返回写入的字符(转换为
int),失败时返回EOF。 - 底层细节 :它通常被实现为宏,而不是函数,以减少函数调用的开销。但标准保证它的行为如同一个函数,你可以安全地对其取地址(虽然很少需要这么做)。
// 示例:用 fputc 实现一个简单的字符串写入函数
void my_fputs(const char *str, FILE *fp) {
while (*str != ‘\0‘) {
if (fputc(*str, fp) == EOF) {
// 处理写入错误,例如磁盘满
perror(“写入文件失败“);
break;
}
str++;
}
}
实操心得 :
-
错误检查不可少
:永远不要假设
fputc总能成功。磁盘空间不足、文件被移除、流已关闭等情况都会导致失败。对于关键数据,检查返回值是必须的。 -
效率考量
:虽然
fputc简单,但频繁调用它写入大量数据效率很低,因为每次调用都可能涉及函数/宏开销和缓冲区检查。对于批量写入,应优先使用fputs或fwrite。
3.2 fputs:写入整个字符串
int fputs(const char *s, FILE *stream);
它把
s
指向的、以空字符
\0
结尾的字符串写入流,但
不写入结尾的空字符
,也
不自动添加换行符
。
-
与
puts()的区别 :这是最容易混淆的点。puts(s)等价于fputs(s, stdout)再加上一个自动追加的换行符\n。所以fputs(“Hello“, stdout);不会换行,而puts(“Hello“);会。 -
返回值
:成功返回非负值(通常为0),失败返回
EOF。
FILE *fp = fopen(“log.txt“, “a“); // “a“ 模式追加写入
if (fp) {
fputs(“[INFO] 程序启动\n“, fp); // 需要自己加\n
fputs(“[INFO] 加载配置...“, fp); // 这一行不会换行
// ... 一些操作
fputs(“完成\n“, fp); // 和上一行共同组成 “加载配置...完成”
fclose(fp);
}
3.3 对应的读取函数:fgetc 与 fgets
有写就有读,它们是
fputc
和
fputs
的镜像。
-
int fgetc(FILE *stream);:从流中读取下一个字符,返回int类型。到达文件末尾或出错时返回EOF。 关键点 :为了区分有效字符(0-255)和EOF(-1),返回值必须是int。如果你错误地赋值给char,在遇到值为0xFF的字符时,可能被错误地解释为EOF。// 正确做法 int c; while ((c = fgetc(fp)) != EOF) { putchar(c); } // 危险做法 char ch; // 可能是 signed char,范围 -128~127 while ((ch = fgetc(fp)) != EOF) { // 如果读到0xFF,转换为char可能是-1,被误判为EOF // ... } -
char *fgets(char *s, int size, FILE *stream);:从流中读取最多size-1个字符到缓冲区s中。读取会在遇到换行符\n或文件结束时停止。换行符(如果被读取)会被 存储 到缓冲区,然后在末尾自动添加空字符\0。这是它与gets()(已废弃,极其危险)最大的安全区别。gets()不检查缓冲区边界,是缓冲区溢出攻击的经典入口。
嵌入式/RTOS特别提醒
:
在你提供的资料中提到:“On embedded/ RTOS systems this function only is implemented for stdin, stdout and stderr files.” 这句话非常关键。在许多轻量级或裸机嵌入式C库中,为了节省空间,文件操作函数可能只对标准流(stdin, stdout, stderr)提供完整支持,这些流可能重定向到串口(UART)。对于通过
fopen
打开的普通磁盘文件,
fputc
/
fgetc
可能无法工作。在开发嵌入式软件时,
务必查阅你所使用的C库(如newlib, glibc for embedded, 或厂商提供的库)的文档
,确认哪些函数在哪些场景下可用。通常,你需要使用更底层的、面向特定存储设备的驱动API(如SPI Flash的读写函数)来替代标准文件操作。
4. 格式化输入输出:fprintf 与 fscanf
当需要读写结构化的文本数据(如配置文件、日志)时,逐字符或整串操作太原始。这时就该
fprintf
和
fscanf
登场了。
4.1 fprintf:把printf的输出重定向到文件
int fprintf(FILE *stream, const char *format, ...);
它就是
printf
的通用版本。
printf(“Hello %s\n“, name);
本质上就是
fprintf(stdout, “Hello %s\n“, name);
。
-
核心价值
:它能将各种类型的数据(int, float, string等)按照指定的格式(
format字符串)转换成文本,并写入任何流。这是生成人类可读文件(如日志、CSV)的主要工具。 -
格式字符串详解
:
format字符串包含普通字符和以%开头的转换说明符。例如%10s表示输出一个至少占10个字符宽的字符串,右对齐。%4.4f表示输出浮点数,总宽度至少4字符,其中小数点后保留4位。%-10d表示输出整数,左对齐,占10字符宽。
FILE *fp = fopen(“data.txt“, “w“);
if (fp) {
int id = 1001;
float score = 95.5f;
char name[] = “Alice“;
// 格式化写入,便于其他程序或人工阅读
fprintf(fp, “ID: %05d, Name: %-10s, Score: %6.2f\n“, id, name, score);
// 输出内容:ID: 01001, Name: Alice , Score: 95.50
fclose(fp);
}
4.2 fscanf:从文件解析格式化文本
int fscanf(FILE *stream, const char *format, ...);
它是
scanf
的通用版本,用于从流中读取数据,并根据
format
字符串进行解析。
-
工作原理
:
fscanf会尝试匹配format中的普通字符,并按照转换说明符(如%d,%f,%s)解析输入流中的数据,将结果存储到后续参数指向的变量中。 所有非指针的参数都必须传递其地址 (使用&取地址符),因为fscanf需要修改它们。 -
返回值
:返回成功匹配并赋值的输入项的数量。如果文件一开始就结束,则返回
EOF。这个返回值对于错误处理和循环读取至关重要。
FILE *fp = fopen(“data.txt“, “r“);
if (fp) {
int id;
char name[20];
float score;
// 尝试从文件中读取一行格式化的数据
int items_matched = fscanf(fp, “ID: %d, Name: %19s, Score: %f“, &id, name, &score);
// 注意:%19s 防止缓冲区溢出,为末尾的\0留出空间
if (items_matched == 3) {
printf(“成功读取: ID=%d, Name=%s, Score=%.2f\n“, id, name, score);
} else if (items_matched == EOF) {
printf(“已到达文件末尾。\n“);
} else {
printf(“格式匹配错误,只匹配了 %d 项。\n“, items_matched);
}
fclose(fp);
}
fscanf的“陷阱”与高级技巧 :
-
空白符处理
:对于
%d,%f,%s等大多数说明符,fscanf会跳过输入开始处的空白符(空格、制表符、换行符)。但%c不会跳过任何空白符,它会读取下一个字符,无论它是什么。 -
%s的危险性 :%s会读取非空白字符序列,直到遇到空白符。它不会检查目标缓冲区的大小,极易导致缓冲区溢出。 务必使用宽度限定符 ,如%19s表示最多读取19个字符。 -
扫描集
%[]:这是一个强大但易被忽略的功能。%[a-z]会读取所有小写字母,直到遇到非小写字母。%[^,\n]会读取所有字符,直到遇到逗号或换行符。这在解析CSV或特定分隔符的数据时非常有用。char buffer[100]; // 读取一行,直到换行符,但包含空格 fscanf(fp, “%[^\n]“, buffer); // 注意:上面的调用不会消耗换行符,下次fscanf会立刻遇到\n。通常需要加一个 fgetc(fp) 来读取并丢弃换行符。 -
赋值抑制符
*:在%和转换字符之间加*,表示读取该字段但不赋值。例如fscanf(fp, “%*d %d“, &important_num);会跳过第一个整数,只读取第二个。
重要提示 :
fscanf对于格式错误的数据非常脆弱。在解析来源不可控的文件(如用户输入、网络数据)时,更安全的做法是先用fgets读取整行到缓冲区,然后用sscanf(字符串版本的scanf)进行解析,并仔细检查返回值。这样既能控制行长度,又能进行更精细的错误恢复。
5. 二进制数据块操作:fread 与 fwrite
当处理非文本数据(如图像、音频、结构体、数组)时,
fprintf
/
fscanf
的文本转换过程既低效又可能损失精度(如浮点数)。此时,
fread
和
fwrite
是直接的选择,它们直接在内存块和文件之间搬运字节。
5.1 fwrite:将内存镜像写入文件
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
这个函数的参数设计体现了C语言的哲学:给你足够的控制力,也给你足够的犯错空间。
-
参数解析
:
-
ptr: 指向要写入数据的内存起始地址。 -
size: 每个数据项的字节大小。通常用sizeof(数据类型)获取。 -
nmemb: 要写入的数据项个数。 -
stream: 目标文件流。
-
-
工作方式
:函数从
ptr开始,连续读取size * nmemb个字节,写入流。 它不关心这些字节代表什么 (整数、结构体、还是乱码),只是忠实地复制。 -
返回值
:返回成功写入的
数据项个数
(
nmemb),而非字节数。如果返回值小于nmemb,说明发生了错误(如磁盘满)。 永远要检查这个返回值!
struct SensorData {
uint32_t timestamp;
float temperature;
float humidity;
};
struct SensorData readings[100];
// ... 填充 readings 数组 ...
FILE *fp = fopen(“sensor_log.bin“, “wb“); // 注意 “b“ 二进制模式
if (fp) {
size_t items_written = fwrite(readings, sizeof(struct SensorData), 100, fp);
if (items_written != 100) {
perror(“写入传感器数据失败“);
// 处理部分写入的情况
}
fclose(fp);
}
5.2 fread:从文件读取数据到内存
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
参数和
fwrite
完全对称,行为也对称。
-
工作方式
:尝试从流中读取
size * nmemb个字节到ptr指向的内存中。 -
返回值
:返回成功读取的
数据项个数
。如果返回值小于
nmemb,可能因为:1) 到达文件末尾 (EOF);2) 发生读取错误。需要用feof(fp)和ferror(fp)来区分。 -
重要特性
:如果文件剩余字节数不足以满足一个完整
size的数据项,fread会直接失败,该项不会被读取。例如,size=sizeof(int)=4,nmemb=10,但文件只剩35字节,则最多只能读取8个int(32字节),返回值是8。
// 接上例,读取数据
struct SensorData loaded_data[100];
FILE *fp = fopen(“sensor_log.bin“, “rb“); // 注意 “b“ 二进制模式
if (fp) {
size_t items_read = fread(loaded_data, sizeof(struct SensorData), 100, fp);
if (items_read != 100) {
if (feof(fp)) {
printf(“文件已读完,实际读取了 %zu 条记录。\n“, items_read);
} else if (ferror(fp)) {
perror(“读取文件时发生错误“);
}
}
fclose(fp);
}
fread/fwrite 的核心注意事项与经验 :
-
二进制模式是必须的
:如前所述,在Windows等系统上,文本模式会进行换行符转换,这会彻底破坏二进制数据的完整性。读写二进制文件,
fopen的模式字符串里必须带b。 -
结构体对齐与填充
:这是最大的坑之一。编译器为了内存访问效率,可能会在结构体成员之间插入“填充字节”(Padding)。
sizeof(struct SensorData)可能大于所有成员大小之和。当你用fwrite写入一个结构体时,这些填充字节的 不确定值 也被写入了文件。如果换一个编译器、甚至换一个编译选项,结构体的内存布局可能改变,此时再用fread读回来,数据就对不上了。-
解决方案A(跨平台/长期存储不推荐)
:不要直接读写包含填充字节的结构体。改为逐个成员读写,或使用
#pragma pack(1)(编译器指令,慎用)取消填充,但这可能降低性能并引发硬件对齐错误。 -
解决方案B(推荐)
:使用
序列化/反序列化
。定义明确的文件格式(如TLV,Type-Length-Value),将每个成员单独转换为字节流(如用
htonl处理整数保证字节序)再写入。读取时反向解析。虽然麻烦,但最可靠、最可移植。
-
解决方案A(跨平台/长期存储不推荐)
:不要直接读写包含填充字节的结构体。改为逐个成员读写,或使用
-
字节序(Endianness)问题
:如果数据要在不同架构(如x86的小端序和某些网络协议的大端序)的机器间交换,整数、浮点数等多字节数据的字节顺序需要处理。通常使用
htonl,ntohl等函数进行转换。 -
更新模式(“+“)下的读写切换
:在
“r+b“或“w+b“模式下,同一个流不能连续进行读写操作而不重新定位。例如,写完之后想读刚写入的数据,必须在写和读之间插入fflush(fp)、fseek(fp, 0, SEEK_CUR)或rewind(fp)等操作来刷新缓冲区或重置文件位置。
6. 随机访问与文件定位:fseek、ftell、rewind
文本流通常是顺序访问的,但很多场景(如数据库、存档文件)需要随机访问文件任意位置的数据。这就是文件定位函数的用武之地。
6.1 ftell:获取当前位置
long int ftell(FILE *stream);
返回当前文件位置指示器相对于文件开头的
字节偏移量
。对于二进制流,这个值就是距离文件开头的字节数。对于文本流,这个值不一定等于字符数(由于换行符转换等),但它可以安全地传递给
fseek
用于返回该位置。
6.2 fseek:移动位置指示器
int fseek(FILE *stream, long offset, int whence);
这是实现随机访问的核心。
-
参数
whence:-
SEEK_SET:从文件 开头 计算偏移。offset必须 >= 0。 -
SEEK_CUR:从 当前位置 计算偏移。offset可正可负。 -
SEEK_END:从文件 末尾 计算偏移。offset通常 <= 0(用于向后定位),但标准允许向后定位超过文件末尾,这会在该位置写入数据时创建“空洞”。
-
- 返回值 :成功返回0,失败返回非0值。
-
重要限制
:对于以文本模式打开的文件(尤其是在Windows上),
fseek和ftell的行为受到换行符转换的影响,offset可能不是直观的字节数。唯一可移植的操作是:fseek(fp, 0, SEEK_SET)(回到开头),fseek(fp, 0, SEEK_END)(定位到末尾),以及fseek(fp, pos, SEEK_SET)其中pos是之前从ftell获得的值。
6.3 rewind:快速回到开头
void rewind(FILE *stream);
它等价于
(void)fseek(fp, 0L, SEEK_SET)
,但同时会清除流的错误标志。比
fseek
更简洁。
6.4 fgetpos 与 fsetpos:处理大文件
ftell
和
fseek
使用
long int
表示位置,在32位系统上可能无法处理大于2GB的文件。C标准提供了
fgetpos
和
fsetpos
这对函数,它们使用不透明的
fpos_t
类型来记录位置,理论上可以处理任意大的文件。
fpos_t pos;
fgetpos(fp, &pos); // 保存当前位置
// ... 一些操作后
fsetpos(fp, &pos); // 精确地回到之前的位置
实战示例:读取文件末尾的固定长度记录 假设一个日志文件,每条记录是固定大小的结构体,我们想读取最后一条记录。
struct LogRecord {
time_t timestamp;
char message[256];
};
FILE *fp = fopen(“app.log“, “rb“);
if (fp) {
// 1. 定位到文件末尾
if (fseek(fp, 0, SEEK_END) != 0) {
perror(“无法定位到文件末尾“);
fclose(fp);
return;
}
// 2. 获取文件总大小(字节数)
long file_size = ftell(fp);
if (file_size == -1L) {
perror(“无法获取文件大小“);
fclose(fp);
return;
}
// 3. 计算最后一条记录的起始位置
// 假设文件大小正好是记录的整数倍
long last_record_offset = file_size - sizeof(struct LogRecord);
if (last_record_offset < 0) {
printf(“文件为空或损坏。\n“);
fclose(fp);
return;
}
// 4. 定位到最后一条记录的开头
if (fseek(fp, last_record_offset, SEEK_SET) != 0) {
perror(“无法定位到最后一条记录“);
fclose(fp);
return;
}
// 5. 读取记录
struct LogRecord last_record;
if (fread(&last_record, sizeof(struct LogRecord), 1, fp) != 1) {
if (feof(fp)) {
printf(“意外到达文件末尾。\n“);
} else {
perror(“读取记录失败“);
}
} else {
printf(“最后一条记录时间: %ld, 消息: %s\n“,
(long)last_record.timestamp, last_record.message);
}
fclose(fp);
}
7. 常见问题、错误处理与调试技巧
文件操作是I/O密集型任务,出错是常态而非例外。健壮的程序必须处理这些错误。
7.1 必须检查的返回值
| 函数 | 成功返回值 | 失败/特殊返回值 | 检查要点 |
|---|---|---|---|
fopen
|
非NULL的
FILE*
|
NULL
| 总是检查 !失败原因:路径错误、权限不足、磁盘满。 |
fclose
|
0
|
EOF
| 关闭失败可能意味着数据未完全写入(缓冲区未刷新)。对于关键文件,应检查。 |
fread
/
fwrite
|
请求的项数 (
nmemb
)
| 小于请求的项数 |
检查是否等于
nmemb
。小于则用
feof()
和
ferror()
判断原因。
|
fscanf
| 成功匹配并赋值的输入项数 |
EOF
(文件结束) 或 小于预期项数
|
循环读取时,判断是否
== EOF
结束。解析时,判断是否等于预期项数。
|
fseek
/
fsetpos
|
0
| 非0 | 定位失败(如位置超出文件范围)。 |
ftell
| 当前文件位置 (>=0) |
-1L
| 获取位置失败。 |
fgetc
,
fputc
等
| 读取/写入的字符 (转为int) |
EOF
|
区分是文件结束还是错误,需用
feof()
和
ferror()
。
|
7.2 错误信息获取:perror 与 strerror
当函数失败并设置全局
errno
变量后,可以用以下方法获取可读的错误描述:
-
void perror(const char *s);:打印你提供的字符串s,后跟冒号和当前errno对应的错误信息。非常方便。FILE *fp = fopen(“nonexistent.txt“, “r“); if (fp == NULL) { perror(“打开文件失败“); // 输出: 打开文件失败: No such file or directory } -
char *strerror(int errnum);:需要#include <string.h>。返回错误码对应的字符串,可以用于自定义格式化输出。
7.3 文件尾与错误状态清除
-
int feof(FILE *stream);:检查流的 文件结束指示器 是否被设置。仅在尝试读取超过文件末尾后才会为真。 常见误区 :不要用while (!feof(fp))作为读取循环的条件,因为这会导致最后一次读取无效数据后仍进入循环。正确的模式是:while (fgets(buffer, sizeof(buffer), fp) != NULL) { // 处理 buffer } // 循环结束后,再用 feof() 或 ferror() 判断是正常结束还是出错 -
int ferror(FILE *stream);:检查流的 错误指示器 是否被设置。 -
void clearerr(FILE *stream);:清除流的文件结束和错误指示器。在发生错误后,如果希望重试,需要先调用此函数。
7.4 嵌入式环境下的特殊考量
-
函数支持不全
:如资料所述,嵌入式C库可能只实现标准流(
stdin,stdout,stderr)的操作。操作普通文件需用底层驱动。 -
没有文件系统
:在裸机或极简RTOS中,可能根本没有“文件”的概念。数据可能直接写入EEPROM、Flash的特定扇区。你需要实现类似
_read,_write的系统调用(syscall)钩子,将标准库的I/O调用映射到你的设备驱动。 -
缓冲区大小
:嵌入式系统内存紧张。可以通过
setbuf(fp, NULL)关闭缓冲,或使用setvbuf设置自定义的小缓冲区,以减少内存占用,但会降低I/O效率。 -
实时性
:
fwrite的数据可能还在缓冲区,未真正写入非易失存储器。系统崩溃会导致数据丢失。对于关键数据,写入后应立即fflush(fp),甚至调用操作系统同步命令(如fsync)。
7.5 调试技巧:观察文件实际内容
当程序写入文件的内容和预期不符时,不要只相信
printf
。用十六进制查看工具(如
hexdump -C filename
在Linux,或
od -x filename
,或在Windows上用Notepad++的插件)直接查看文件的原始字节。这能帮你立刻发现文本/二进制模式错误、字节序问题、结构体填充、或者多余的换行符。

4082


被折叠的 条评论
为什么被折叠?



