3. 标准I/O编程¶
标准I/O库作为C语言标准库的核心组件,基于系统调用封装,屏蔽了底层硬件与系统差异, 自带缓冲区优化机制,简化了I/O操作流程,兼具跨平台性、易用性和高效性, 是C语言文件操作、数据交互的首选方案。
本章的示例代码目录为:base_linux/file_io/stdio
3.1. 标准I/O库简介¶
3.1.1. 标准I/O库的定义与起源¶
标准I/O库(Standard I/O Library)是ANSI C标准定义的通用I/O操作库,头文件为<stdio.h>, 由C语言标准委员会统一规范,所有支持C语言的平台(Linux、Windows、macOS等)均实现了该库, 彻底解决了底层系统调用的跨平台兼容性问题。
标准I/O库的本质是对Linux底层系统调用(read、write、open、close等)的封装, 通过在用户空间构建缓冲区,减少频繁的内核态与用户态切换,降低磁盘I/O次数, 大幅提升数据读写效率,尤其适合大量数据的读写场景。
3.1.2. 标准I/O库的优势¶
跨平台兼容性:遵循C语言标准,代码无需修改即可在不同操作系统编译运行,摆脱平台依赖;
自带缓冲区优化:内置用户态缓冲区,自动聚合读写数据,减少系统调用次数,提升I/O效率;
操作简洁易用:封装了丰富的高层函数,无需关注底层硬件细节,简化文件打开、读写、关闭等操作;
功能丰富全面:支持字符、字符串、格式化、二进制等多种读写方式,适配各类数据交互场景;
完善的错误处理:搭配全局错误码变量,可精准定位I/O操作失败原因,便于调试排错。
3.1.3. 核心头文件与数据类型¶
核心头文件:<stdio.h>是标准I/O编程的必备头文件,包含了所有标准I/O函数的声明、宏定义、数据类型定义与结构体原型, 只要程序中使用标准I/O相关函数,就必须包含该头文件,否则会出现函数未定义的编译错误;
核心数据类型:FILE结构体是标准I/O库的核心载体,通常被称为“流”,是对文件、终端、设备等I/O资源的抽象封装, 内部集成了文件描述符、缓冲区起始地址、缓冲区大小、当前读写偏移量、读写状态标志、错误标志等关键信息, 开发者无需关注结构体内部的具体实现细节,只需通过FILE*指针即可完成对I/O资源的所有操作,极大简化了开发流程;
常用宏:EOF(End Of File)是文件结束标志宏,本质值为-1,用于判断文件读取是否完成; NULL是空指针宏,用于初始化指针、判断指针有效性;BUFSIZ是标准I/O默认缓冲区大小宏,通常为4096字节或8192字节, 适配主流文件系统的块大小,是缓冲区操作的常用参考值。
3.2. 标准I/O的缓冲区机制¶
缓冲区是标准I/O库的核心设计,指在用户空间开辟的一段内存区域,数据先存入缓冲区, 待满足特定条件后,再一次性批量写入内核或磁盘,以此减少系统调用次数,提升程序运行效率。 根据缓冲方式的不同,标准I/O分为全缓冲、行缓冲、无缓冲三类,不同缓冲机制适配不同场景。
3.2.1. 全缓冲¶
全缓冲只有当缓冲区被填满、执行fflush刷新操作或关闭流时,数据才会从缓冲区写入目标文件/设备, 适合普通磁盘文件的读写操作,是标准I/O操作文件的默认缓冲方式。
触发刷新条件:缓冲区满、调用fflush()、调用fclose()、程序正常退出。
特点:I/O效率最高,适合大量数据的批量读写,但数据实时性较差,缓冲区未刷新前断电会导致数据丢失。
3.2.2. 行缓冲¶
行缓冲是当缓冲区中出现换行符(\n)、缓冲区被填满、执行刷新操作或关闭流时,数据立即写入目标, 适合标准输入流(stdin)、标准输出流(stdout),适配终端交互场景。
触发刷新条件:遇到换行符、缓冲区满、调用fflush()、调用fclose()、程序正常退出。
特点:兼顾效率与实时性,终端输入输出时,输入回车即可立即处理数据,符合用户交互习惯。
3.2.3. 无缓冲¶
无缓冲就是不存在缓冲区,数据直接写入目标文件/设备,无任何延迟。 适合标准错误流(stderr),确保错误信息立即输出,便于快速排查程序异常。
特点:数据实时性极强,无丢失风险,但频繁触发系统调用,I/O效率较低。
3.2.4. 缓冲区相关操作函数¶
3.2.4.1. fflush函数¶
fflush函数功能是强制刷新指定流的缓冲区,将缓冲区数据立即写入目标。
1 2 3 4 5 6 7 8 | // 所需头文件
#include <stdio.h>
// 函数原型
int fflush(FILE *stream);
// 参数:stream-待刷新的流指针,传入NULL则刷新所有打开的流
// 返回值:成功返回0,失败返回EOF
|
3.2.4.2. setbuf/setvbuf函数¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 所需头文件
#include <stdio.h>
// setbuf函数原型
void setbuf(FILE *stream, char *buf);
// setvbuf函数原型(更灵活,推荐使用)
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
//参数:
// stream参数:指向FILE对象的指针,该FILE对象标识了一个打开的流
// buffer参数:分配给用户的缓冲,它的长度至少为BUFSIZ字节,BUFSIZ是一个宏常量,表示数组的长度
// mode参数:_IOFBF(全缓冲)、_IOLBF(行缓冲)、_IONBF(无缓冲)
// size参数:自定义缓冲区大小
|
必须在流打开后、未进行任何读写操作前设置缓冲区。自定义缓冲区需全局声明或动态分配,避免栈内存释放导致的野指针问题。
3.3. 标准输入输出与错误¶
Linux系统启动后,会默认打开三个标准流,对应终端的输入、输出、错误提示设备, 无需手动打开/关闭,可直接调用标准I/O函数操作,是程序与用户交互的核心通道。
3.3.1. 三大标准流说明¶
三大标准流如下:
标准流名称 |
FILE指针 |
缓冲类型 |
作用 |
|---|---|---|---|
标准输入流 |
stdin |
行缓冲 |
接收用户输入的数据 |
标准输出流 |
stdout |
行缓冲 |
向终端输出正常数据、提示信息、运行结果 |
标准错误流 |
stderr |
无缓冲 |
向终端输出程序错误信息、异常提示 |
3.3.2. 标准流的重定向¶
Linux支持标准流重定向,可将默认的终端输入/输出改为文件、管道等其他设备,不修改代码即可实现数据流向切换,常用重定向符号:
>:标准输出重定向,例:./test > output.txt;
>>:标准输出追加重定向,例:./test >> output.txt;
2>:标准错误重定向,例:./test 2> error.txt;
<:标准输入重定向,例:./test < input.txt;
&>:标准输出+标准错误一并重定向,例:./test &> all.log。
3.3.3. 标准流常用操作函数¶
3.3.3.1. 格式化输出函数¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 所需头文件
#include <stdio.h>
// printf默认输出至stdout
int printf(const char *format, ...);
// fprintf可指定输出流,stdout/stderr/文件流
int fprintf(FILE *stream, const char *format, ...);
// 参数:
// stream:指向FILE对象的指针,该FILE对象标识了流
// format 字符串,包含了要被写入到流stream中的文本
// 返回值:如果成功,则返回写入的字符总数,否则返回一个负数
|
格式化输出函数支持%d、%s、%f、%c等多种格式符,可灵活拼接变量与字符串, 实现格式化数据输出,其中fprintf相比printf更灵活,可指定任意流作为输出目标, 是区分正常输出与错误输出的常用函数。
3.3.3.2. 格式化输入函数¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 所需头文件
#include <stdio.h>
// scanf:默认从stdin读取数据
int scanf(const char *format, ...);
// fscanf:可指定输入流
int fscanf(FILE *stream, const char *format, ...);
// 参数:
// stream:指向FILE对象的指针,该FILE对象标识了流
// format 字符串,包含了以下各项中的一个或多个:
// 空格字符、非空格字符和format说明符
// 返回值:如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回EOF
|
格式化输入函数需严格匹配格式符与变量类型,且变量需传入地址符,否则会出现内存访问异常; fscanf可实现从文件读取格式化数据,是解析配置文件、结构化文本的常用函数。
3.4. 标准I/O库核心操作函数¶
标准库实际是对系统调用再次进行了封装,使用C标准库编写的代码,能方便地在不同的系统上移植。 例如Windows系统打开文件操作的系统API为OpenFile,Linux则为open,C标准库都把它们封装为fopen,Windows下的C库会通过fopen调用OpenFile函数实现操作, 而Linux下则通过glibc调用open打开文件。用户代码如果使用fopen,那么只要根据不同的系统重新编译程序即可,而不需要修改对应的代码。
标准I/O库的文件操作遵循固定的标准化流程:打开流->读写流->定位流->关闭流,每个环节都对应专属的标准I/O函数, 支持字符、字符串、格式化、二进制等多种数据读写方式,可适配不同业务场景的I/O需求。
3.4.1. 打开流¶
3.4.1.1. fopen/fdopen函数¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // 所需头文件
#include <stdio.h>
// 通过文件路径打开文件流,是实际开发中最常用的打开方式
FILE *fopen(const char *path, const char *mode);
// 将底层系统调用的文件描述符,转换为标准I/O流指针
FILE *fdopen(int fd, const char *mode);
// 参数:
// path:待打开文件的路径字符串,支持相对路径与绝对路径
// mode:文件打开模式字符串,指定读写权限、操作模式
// fd:底层系统调用获取的有效文件描述符,int类型
// 返回值:
// 成功打开流,返回对应的FILE*类型流指针;
// 失败返回NULL,可通过errno获取具体错误原因
|
mode可以是以下表格中的值:
模式 |
功能 |
文件不存在 |
文件存在 |
|---|---|---|---|
r |
只读,文本模式 |
报错,返回NULL |
正常打开,从文件开头读取 |
r+ |
读写,文本模式 |
报错,返回NULL |
正常打开,支持读写,不清空原有内容 |
w |
只写,文本模式 |
自动创建新文件 |
清空原有内容,从文件开头写入 |
w+ |
读写,文本模式 |
自动创建新文件 |
清空原有内容,支持读写操作 |
a |
追加只写,文本模式 |
自动创建新文件 |
保留原有内容,从文件末尾追加写入 |
a+ |
追加读写,文本模式 |
自动创建新文件 |
保留原有内容,写入默认在末尾,读取可任意定位 |
在基础打开模式后追加b字符(如rb、wb、ab),表示切换为二进制模式,专门用于处理图片、音频、视频、可执行文件等非文本文件, 避免文本模式下对换行符、特殊字符的自动转换;Linux系统对文本模式与二进制模式不做区分,两种模式行为一致, 但Windows系统存在明显差异,开发跨平台程序时必须严格区分。
3.4.2. 关闭流¶
3.4.2.1. fclose函数¶
fclose函数用于自动刷新流缓冲区,将残留数据写入目标文件,释放流占用的内存资源,关闭文件。
1 2 3 4 5 6 7 8 9 | // 所需头文件
#include <stdio.h>
// 函数原型
int fclose(FILE *stream);
// 参数:stream 待关闭的有效文件流指针,FILE*类型
// 返回值:成功关闭流,返回0;失败,返回EOF
|
fclose函数是标准I/O编程中不可或缺的收尾操作,若遗漏关闭流,不仅会造成进程内存资源泄漏,长期运行会耗尽系统资源, 还会导致缓冲区残留数据无法刷入文件,造成数据丢失。
3.4.3. 读写操作函数¶
3.4.3.1. 单字符读写¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 所需头文件
#include <stdio.h>
// 从指定流读取单个字符
int fgetc(FILE *stream);
// 向指定流写入单个字符
int fputc(int c, FILE *stream);
// 参数:
// stream:目标操作流指针,FILE*类型,fgetc对应读流,fputc对应写流
// c:待写入的字符,int类型,传入时会自动转换为unsigned char类型
// 返回值:
// fgetc:读取成功返回读取的字符(int型),读取失败/到达文件尾返回EOF;
// fputc:写入成功返回写入的字符(int型),写入失败返回EOF
|
单字符读写函数适合逐字符处理文本、解析特殊字符场景,操作粒度细,返回值需严格判断,避免EOF导致的逻辑异常,是基础字符处理的核心函数。
3.4.3.2. 字符串读写¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // 所需头文件
#include <stdio.h>
// 从指定流读取指定长度的字符串,自动追加字符串结束符'\0'
char *fgets(char *s, int size, FILE *stream);
// 向指定流写入字符串,不自动追加换行符,灵活可控
int fputs(const char *s, FILE *stream);
// 参数:
// s:字符数组/缓冲区指针,fgets用于存储读取的字符串,fputs为待写入的字符串首地址
// size:读取的最大字符数,int类型,实际读取size-1个字符,预留位置存储结束符
// stream:目标操作流指针,FILE*类型,fgets对应读流,fputs对应写流
// 返回值:
// fgets:读取成功返回存储字符串的缓冲区指针s,读取失败/到达文件尾返回NULL
// fputs:写入成功返回非负值,写入失败返回EOF
|
fgets会限制读取长度,彻底杜绝缓冲区溢出漏洞,相比已弃用的gets函数更安全; fputs无自动换行,可精准控制输出格式,适合拼接字符串输出场景,二者是文本字符串读写的最优组合。
3.4.3.3. 二进制读写¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 所需头文件
#include <stdio.h>
// 二进制读函数:从指定流读取批量二进制数据,适用于结构体、数组等非格式化数据
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
// 二进制写函数:向指定流写入批量二进制数据,适用于结构化数据、二进制文件存储
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
// 参数:
// ptr:数据存储缓冲区地址,fread为读取数据的存储区,fwrite为待写入数据的起始地址
// size:单个数据单元的字节数,size_t无符号整型,如单字节char为1,整型int为4
// nmemb:期望读写的数据单元个数,size_t无符号整型
// stream:目标操作流指针,FILE*类型,需以对应读写模式打开
// 返回值:成功读写的数据单元个数,出错或到达文件末尾返回小于nmemb的数值
|
二进制读写函数以数据块为单位操作,不做任何数据转换,适合结构体、数组、二进制资源等非格式化数据的读写, 效率远高于格式化读写,是存储结构化数据、二进制文件的核心选择。
3.4.4. 流定位函数¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // 所需头文件
#include <stdio.h>
// 获取当前流的读写偏移量,可用于计算文件大小
long ftell(FILE *stream);
// 设置流的读写偏移量,实现文件随机读写
int fseek(FILE *stream, long offset, int whence);
// 重置读写偏移量至文件开头,等价于fseek(stream, 0, SEEK_SET),无返回值
void rewind(FILE *stream);
// 参数:
// stream:待定位的文件流指针,FILE*类型
// offset:偏移量,long类型,正数向后偏移,负数向前偏移
// whence:偏移基准位置,可选值:
// SEEK_SET(以文件开头为基准偏移)、SEEK_CUR(以当前偏移量为基准偏移)、SEEK_END(以文件末尾为基准偏移)
// 返回值:
// ftell:成功返回当前读写偏移量,失败返回-1L;
// fseek:成功返回0,失败返回非0值;
// rewind:无返回值,执行后流偏移量重置为文件开头,同时清除流错误标志
|
流定位函数打破了文件顺序读写的限制,实现随机读写,适合大文件定位修改、结构化数据随机访问场景, ftell可配合fseek实现文件大小计算、偏移量回溯,是高级文件操作的必备函数。
3.4.5. 错误处理函数¶
3.4.5.1. ferror函数¶
ferror函数用于检测指定的文件流是否发生了读写错误。
1 2 3 4 5 6 7 8 9 10 11 | // 所需头文件
#include <stdio.h>
// 函数原型
int ferror(FILE *stream);
// 参数:stream 指向已打开文件流的指针,用于指定要检测的目标流
// 返回值:
// 非零值:该流上发生过错误
// 0 :该流上未发生错误
|
3.4.5.2. feof函数¶
feof函数用于检测文件流的文件结束标志(EOF)是否被置位,用于判断是否已读取到文件末尾。
1 2 3 4 5 6 7 8 9 10 11 | // 所需头文件
#include <stdio.h>
// 函数原型
int feof(FILE *stream);
// 参数:stream 指向已打开文件流的指针,用于指定要检测的目标流
// 返回值:
// 非零值 - 已到达文件末尾(EOF标志被置位)
// 0 - 尚未到达文件末尾
|
3.4.5.3. clearerr函数¶
clearerr函数用于清除指定文件流的错误标志和文件结束标志(EOF),使流恢复到正常状态。
1 2 3 4 5 6 7 | // 所需头文件
#include <stdio.h>
// 函数原型
void clearerr(FILE *stream);
// 参数:stream 指向已打开文件流的指针,用于指定要重置的目标流
|
3.4.5.4. perror函数¶
perror函数用于将上一个系统调用或库函数产生的错误原因,以人类可读的方式打印到标准错误流(stderr)。
1 2 3 4 5 6 7 | // 所需头文件
#include <stdio.h>
// 函数原型
void perror(const char *s);
// 参数:s 自定义的提示字符串。若不为NULL,函数会先打印该字符串,后跟一个冒号和空格,再打印错误描述;若为NULL,直接打印错误描述
|
3.4.5.5. strerror函数¶
strerror函数用于将整型的错误码转换为对应的错误描述字符串,适用于自定义日志系统。
1 2 3 4 5 6 7 8 9 10 11 12 | // 所需头文件
#include <string.h>
#include <errno.h>
// 函数原型
char *strerror(int errnum);
// 参数:errnum 错误码,通常直接传入全局变量errno
// 返回值:
// 成功:返回一个指向静态错误描述字符串的指针(char*类型),该字符串由系统管理,无需手动释放
// 失败:若传入的错误码无效,返回一个指向"Unknown error"(或类似描述)的字符串指针
|
3.5. 标准I/O示例¶
3.5.1. 文件操作实验¶
3.5.1.1. 程序源码¶
下面我们使用C标准库进行文件操作实验,如下所示。
1 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 34 35 36 37 38 39 40 41 42 43 44 | #include <stdio.h>
#include <string.h>
//要写入的字符串
const char buf[] = "filesystem_test:Hello World!\n";
//文件描述符
FILE *fp;
char str[100];
int main(void)
{
//创建一个文件
fp = fopen("filesystem_test.txt", "w+");
//正常返回文件指针
//异常返回NULL
if(NULL == fp){
printf("Fail to Open File\n");
return 0;
}
//将buf的内容写入文件
//每次写入1个字节,总长度由strlen给出
fwrite(buf, 1, strlen(buf), fp);
//写入Embedfire
//每次写入1个字节,总长度由strlen给出
fwrite("Embedfire\n", 1, strlen("Embedfire\n"),fp);
//把缓冲区的数据立即写入文件
fflush(fp);
//此时的文件位置指针位于文件的结尾处,使用fseek函数使文件指针回到文件头
fseek(fp, 0, SEEK_SET);
//从文件中读取内容到str中
//每次读取100个字节,读取1次
fread(str, 100, 1, fp);
printf("File content:\n%s \n", str);
fclose(fp);
return 0;
}
|
如果之前有学习过C语言的文件操作,本实验代码非常容易理解, 它的流程就是使用fopen创建文件、使用fwrite写入内容, 使用fflush确保缓冲区的内容写到文件, 然后使用fseek重置文件位置指针,使用fread把文件的内容读出,最后调用fclose关闭文件。
其中的fopen函数调用时使用了参数“w+”,表示每次都创建新的空文件,且带上读权限打开,函数调用后得到文件描述符fp,在它后面的fwrite、fread、fflush等函数都是通过这个fp文件描述符访问该文件的。
3.5.1.2. Makefile说明¶
Makefile是跟工程目录匹配的,本实验仅有一个C文件,且与Makefile处于同级目录。
Makefile内容如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #定义变量
TARGET = stdio
#定义编译器
CC = gcc
#定义头文件的位置()
CFLAGS = -I.
#定义头文件
DEPS =
#定义目标文件
OBJS = $(TARGET).o
#目标文件
$(TARGET): $(OBJS)
$(CC) -o $@ $^ $(CFLAGS)
#*.o文件的生成规则
%.o: %.c $(DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
#伪目标
.PHONY: clean
#make clean清除编译结果
clean:
rm -f $(TARGET) $(TARGET).o
|
3.5.1.3. 编译及测试¶
使用如下步骤进行编译测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #编译程序
make
#执行程序
./stdio
#信息输出如下
File content:
filesystem_test:Hello World!
Embedfire
#查看创建的文件
cat filesystem_test.txt
#信息输出如下
filesystem_test:Hello World!
Embedfire
|
3.5.2. 标准输入输出终端交互实验¶
3.5.2.1. 程序源码¶
下面我们使用C标准库进行标准输入输出终端交互实验,如下所示。
1 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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | #include <stdio.h>
int main() {
// 定义存储用户信息的变量
char name[30];
int age;
// 标准输出打印提示信息,行缓冲遇换行符刷新,实时显示
printf("===== 用户信息录入系统 =====\n");
printf("请输入姓名:");
// 从标准输入读取姓名,限制长度避免缓冲区溢出
fgets(name, sizeof(name), stdin);
// 处理fgets读取的换行符,优化输出效果
for (int i = 0; name[i] != '\0'; i++) {
if (name[i] == '\n') {
name[i] = '\0';
break;
}
}
printf("请输入年龄:");
// 从标准输入读取年龄,接收返回值用于校验
int ret = scanf("%d", &age);
// 错误处理:输入格式错误,通过标准错误流实时输出
if (ret != 1) {
// 标准错误流无缓冲,信息立即打印
fprintf(stderr, "输入错误:年龄必须为合法整数!\n");
return -1;
}
// 年龄合法性校验
if (age < 0 || age > 150) {
fprintf(stderr, "输入错误:年龄超出合法范围(0-150)!\n");
return -1;
}
// 打印最终录入结果
printf("\n===== 录入结果 =====\n");
printf("姓名:%s\n", name);
printf("年龄:%d\n", age);
printf("信息录入成功!\n");
return 0;
}
|
以上基于三大标准流,实现用户信息录入与校验,从键盘读取用户姓名、年龄,通过标准输出打印录入结果,参数异常时通过标准错误流实时输出提示。
3.5.2.2. Makefile说明¶
Makefile与stdio1示例的Makefile一致,此处不重复说明。
3.5.2.3. 编译及测试¶
使用如下步骤进行编译测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #编译程序
make
#执行程序,依次输入姓名和年龄
./stdio
#信息输出如下
===== 用户信息录入系统 =====
请输入姓名:张三
请输入年龄:18
===== 录入结果 =====
姓名:张三
年龄:18
信息录入成功!
|
3.5.3. 二进制文件读写实验¶
3.5.3.1. 程序源码¶
下面我们使用C标准库进行二进制文件读写实验,如下所示。
1 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 34 35 36 37 38 39 40 41 42 | #include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义学生信息结构体,用于存储结构化数据
typedef struct {
char name[20];
int id;
float score;
} Student;
int main() {
// 初始化待存储的学生数据
Student stu1 = {"张三", 1001, 95.5};
Student stu2; // 用于存储读取的学生数据
// 1. 以二进制只写模式打开文件,存储结构体数据
FILE *fp = fopen("student.bin", "wb");
if (fp == NULL) {
perror("文件打开失败");
exit(-1);
}
// 二进制写入结构体数据,1表示写入1个数据单元
fwrite(&stu1, sizeof(Student), 1, fp);
printf("学生信息二进制写入成功!\n");
fclose(fp); // 关闭写流
// 2. 以二进制只读模式打开文件,读取结构体数据
fp = fopen("student.bin", "rb");
if (fp == NULL) {
perror("文件读取失败");
exit(-1);
}
// 二进制读取数据,存入stu2变量
fread(&stu2, sizeof(Student), 1, fp);
// 打印解析后的结构化数据
printf("===== 学生信息读取结果 =====\n");
printf("学号:%d\n姓名:%s\n成绩:%.1f\n", stu2.id, stu2.name, stu2.score);
fclose(fp);
return 0;
}
|
以上基于fread、fwrite二进制读写函数,实现结构体数据的文件存储与读取,解决结构化数据持久化问题。
3.5.3.2. Makefile说明¶
Makefile与stdio1示例的Makefile一致,此处不重复说明。
3.5.3.3. 编译及测试¶
使用如下步骤进行编译测试:
1 2 3 4 5 6 7 8 9 10 11 12 | #编译程序
make
#执行程序
./stdio
#信息输出如下
学生信息二进制写入成功!
===== 学生信息读取结果 =====
学号:1001
姓名:张三
成绩:95.5
|
3.6. 如何决择¶
标准I/O库与底层系统调用的区别如下:
对比维度 |
标准I/O库 |
底层系统调用 |
|---|---|---|
依赖关系 |
依赖C标准库,跨平台通用 |
依赖操作系统内核,平台专属 |
缓冲区 |
自带用户态缓冲区,效率更高 |
无用户态缓冲区,直接操作内核 |
操作单位 |
以流(FILE*)为操作单位 |
以文件描述符(int)为操作单位 |
适用场景 |
常规文件读写、数据交互、跨平台程序 |
底层开发、高性能I/O、内核相关程序 |
既然C标准库和系统调用都能够操作文件,那么应该选择哪种操作呢?考虑的因素如下:
使用系统调用会影响系统的性能。执行系统调用时,Linux需要从用户态切换至内核态, 执行完毕再返回用户代码,所以减少系统调用能减少这方面的开销。 如库函数写入数据的文件操作fwrite最后也是执行了write系统调用, 如果是写少量数据的话,直接执行write可能会更高效,但如果是频繁的写入操作, 由于fwrite的缓冲区可以减少调用write的次数,这种情况下使用fwrite能更节省时间。
硬件本身会限制系统调用本身每次读写数据块的大小。如针对某种存储设备的write函数每次可能必须写4kB的数据 那么当要写入的实际数据小于4kB时,write也只能按4kB写入,浪费了部分空间, 而带缓冲区的fwrite函数面对这种情况,会尽量在满足数据长度要求时才执行系统调用,减少空间开销。
也正是由于库函数带缓冲区,使得我们无法清楚地知道它何时才会真正地把内容写入到硬件上, 所以在需要对硬件进行确定的控制时,我们更倾向于执行系统调用。
