标准IO
标准IO
一、文件的打开与关闭
不管用系统IO函数还是标准IO函数,操作文件的第一步,都是"打开(open/fopen)"文件,需要注意:
系统IO:打开文件得到的是一个整数,称为文件描述符。
标准IO:打开文件得到的是一个指针,称为文件指针。
文件指针指向结构体 FILE,该结构体内部包含了文件描述符,如下图:
二、基础API接口
① 打开文件 fopen
函数原型:
FILE *fopen(const char * pathname, const char * mode);
参数分析:
pathname --> 需要打开的目标文件的完整路径+名字
mode --> 打开的标记
r 只读打开,并读写位置默认为文件头
r+ 可读【可写】打开,并读写位置默认为文件头
w 只写方式打开,如果不存在则创建,如果存在则清空文件。
w+ 【必须得到一个全新“空白”的文件】可读可写方式打开,如果不存在则创建,如果存在则清空。
a 【追加方式】只写方式打开,如果不存在则创建,如果存在则末尾追加
a+ 【追加方式】可读可写方式打开,如果不存在则创建,如果存在则末尾追加
返回值:
成功 返回一个文件流指针
失败 返回NULL 并设置错误码
② 读写文件 fread fwrite
按数据块读写
函数原型:
size_t fread ( void * ptr ,size_t size, size_t nmemb , FILE *restrict stream);
size_t fwrite(const void * ptr ,size_t size, size_t nmemb , FILE *restrict stream);
参数分析:
ptr -->
把读取到的数据存入该内存空间【可读可写】
需要写入的数据的内存空间【只读即可】
size --> 需要读写的数据的尺寸
nmemb --> 需要读写的数据块是多少
stream --> 需要读写的目标文件流指针
返回值:
成功 返回实际读写的数据块
失败 0
按字节读写文本文件
- 关键点:
fgetc()与getc()功能完全一样,区别是fgetc是函数,而getc是宏。fputc()与putc()功能完全一样,区别是fputc是函数,而putc是宏。getchar()和putchar()只能针对键盘输入和屏幕输出,不能指定别的文件。
按行读写文本文件
关键点:
- 对于读操作而言,返回
EOF(-1)意味着读操作失败,这有两种情况:- i.如果
feof(fp)为真,此时意味着读到了文件末尾,没有数据可读了。 - ii.如果
ferror(fp)为真,此时意味着遇到了错误。
- i.如果
- 读操作函数接口的返回值是
int,而不是char,原因是当读操作失败是返回的EOF的数值是 -1,而char型数据可能无法表达 -1。
fgets() 和 gets() 都是按行读取文件数据,他们的区别是:
fgets()可以读取指定的任意文件,而gets()只能从键盘读取。fgets()有内存边界判断,而gets()没有,因此后者是不安全的,不建议使用。fgets()在任何情形下都按原样读取数据,会读取\n,但gets()会自动去除数据末尾的‘\n’
fputs() 和 puts() 都是按行将数据写入文件,他们的区别是:
fputs()可以将数据写入指定的任意文件,而puts()只能将数据输出到屏幕。fputs()在任何情形下都按原样写入数据,但puts()会自动给写入数据的末尾加上‘\n’
按指定格式读写文本文件
- 注意:
1.fprintf( )不仅可以像printf( )一样向标准输出设备输出信息,也可以向由stream指定的任何有相应权限的文件写入数据。
2.sprintf()和snprintf()都是向一块自定义缓冲区写入数据,不同的是后者第二个参数提供了这块缓冲区的大小,避免缓冲区溢出,因此应尽量使用后者,放弃使用前者。
3.fscanf( )不仅可以像scanf( )一样从标准输入设备读入信息,也可以从由stream指定的任何有相应权限的文件读入数据。
4.sscanf( )从一块由s指定的自定义缓冲区中读入数据。
5.这些函数的读写都是带格式的,格式由下表规定:
③ 设置偏移量
函数原型
int fseek(FILE *stream, long offset, int whence);
参数分析:
stream --> 指明具体的文件流指针
offset --> 设置具体的偏移量
whence --> 如何偏移
SEEK_SET 设置读写位置到offset 的位置(从0开始)
SEEK_CUR 以当前位置开始往前或往后偏移 (offset 可以是正数,也可以是负数)
SEEK_END 把偏移量从文件末尾开始设置 一般把offset设置为负数
返回值:
成功 返回0
失败 返回-1 并设置错误码
④ 获取当前的文件读写位置
long ftell(FILE *stream);
⑤ 关闭文件
int fclose(FILE *stream);
⑥ 示例代码
// 标准 I/O 示例代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 示例1:fopen的各种打开模式
void example1() {
FILE *fp;
// r 模式:只读打开
fp = fopen("test.txt", "r");
if (fp == NULL) {
printf("r模式:文件不存在或打开失败\n");
} else {
printf("r模式:文件打开成功\n");
fclose(fp);
}
// w 模式:只写打开,不存在则创建,存在则清空
fp = fopen("test.txt", "w");
if (fp == NULL) {
printf("w模式:打开失败\n");
} else {
printf("w模式:文件打开/创建成功\n");
fclose(fp);
}
// a 模式:追加方式打开
fp = fopen("test.txt", "a");
if (fp == NULL) {
printf("a模式:打开失败\n");
} else {
printf("a模式:文件打开成功\n");
fclose(fp);
}
// r+ 模式:可读可写打开
fp = fopen("test.txt", "r+");
if (fp == NULL) {
printf("r+模式:打开失败\n");
} else {
printf("r+模式:文件打开成功\n");
fclose(fp);
}
// w+ 模式:可读可写,清空文件
fp = fopen("test.txt", "w+");
if (fp == NULL) {
printf("w+模式:打开失败\n");
} else {
printf("w+模式:文件打开成功\n");
fclose(fp);
}
// a+ 模式:可读可写,追加方式
fp = fopen("test.txt", "a+");
if (fp == NULL) {
printf("a+模式:打开失败\n");
} else {
printf("a+模式:文件打开成功\n");
fclose(fp);
}
}
// 示例2:按数据块读写
void example2() {
FILE *fp;
struct Student {
char name[20];
int age;
float score;
};
struct Student students[3] = {
{"张三", 20, 85.5},
{"李四", 21, 92.0},
{"王五", 19, 78.5}
};
struct Student read_students[3];
// 写入数据块
fp = fopen("students.dat", "wb");
if (fp == NULL) {
printf("打开文件失败\n");
return;
}
size_t written = fwrite(students, sizeof(struct Student), 3, fp);
printf("写入了 %zu 个学生数据\n", written);
fclose(fp);
// 读取数据块
fp = fopen("students.dat", "rb");
if (fp == NULL) {
printf("打开文件失败\n");
return;
}
size_t read = fread(read_students, sizeof(struct Student), 3, fp);
printf("读取了 %zu 个学生数据\n", read);
for (int i = 0; i < read; i++) {
printf("学生%d: 姓名=%s, 年龄=%d, 成绩=%.1f\n",
i+1, read_students[i].name, read_students[i].age, read_students[i].score);
}
fclose(fp);
}
// 示例3:按字节读写
void example3() {
FILE *fp;
char ch;
// 写入字节数据
fp = fopen("byte_test.txt", "w");
if (fp == NULL) {
printf("打开文件失败\n");
return;
}
for (ch = 'A'; ch <= 'Z'; ch++) {
fputc(ch, fp);
}
fputc('\n', fp);
fclose(fp);
// 读取字节数据
fp = fopen("byte_test.txt", "r");
if (fp == NULL) {
printf("打开文件失败\n");
return;
}
printf("文件内容: ");
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
fclose(fp);
}
// 示例4:按行读写
void example4() {
FILE *fp;
char buffer[256];
// 写入行数据
fp = fopen("line_test.txt", "w");
if (fp == NULL) {
printf("打开文件失败\n");
return;
}
fputs("这是第一行\n", fp);
fputs("这是第二行\n", fp);
fputs("这是第三行\n", fp);
fclose(fp);
// 读取行数据
fp = fopen("line_test.txt", "r");
if (fp == NULL) {
printf("打开文件失败\n");
return;
}
printf("文件内容:\n");
int line_num = 1;
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("第%d行: %s", line_num++, buffer);
}
fclose(fp);
}
// 示例5:格式化读写
void example5() {
FILE *fp;
char name[20];
int age;
float salary;
// 格式化写入
fp = fopen("format_test.txt", "w");
if (fp == NULL) {
printf("打开文件失败\n");
return;
}
fprintf(fp, "姓名: %s, 年龄: %d, 工资: %.2f\n", "张三", 25, 8000.50);
fprintf(fp, "姓名: %s, 年龄: %d, 工资: %.2f\n", "李四", 30, 12000.75);
fclose(fp);
// 格式化读取
fp = fopen("format_test.txt", "r");
if (fp == NULL) {
printf("打开文件失败\n");
return;
}
printf("格式化读取:\n");
while (fscanf(fp, "姓名: %s, 年龄: %d, 工资: %f", name, &age, &salary) == 3) {
printf("姓名: %s, 年龄: %d, 工资: %.2f\n", name, age, salary);
// 跳过换行符
fgetc(fp);
}
fclose(fp);
}
// 示例6:文件偏移操作
void example6() {
FILE *fp;
char buffer[100];
fp = fopen("seek_test.txt", "w+");
if (fp == NULL) {
printf("打开文件失败\n");
return;
}
// 写入测试数据
fprintf(fp, "0123456789ABCDEFGHIJ");
// 获取当前位置
long pos = ftell(fp);
printf("写入后当前位置: %ld\n", pos);
// 回到文件开头
fseek(fp, 0, SEEK_SET);
printf("回到开头后位置: %ld\n", ftell(fp));
// 读取前10个字符
fread(buffer, 1, 10, fp);
buffer[10] = '\0';
printf("前10个字符: %s\n", buffer);
printf("读取后位置: %ld\n", ftell(fp));
// 从当前位置向后移动5字节
fseek(fp, 5, SEEK_CUR);
printf("向后移动5字节后位置: %ld\n", ftell(fp));
// 从文件末尾向前移动3字节
fseek(fp, -3, SEEK_END);
printf("从末尾向前3字节位置: %ld\n", ftell(fp));
// 读取当前位置的字符
char ch = fgetc(fp);
printf("当前位置的字符: %c\n", ch);
fclose(fp);
}
// 示例7:错误处理
void example7() {
FILE *fp;
fp = fopen("nonexistent.txt", "r");
if (fp == NULL) {
perror("打开文件失败");
printf("错误号: %d\n", errno);
return;
}
// 模拟读取到文件末尾
fgetc(fp); // 读取一个字符
while (!feof(fp)) {
fgetc(fp);
}
if (feof(fp)) {
printf("已到达文件末尾\n");
}
if (ferror(fp)) {
printf("文件操作发生错误\n");
}
fclose(fp);
}
// 示例8:sprintf和snprintf
void example8() {
char buffer1[50];
char buffer2[50];
int year = 2024;
char month[] = "March";
int day = 15;
// sprintf - 不安全的版本
sprintf(buffer1, "今天是 %s %d, %d", month, day, year);
printf("sprintf: %s\n", buffer1);
// snprintf - 安全的版本
snprintf(buffer2, sizeof(buffer2), "今天是 %s %d, %d", month, day, year);
printf("snprintf: %s\n", buffer2);
// 测试缓冲区溢出保护
char small_buf[10];
int needed = snprintf(small_buf, sizeof(small_buf), "这是一个很长的字符串");
printf("需要的缓冲区大小: %d, 实际写入: %s\n", needed, small_buf);
}
int main() {
printf("=== 示例1: fopen各种打开模式 ===\n");
example1();
printf("\n=== 示例2: 按数据块读写 ===\n");
example2();
printf("\n=== 示例3: 按字节读写 ===\n");
example3();
printf("\n=== 示例4: 按行读写 ===\n");
example4();
printf("\n=== 示例5: 格式化读写 ===\n");
example5();
printf("\n=== 示例6: 文件偏移操作 ===\n");
example6();
printf("\n=== 示例7: 错误处理 ===\n");
example7();
printf("\n=== 示例8: sprintf和snprintf ===\n");
example8();
return 0;
}
04 标准IO的缓冲区(课堂原件,未改动)
> 以下所有内容均与原 .docx 保持一致,仅追加「个人拓展」章节,可直接复制使用。
三、标准IO缓冲区
标准IO实际上是系统IO的封装,这种封装体现如下图所示,fopen()函数将调用open()得到的文件描述符填入结构体 FILE 中,并为文件分配缓冲区、设置缓冲区类型,最后给用户返回指向 FILE 的指针 fp,称为文件指针:
如上图所示,每当使用标准IO的写操作函数,试图将数据写入文件 a.txt 时,数据都会流过缓冲区,然后再在适当的时刻冲洗(或称刷新,flush)到内核,最后才真正写入设备文件。
按照缓冲区按照什么时候冲洗数据到内核,可以将缓冲区分成以下三类:
不缓冲类型(_IONBF)
一旦有数据,立刻将数据冲洗到文件(同步到外围设备 系统IO)
全缓冲类型(_IOFBF)
- 一旦填满缓冲区,立刻将数据冲洗到文件(同步到外围设备)
- 程序正常退出时,立刻将数据冲洗到文件
- 如果程序段错误或
Ctrl + C强制终止,则数据会留在缓冲区无法同步到文件中(数据丢失) - 遇到 fflush() 强制冲洗时(强制同步),立刻将数据冲洗到文件
- 关闭文件时,立刻将数据冲洗到文件
- 读取文件内容时,立刻将数据冲洗到文件
- 改变缓冲区类型时,立刻将数据冲洗到文件(
setbuf/setvbuf...)
行缓冲类型(_IOLBF)
同全缓冲类型,一旦遇到\n时,立刻将数据冲洗到文件
关键点摘要
- 缓冲(buffer)都是针对写操作而言的,缓冲的存在是为了提高写效率
- 对于标准输出而言,默认是行缓冲的
- 对于标准出错而言,默认是不缓冲的
- 对于普通文件而言,默认都是全缓冲类型
- 滞留在缓冲区中的数据有时被称为脏数据(dirty data)
- 脏数据的存在代表程序操作的结果与文件真实状态不一致,若未正常冲洗这些脏数据就退出程序(异常退出:崩溃、Ctrl+C)则有可能会造成数据丢失
- 这三种缓冲类型,可以通过函数 setbuf()/setbuf() 来修改
如何设置缓冲区类型
函数原型:
基础的万能类型
int setvbuf (FILE *restrict stream, char buf[restrict .size],int mode, size_t size);
可以便捷设置为无缓冲buf 设置为NULL
void setbuf (FILE *restrict stream, char *restrict buf );
保持缓冲区类型不变,但是需要调成缓冲区的地址
void setbuffer (FILE *restrict stream, char buf[restrict .size], size_t size );
保持缓冲区地址不变,但是改变缓冲区类型为行缓冲
void setlinebuf(FILE *stream );
参数分析:
stream --> 需要设置缓冲区的目标文件
buf --> 指定用户的自定义缓冲区地址
mode --> 缓冲区类型
_IONBF unbuffered 无缓冲
_IOLBF line buffered 行缓冲
_IOFBF fully buffered 全缓冲
size --> 缓冲区的具体尺寸
示例:
// 把现有的缓冲区类型设置为行行冲
setvbuf(stream, NULL, _IOLBF, 0);
setlinebuf( stream );
// 设置无缓冲
setvbuf(fp, NULL, _IONBF, 0);

浙公网安备 33010602011771号