
⭐️在这个怀疑的年代,我们依然需要信仰。
个人主页:YYYing.
⭐️Linux/C++进阶 系列专栏:【从零开始的linux/c++进阶编程】
⭐️ 其他专栏:【linux基础】【数据结构与算法】【从零开始的计算机网络学习】
系列上期内容:【Linux/C++进阶篇(二) 】自动化构建:Makefile/CMake
系列下期内容:暂无
目录
[🎯 原型](#🎯 原型)
[🔍 特殊的FILE指针](#🔍 特殊的FILE指针)
[📖 打开与关闭文件:fopen/fclose](#📖 打开与关闭文件:fopen/fclose)
[📖 关于错误码的问题](#📖 关于错误码的问题)
[📖 单字符读写:fputc/fgetc](#📖 单字符读写:fputc/fgetc)
[📖 字符串读写:fputs/fgets](#📖 字符串读写:fputs/fgets)
[📖 关于标准IO的缓冲区问题](#📖 关于标准IO的缓冲区问题)
[📖 格式化读写:fprintf/fscanf](#📖 格式化读写:fprintf/fscanf)
[📖 格式串转字符串存入字符数组中:sprintf/snprintf](#📖 格式串转字符串存入字符数组中:sprintf/snprintf)
[📖 模块化读写:fread/fwrite](#📖 模块化读写:fread/fwrite)
[📖 关于文件内光标:fseek/ftell/rewind](#📖 关于文件内光标:fseek/ftell/rewind)
[📖 打开与关闭文件:open/close](#📖 打开与关闭文件:open/close)
[📖 读写文件:read/write](#📖 读写文件:read/write)
[📖 关于光标的操作:lseek](#📖 关于光标的操作:lseek)

前言:
为什么需要两种I/O?
在C/C++语言的世界里,输入输出(I/O)操作是我们与外部世界沟通的桥梁。但是你可能不知道,C/C++语言提供了两套完全不同的I/O系统:
-
标准I/O:高级、可移植、带缓冲的接口,使用系统提供的库函数实现
-
文件I/O:低级、直接、无缓冲的系统调用,每进行一次系统调用,进程会从用户空间向内核空间进行一次切换, 当用户空间与内核空间进行切换时,进程就会进入一次挂起状态,从而导致进程执行效率低
-
标准I/O与文件I/O的区别:标准IO相比于文件IO而言,提供了缓冲区,用户可以将数据先放入缓冲区 中,等到缓冲区时机到了后,统一进行一次系统调用,将数据刷入内核空间

那为什么要有两套呢?它们各有什么优缺点?什么时候该用哪一种?今天,我们将彻底揭开它们的面纱。
(本文凡涉及到各函数API均参考linux内核man手册,翻译如有不适请指出,感激不尽)
标准I/O
一、标准I/O的实现原理
标准I/O的核心思想是流 。流是一个抽象的概念,表示数据的流动,可以是文件、终端、网络等。**那我们现在设想我们的程序怎么去对一个文件进行操作呢?**是不是会用到一些接口函数,但程序也不知道我们文件到底在哪,或者说是哪个文件呢?那我们看下图:

二、FILE结构体
FILE结构体是系统提供的用于描述一个文件全部信息的结构体
🎯 原型
cpp
struct FILE{
char * _IO_buf_base; //缓冲区的起始地址
char * _IO_buf_end; //缓冲区终止地址
int _fileno; //文件描述符,用于进行系统调用
};
🔍 特殊的FILE指针
|------------|--------|
| stderr | 标准出错指针 |
| stdin | 标准输入指针 |
| stdout | 标准输出指针 |
这三个指针,全部都是针对于终端文件而言的,当程序启动后,系统默认打开的三个特殊文件指针
三、常用的标准I/O函数
📖 打开与关闭文件:fopen/fclose
|----------|-------------------------------------------------------------|----------------------------|
| 函数原型 | FILE *fopen(const char *path, const char *mode); | int fclose(FILE *fp); |
| 头文件 | stdio.h | stdio.h |
| 功能 | 打开指定的文件 | 关闭指定的文件 |
| 参数说明 | 参数1:表示文件路径,是一个字符串 参数2:打开文件的方式,也是一个字符串,字符串必须以以下的字符开头 | 要关闭的文件指针(由fopen返回的结果) |
| 返回值 | 成功返回打开的文件指针,失败返回NULL并置位错误码 | 成功执行返回0,失败返回EOF并置位错误码 |
cpp
#include"stdio.h" //标准的输入输出头文件
int main(int argc, const char *argv[]){
//1、定义一个文件指针
FILE *fp = NULL;
//以只读的形式打开一个不存在的文件,并将返回结果存入到fp指针中
//fp = fopen("./file.txt", "r");
//此时会报错,原因是以只读的形式打开一个不存在的文件,是不允许的
//以只写的形式打开一个不存在的文件,如果文件不存在就创建一个空的文件,如果文件存在就清空
fp = fopen("./file.txt", "w");
if(fp == NULL){
printf("fopen error\n");
return -1;
}
printf("fopen success\n");
//2、关闭文件
fclose(fp);
return 0;
}
文件模式总结:
|--------|-----------------------------------------------------------------|
| 模式 | 功能 |
| r | 以只读 的形式打开文件,文件光标定位在开头部分 |
| r+ | 以读写 的形式打开文件,文件光标定位在开头部分 |
| w | 以只写 的形式打开文件,如果文件不存在,就创建文件,如果文件存在,就清空,光标定位在开头 |
| w+ | 以读写 的形式打开文件,如果文件不存在,就创建文件,如果文件存在,就清空, 光标定位在开头 |
| a | 以追加(结尾写) 的形式打开文件,如果文件不存在就创建文件,光标定位在文件末尾 |
| a+ | 以读写 的形式打开文件,如果文件不存在就创建文件,如果文件存在,读取操作光 标定位在开头,写操作光标定位在结尾 |
📖 关于错误码的问题
概念:
当内核提供的函数出错后,内核空间会向用户空间反馈一个错误信息,由于错误信息比较多 也比较复杂,系统就给每种不同的错误信息起了一个编号,用一个整数表示,这个整数就是错误码
关于错误码的处理函数:
|----------|----------------------------------|--------------------------------------------|
| 函数原型 | char *strerror(int errnum); | void perror(const char *s); |
| 头文件 | string.h,错误码所在头文件是errno.h | stdio.h |
| 功能 | 将错误码转换为错误信息描述 | 输出当前错误码对应的错误信息 |
| 参数说明 | 错误码 | 提示符号,会原样打印出来,并且会在提示数据后面加上冒号,并输出完后,自动换行 |
| 返回值 | 错误信息字符串描述 | 无返回值 |
演示代码如下:
cpp
#include<stdio.h> //标准的输入输出头文件
#include<errno.h> //错误码所在的头文件
#include<string.h> //字符串处理的头文件
int main(int argc, const char *argv[]){
//1、定义一个文件指针
FILE *fp = NULL;
fp = fopen("./file.txt", "r");
if(fp == NULL){
//打印错误信息,并输出错误码 2
printf("fopen error: %d, errmsg:%s\n", errno, strerror(errno));
//打印当前错误码对应的错误信息
perror("fopen error");
return -1;
}
printf("fopen success\n");
//2、关闭文件
fclose(fp);
}
📖 单字符读写:fputc/fgetc
|----------|-------------------------------------|--------------------------------------|
| 函数原型 | int fgetc(FILE *stream); | int fputc(int c, FILE *stream); |
| 头文件 | stdio.h | stdio.h |
| 功能 | 从指定文件中读取一个字符数据,并以无符号整数的形式返回 | 将指定的字符c写入到stream指向的文件中 |
| 参数说明 | 文件指针 | 文件指针 |
| 返回值 | 成功返回读取的字符对应的无符号整数,失败返回EOF并置位错误码 | 成功返回写入字符对的无符号整数,失败返回EOF并置位错误码 |
我们现在用fgetc和fputc完成两个文件的拷贝工作,实现指令cp的功能: cp srcfile destfile
cpp
#include<stdio.h>
int main(int argc, const char *argv[]){
//判断外部传参的个数是否为 3
if(argc != 3){
printf("input file error\n");
printf("usage:./a.out srcfile destfile\n");
return -1;
}
//以只读的形式打开源文件,以只写的形式打开目标文件
FILE *srcfp = NULL; //源文件文件指针
FILE *destfp = NULL; //目标文件文件指针
if((srcfp = fopen(argv[1], "r")) ==NULL){
perror("srcfile open error");
return -1;
}
if((destfp = fopen(argv[2], "w")) ==NULL){
perror("destfile open error");
return -1;
}
//将源文件中的内容搬运到目标文件中
char ch = 0; //搬运工
while(1){
ch = fgetc(srcfp); //从源文件中读取一个字符
if(ch == EOF){
break; //文件全部读取结束
}
fputc(ch, destfp); //将读取的字符写入到目标文件中
}
//关闭两个文件
fclose(srcfp);
fclose(destfp);
printf("拷贝成功\n");
return 0;
}
📖 字符串读写:fputs/fgets
此处我相信也是部分初学者一开始从终端输入字符串的方法。
|----------|------------------------------------------------------------------------------------|-----------------------------------------------|
| 函数原型 | char *fgets(char *s, int size, FILE *stream); | int fputs(const char *s, FILE *stream); |
| 头文件 | stdio.h | stdio.h |
| 功能 | 从stream指向的文件中最多读取size-1个字符到s容器中,遇到回车或文件结束,会结束一 次读取,并且会将回车放入容器,最后自动加上一个字符串结束标识'\0' | 将指定的字符串,写入到指定的文件中 |
| 参数说明 | 参数1:字符数组容器起始地址 参数2:要读取的字符个数,最多读取size-1个字符 参数3:文件指针 | 参数1:要被写入的字符串 参数2:文件指针 |
| 返回值 | 成功返回容器s的起始地址,失败返回NULL | 成功返回本次写入字符的个数,失败返回EOF |
我们现在再用fputs与fgets完成两个文件的拷贝
cpp
#include<iostream>
#include<stdio.h>
#include<string.h>
int main(int argc, const char *argv[]){
//判断外部是否传入两个文件
if(argc != 3){
printf("inut file error\n");
printf("usage:./a.out srcfile destfile\n");
return -1;
}
//以只读的形式打开源文件,以只写的形式打开目标文件
FILE *srcfp = NULL;
FILE *destfp = NULL;
if((srcfp = fopen(argv[1], "r")) == NULL){
perror("fopen error");
return -1;
}
if((destfp = fopen(argv[2], "w")) == NULL){
perror("fopen error");
return -1;
}
//定义搬运工
char buf[128] = "";
while(1){
char *ptr = fgets(buf, sizeof(buf), srcfp); //将将数据从源文件中读取下来
if(ptr == NULL){
break; //读取结束
}
fputs(buf, destfp); //将从源文件中读取的数据写入到目标文件中
}
//关闭文件
fclose(srcfp);
fclose(destfp);
printf("拷贝成功\n");
return 0;
}
📖 关于标准IO的缓冲区问题
缓存区刷新函数:fflush
|----------|--------------------------------|
| 函数原型 | int fflush(FILE *stream); |
| 头文件 | stdio.h |
| 功能 | 刷新给定的文件指针对应的缓冲区 |
| 参数说明 | 文件指针 |
| 返回值 | 成功返回0,失败返回EOF并置位错误码 |
行缓存
和终端文件相关的缓冲区叫做行缓存,行缓冲区的大小为1024字节,对应的文件指针:stdin、stdout
行缓存的刷新时机:
cpp
#include<iostream>
#include<stdio.h>
int main(int argc, const char *argv[]){
//1、验证缓冲区如果没有达到刷新时机,就不会将数据进行刷新
printf("hello world");
while(1); //阻塞程序不让进程结束
//在终端上打印输出一个hello world,没有到缓冲区的刷新时机,就不会输出数据
//2、当程序结束后,会刷新行缓冲区
printf("hello world");
//3、当遇到换行时,会刷新行缓存
printf("hello world\n");
while(1);
//4、当输入输出发生切换时,也会刷新行缓存
int num = 0;
printf("请输入>>>"); //向标准输出缓冲区中写入一组数据,没有换行符号
scanf("%d", &num);
//5、当关闭行缓存对应的文件指针时,也会刷新行缓存
printf("hello world"); //向标准输出缓冲区中写入数据,没有换行
fclose(stdout); //关闭标准输出指针
while(1);
//6、使用fflush函数手动刷新缓冲区时,行缓存会被刷新
printf("hello world");
fflush(stdout);
while(1);
//7、当缓冲区满了后,会刷新行缓存,行缓存大小:1024字节
for(int i=0; i<1025; i++){
printf("A");
}
while(1);
//防止程序结束
return 0;
}
全缓存
和外界文件相关的缓冲区叫做全缓存,全缓冲区的大小为4096字节,对应的文件指针:fp
全缓存的刷新时机:
cpp
#include<iostream>
#include<stdio.h>
using namespace std;
int main(int argc, const char *argv[]){
//打开一个文件
FILE *fp = NULL;
if((fp = fopen("./aa.txt", "r+")) == NULL){ //以读写的形式打开文件
perror("fopen error");
return -1;
}
//1、当缓冲区刷新时机未到时,不会刷新全缓存
fputs("hello world", fp); //将字符串写入到文件中
while(1);
//2、当遇到换行时,也不会刷新全缓存
fputs("hello world\n", fp); //将字符串写入到文件中
while(1);
//3、当程序结束后,会刷新全缓存
fputs("hello world 你好 星球!\n", fp);
//4、当输入输出发生切换时,会刷新全缓存
fputs("I love China\n", fp); //向文件中输出一个字符串
fgetc(fp); //从文件中读取一个字符,主要是让输入输出发生切换
while(1);
//5、当关闭缓冲区对应的文件指针时,也会刷新全缓存
fputs("上海欢迎你", fp);
fclose(fp); //刷新文件指针
while(1);
//6、当手动刷新缓冲区对应的文件指针时,也会刷新全缓存
fputs("上海欢迎你", fp);
fflush(fp); //刷新文件指针
while(1);
//7、当缓冲区满了后,再向缓冲区中存放数据时会刷新全缓存
for(int i=0; i<4097; i++){
fputc('A', fp); //循环将单字符放入文件中
}
while(1);
return 0;
}
不缓存
和标准出错相关的缓冲区叫不缓存,不缓存的大小为0字节,对应的文件指针:stderr
不缓存的刷新时机:只要放入数据,立马进行刷新
cpp
#include<iostream>
#include<stdio.h>
using namespace std;
int main(int argc, const char *argv[]){
//perror("a"); //向标准出错中放入数据
fputs("A", stderr); //向标准出错缓冲区中写入一个字符A
while(1); //阻塞
return 0;
}
三种缓存区的大小验证
cpp
#include<iostream>
#include<stdio.h>
int main(int argc, const char *argv[]){
//如果缓冲区没有被使用时,求出大小为0,只有被至少使用了一次后,缓冲区的大小就被分配了
printf("行缓存的大小为:%d\n", stdout->_IO_buf_end - stdout->_IO_buf_base);
//行缓存的大小 1024
printf("行缓存的大小为:%d\n", stdout->_IO_buf_end - stdout->_IO_buf_base);
printf("行缓存的大小为:%d\n", stdin->_IO_buf_end - stdin->_IO_buf_base); //0
//大小为0 因为没有使用标准输入
int num = 0;
std::cin >> num;
//使用了标准的输入缓冲区
printf("行缓存的大小为:%d\n", stdin->_IO_buf_end - stdin->_IO_buf_base);
//1024
//验证全缓存的大小为4096
FILE *fp = NULL;
if((fp = fopen("./aa.txt", "r")) == NULL){
perror("fopen error");
return -1;
}
//未使用全缓存时,大小为0
printf("全缓存的大小为:%d\n", fp->_IO_buf_end - fp->_IO_buf_base); //0
//使用一次全缓存
fgetc(fp); //从文件中读取一个字符
printf("全缓存的大小为:%d\n", fp->_IO_buf_end - fp->_IO_buf_base); //4096
//关闭文件
fclose(fp);
//验证不缓存的大小为0
//未使用不缓存时大小为0
printf("不缓存的大小为:%d\n", stderr->_IO_buf_end - stderr->_IO_buf_base); //0、
//使用一次不缓存
perror("error");
printf("不缓存的大小为:%d\n", stderr->_IO_buf_end - stderr->_IO_buf_base); //0
return 0;
}
下图为效果图:

📖 格式化读写:fprintf/fscanf
|----------|----------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|
| 函数原型 | int fprintf(FILE *stream, const char *format, ...); | int fscanf(FILE *stream, const char *format, ...); |
| 头文件 | stdio.h | stdio.h |
| 功能 | 向指定的文件中输出一个格式串 | 从指定的文件中以指定的格式读取数据,放入程序中 |
| 参数说明 | 参数1:文件指针 参数2:格式串,可以包含格式控制符,%d(整数)、%s(字符串)、%f(小数)、%lf(小数) 参数3:可变参数,输出项列表,参数个数由参数2中的格式控制符的个数决定 | 参数1:文件指针 参数2:格式串,可以包含格式控制符,%d(整数)、%s(字符串)、%f(小数)、%lf(小数) 参数3:可变参数,输入项地址列表,参数个数由参数2中的格式控制符的个数决定 |
| 返回值 | 成功返回输出的字符个数,失败返回一个负数 | 成功返回读入的项数,失败返回EOF并置位错误码 |
使用fprintf与fscanf共同实现登录注册功能
cpp
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int do_register(){
char reg_name[20] = "";
char reg_pwd[20] = "";
char name[20] = "";
char pwd[20] = "";
printf("请输入注册账号: ");
fgets(reg_name, sizeof(reg_name), stdin);
reg_name[strlen(reg_name) - 1] = '\0';
printf("请输入密码: ");
fgets(reg_pwd, sizeof(reg_pwd), stdin);
reg_pwd[strlen(reg_pwd) - 1] = '\0';
FILE* rfp = NULL;
if((rfp = fopen("./usr.txt", "a+")) == NULL){
perror("rfp error ");
return -1;
}
while(1){
int res = fscanf(rfp, "%s %s", name, pwd);
if(res == EOF){
break;
}
if(strcmp(name, reg_name) == 0
&& strcmp(pwd, reg_pwd) == 0){
fclose(rfp);
return 2;
}
}
fprintf(rfp, "%s %s\n", reg_name, reg_pwd);
fclose(rfp);
printf("注册成功");
return 1;
}
int do_login(){
char log_name[20] = "";
char log_pwd[20] = "";
char name[20] = "";
char pwd[20] = "";
printf("请输入账号: ");
fgets(log_name, sizeof(log_name), stdin);
log_name[strlen(log_name)-1] = '\0'; //将字符串最后的换行符换成结束符号
printf("请输入登录密码: ");
fgets(log_pwd, sizeof(log_pwd), stdin);
log_pwd[strlen(log_pwd)-1] = '\0';
FILE* lfp = NULL;
if((lfp = fopen("./usr.txt", "r")) == NULL){
perror("lfp error");
return -1;
}
while(1){
int res = fscanf(lfp, "%s %s", name, pwd);
if(res == EOF){
break;
}
if(strcmp(name, log_name) == 0
&& strcmp(pwd, log_pwd) == 0){
printf("登录成功");
fclose(lfp);
return 1;
}
}
fclose(lfp);
printf("登录失败,请重新登录");
return 0;
}
int main(int argc, const char *argv[]){
int menu = 0;
while(1){
system("clear"); // 创建子进程调用终端指令
printf("\t\t======登录界面=========\n");
printf("\t\t======1、注册==========\n");
printf("\t\t======2、登录==========\n");
printf("\t\t======0、退出==========\n");
printf("请输入功能选项:");
scanf("%d", &menu);
getchar();
switch (menu) {
case 1:{
int res = do_register();
if(res == 2){
printf("此账号已注册过,请重新选择登录界面\n");
}
break;
}
case 2:{
int res = do_login();
if(res == 1){
printf("登录成功后的界面\n");
//此处省略一万行程序代码
}
}
break;
case 0: exit(EXIT_SUCCESS);// 退出进程
default:
printf("输入的数字有误,重新检查\n");
}
printf("请输入任意键按回车清屏\n");
while(getchar() != '\n');
}
return 0;
}
📖 格式串转字符串存入字符数组中:sprintf/snprintf
我们目前所学的函数中的printf是向终端打印一个格式串,而fprintf是向外部文件中打印一个格式,但有时候,想要将多个不同数据类型的数据,组成一个字符串放入字符数组中,此时我们就可以使用 sprintf 或 snprintf
|----------|--------------------------------------------------------|-----------------------------------------------------------------------------------|
| 函数原型 | int sprintf(char *str, const char *format, ...); | int snprintf(char *str, size_t size, const char *format, ...); |
| 头文件 | stdio.h | stdio.h |
| 功能 | 将指定的格式串转换为字符串,放入字符数组中 | 将格式串中最多size-1个字符转换为字符串,存放到字符数组中 |
| 参数说明 | 参数1:字符数组的起始地址 参数2:格式串,可以包含多个格式控制符 参数3:可变参数 | 参数1:字符数组的起始地址 参数2:要转换的字符个数,最多为size-1 参数3:格式串,可以包含多个格式控制符 参数4:可变参数 |
| 返回值 | 成功返回转换的字符个数,失败返回EOF | 如果转换的字符小于size时,返回值就是成功转换字符的个数,如果大于size则只转换 size-1个字符,返回值就是size,失败返回EOF |
看功能我们也能明白,对于前者函数而言,用一个小的容器去存储一个大的转换后的字符时,会出现指针越界的段错误,为 了安全起见引入了snprintf。
cpp
#include<iostream>
#include<cstdio>
#include<cstring>
int main(int argc, const char *argv[]){
char buf[128] = "";
// 将多种数据类型的数据连接成字符串放入字符数组中
// sprintf(buf, 128, "%s %d %lf", "张三", 1001, 99.5);
//安全起见,我们使用snprintf
snprintf(buf, sizeof(buf), "%s %d %lf", "张三", 1001, 99.5);
// 输出转换后的字符串
printf("buf = %s\n", buf);
return 0;
}
📖 模块化读写:fread/fwrite
|----------|------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| 函数原型 | 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); |
| 头文件 | stdio.h | stdio.h |
| 功能 | 从stream指向的文件中,读取nmemb项数据,每一项的大小为size,将整个结果放入ptr指向的容器中 | 向stream指向的文件中写入nmemb项数据,每一项的大小为size,数据的起始地址为ptr |
| 参数说明 | 参数1:容器指针,是一个void*类型,表示可以存储任意类型的数据 参数2:要读取数据每一项的大小 参数3:要读取数据的项数 参数4:文件指针 | 参数1:要写入数据的起始地址 参数2:每一项的大小 参数3:要写入的总项数 参数4:文件指针 |
| 返回值 | 成功返回nmemb,就是成功读取的项数,失败返回小于项数的值,或者是0 | 成功返回写入的项数,失败返回小于项数的值,或者是0 |
结构体的读写:
cpp
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
//定义一个学生信息
class Stu
{
public:
char name[20];
int age;
double score;
};
int main(int argc, const char *argv[]){
//定义文件指针,以只写的形式打开文件
FILE *fp = NULL;
if((fp = fopen("./test.txt", "w")) == NULL){
perror("fopen error");
return -1;
}
//定义三个学生
Stu s[3] = {{"张三",18, 98}, \
{"李四",20, 88}, \
{"王五",16, 95}};
//将三个学生信息写入文件中
fwrite(s, sizeof(Stu), 3, fp);
//关闭文件
fclose(fp);
//再次以只读的形式打开文件
if((fp = fopen("./test.txt", "r")) == NULL){
perror("fopen error");
return -1;
}
//定义一个对象,接收读取的结果
Stu temp;
//从文件中读取一个学生的信息
fread(&temp, sizeof(Stu), 1, fp);
//将读取的数据展示出来
printf("name:%s, age:%d, score:%.2lf\n", temp.name, temp.age, temp.score);
//关闭文件
fclose(fp);
return 0;
}
📖 关于文件内光标:fseek/ftell/rewind
|--------------------------------------------------------|---------|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------|
| 函数原型 | 头文件 | 功能 | 参数说明 | 返回值 |
| int fseek(FILE *stream, long offset, int whence); | stdio.h | 移动文件光标位置,将光标从指定位置处进行前后偏移 | 参数1:文件指针 参数2:偏移量 >0 :表示从指定位置向后偏移n个字节 <0:表示从指定位置向前偏移n个字节 =0:在指定位置处不偏移 参数3:偏移的起始位置 SEEK_SET:文件起始位置 SEEK_CUR:文件指针当前位置 SEEK_END:文件结束位置 | 成功返回0,失败返回-1并置位错误码 |
| long ftell(FILE *stream); | stdio.h | 获取文件指针当前的偏移量 | 文件指针 | 成功返回文件指针所在的位置,失败返回-1并置位错误码 |
| void rewind(FILE *stream); | stdio.h | 将文件光标定位在开头:fseek(fp, 0, SEEK_SET); | 文件指针 | 无返回值 |
代码演示:
cpp
#include<iostream>
#include<cstdio>
#include<cstring>
//定义一个学生信息
class Stu
{
public:
char name[20];
int age;
double score;
};
int main(int argc, const char *argv[]){
//定义文件指针,以只写的形式打开文件
FILE *fp = NULL;
if((fp = fopen("./test.txt", "w+")) == NULL){
perror("fopen error");
return -1;
}
//定义三个学生
Stu s[3] = {{"张三",18, 98}, \
{"李四",20, 88}, \
{"王五",16, 95}};
//将三个学生信息写入文件中
fwrite(s, sizeof(Stu), 3, fp);
//求出文件的大小
printf("此时文件的大小为:%ld\n", ftell(fp));
//将光标移动到开头位置
//fseek(fp, 0, SEEK_SET);
//将光标直接定位到第二个学生信息前,但是此时光标在最后
//fseek(fp, sizeof(Stu), SEEK_SET);
//文件大小
//将光标从开头后移一个学生空间的内容
fseek(fp, -sizeof(Stu)*2,SEEK_CUR);
//定义一个对象,接收读取的结果
Stu temp;
//从文件中读取一个学生的信息
fread(&temp, sizeof(Stu), 1, fp);
//将光标从当前位置向前偏移两个学生空间的内容
//将读取的数据展示出来
printf("name:%s, age:%d, score:%.2lf\n", temp.name, temp.age, temp.score);
//关闭文件
fclose(fp);
return 0;
}
文件I/O
就是通过系统调用(内核提供的函数)实现,只要我们使用文件IO接口,那么进程就会从用户空间向内核空间进行一次切换。标准IO的实现中,也是在内部调用了文件IO操作。该操作效率较低,因为没有缓冲区的概念。文件IO常用的操作:open、close、read、write、lseek
当然,我们文件I/O也得用一个标识去指向我们指定文件,此处我们指定文件的标识就直接是我们标准I/O的FILE结构体里的文件描述符了。
一、文件描述符
🥇 文件描述符的本质是一个大于等于0的整数,在使用open函数打开文件时,就会产生一个用于操作文件的句柄,这就是文件描述符
🥈 在一个进程中,能够打开的文件描述符是有限制的,一般是1024个,[0,1023],可以通过指令 ulimit -a进行查看,如果要更改这个限制,可以通过指令 ulimit -n 数字进行更改
🥉 文件描述符的使用原则一般是最小未分配原则
🏅 特殊的文件描述符:0、1、2,这三个文件描述符在一个进程启动时就默认被打开了,分别表示标准输入、标准输出、标准错误
cpp
#include<iostream>
#include<stdio.h>
int main(int argc, const char *argv[]){
//分别输出标准输入、标准输出、标准出错文件指针对应的文件描述符
printf("stdin->_fileno = %d\n", stdin->_fileno); // 0
printf("stdout->_fileno = %d\n", stdout->_fileno); // 1
printf("stderr->_fileno = %d\n", stderr->_fileno); // 2
return 0;
}
二、常用的文件I/O函数
📖 打开与关闭文件:open/close
|----------|------------------------------------------------------------------------------------------------------|------------------------|
| 函数原型 | int open(const char *pathname, int flags, mode_t mode); | int close(int fd); |
| 头文件 | sys/types.h sys/stat.h fcntl.h | unistd.h |
| 功能 | 打开或创建一个文件,并返回该文件的文件描述符,返回文件描述符的规则是最小未分配原则 | 关闭文件描述符对应的文件 |
| 参数说明 | 参数1:要打开的文件文件路径 参数2:打开模式 参数3:如果参数2中的flags中有O_CREAT时,表示创建新文件,参数3就必须给定,表示新创建的 文件的权限 | 文件描述符 |
| 返回值 | 成功返回打开文件的文件描述符,失败返回-1并置位错误码 | 成功返回0,失败返回-1并置位错误码 |
值得一提的是,这块的参数2与参数3其实是很有说法的:
在参数2中以下三种方式必须选其一:
O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)
这三种打开方式同时可以有零个或多个,跟上述的方式一起用位或运算连接到一起:
🥇**O_CREAT:**用于创建文件,如果文件不存在,则创建文件,如果文件存在,则打开文件,如果 flag中包含了该模式,则函数的第三个参数必须要加上
🥈**O_APPEND:**以追加的形式打开文件,光标定位在结尾
🥉**O_TRUNC:**清空文件
🏅O_EXCL: 常跟O_CREAT一起使用,确保本次要创建一个新文件,如果文件已经存在,则open 函数报错
下图就是对应着我们之前fopen中模式的选择而搭配出来的flags
而对于参数 3 如果当前参数给定了创建的文件权限,最终的结果也不一定是参数3的值,因为系统会用你给定的参数3的值,与系统的umask取反的值进行位与运算后,才是最终创建文件的权限(mode & ~umask)当前终端的umask的值,可以通过指令 umask 来查看,一般默认为为 0022,表示当前进程所在的组中对该文件没有写权限,其他用户对该文件也没有写权限
当前终端的umask的值是可以更改的,通过**指令:"umask 数字"**进行更改,这种方式只对 当前终端有效
普通文件的权限一般为:0644,表示当前用户没有可执行权限,当前组中其他用户和其 他组中的用户都只有读权限
目录文件的权限一般为:0755,表示当前用户具有可读、可写、可执行,当前组中其他 用户和其他组中的用户都没有可写权限
代码演示:
cpp
#include<iostream>
#include<cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, const char *argv[]){
//1、定义文件描述符,对于文件IO而言,句柄就是文件描述符
int fd = -1;
//以只读的形式创建文件,如果文件不存在则创建文件
//如果创建文件时没有给权限,则该文件的权限是随机权限
//如果创建文件时,给定了文件的权限,则文件最终的权限是 给定的 mode&~umask
if((fd = open("./tt.txt", O_WRONLY|O_CREAT, 0644)) == -1)
{
perror("open error");
return -1;
}
printf("open success fd = %d\n", fd); //3,由于0、1、2已经被使用,所以该数为3
//关闭文件
close(fd);
return 0;
}
📖 读写文件:read/write
|----------|---------------------------------------------------------|------------------------------------------------------------|
| 函数原型 | ssize_t read(int fd, void *buf, size_t count); | ssize_t write(int fd, const void *buf, size_t count); |
| 头文件 | unistd.h | unistd.h |
| 功能 | 从fd文件描述符引用的文件中读取count的字符放入buf对应的容器中 | 将buf容器中的count个数据,写入到fd引用的文件中 |
| 参数说明 | 参数1:已经打开文件对应的文件描述符 参数2:容器的起始地址 参数3:要读取的字符个数 | 参数1:已经打开的文件的文件描述符 参数2:要写入的数据的起始地址 参数3:要写入数据的个数 |
| 返回值 | 成功返回读取的字符个数,这个个数可能会小于count的值,失败返回-1并置位错误码 | 成功返回写入的字符个数,这个个数可能小于count,失败返回-1并置位错误码 |
cpp
#include<iostream>
#include<cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main(int argc, const char *argv[]){
//1、定义文件描述符,对于文件IO而言,句柄就是文件描述符
int fd = -1;
//以只读的形式创建文件,如果文件不存在则创建文件
//如果创建文件时没有给权限,则该文件的权限是随机权限
//如果创建文件时,给定了文件的权限,则文件最终的权限是 给定的 mode&~umask
if((fd = open("./tt.txt", O_WRONLY|O_CREAT, 0644)) == -1){
perror("open error");
return -1;
}
printf("open success fd = %d\n", fd); //由于0、1、2已经被使用,所以该数为3
//对数据进行读写操作
char wbuf[128] = "hello world";
//将上述字符串写入文件中
write(fd, wbuf, strlen(wbuf));
//关闭文件
close(fd);
//关闭fd引用的文件
//再次以只读的形式打开文件,此时参数2中不需要使用O_CREAT,那么第三个参数也不需要了
if((fd = open("./tt.txt", O_RDONLY)) == -1){
perror("open error");
return -1;
}
printf("open success fd = %d\n", fd); // 3
//定义接收数据容器
char rbuf[5] = "";
int res = read(fd, rbuf, sizeof(rbuf)); //从文件中读取数据放入rbuf中
write(1, rbuf, res); //向1号文件描述符中写入数据,之前读取多少,现在写入多少
//关闭文件
close(fd);
return 0;
}
📖 关于光标的操作:lseek
|----------|------------------------------------------------------------------------------------------------------------|
| 函数原型 | off_t lseek(int fd, off_t offset, int whence); |
| 头文件 | sys/types.h unistd.h |
| 功能 | 移动光标的位置,并返回光标现在所在的位置 |
| 参数说明 | 参数1:文件描述符 参数2:偏移量 >0:表示从指定位置向后偏移n个字节 <0:表示从指定位置向前偏移n个字节 =0:在指定位置处不偏移 参数3:偏移的起始位置 |
| 返回值 | 光标现在所在的位置 |
注意:lseek的效果 = fseek + ftell
cpp
#include<iostream>
#include<cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, const char *argv[]){
//1、定义文件描述符,对于文件IO而言,句柄就是文件描述符
int fd = -1;
//以读写的形式创建文件,如果文件不存在则创建文件,如果文件存在则清空文件
//如果创建文件时没有给权限,则该文件的权限是随机权限
//如果创建文件时,给定了文件的权限,则文件最终的权限是 给定的 mode&~umask
if((fd = open("./tt.txt", O_RDWR|O_CREAT|O_TRUNC, 0644)) == -1){
perror("open error");
return -1;
}
printf("open success fd = %d\n", fd); //3,由于0、1、2已经被使用,所以该数为3
//对数据进行读写操作
char wbuf[128] = "hello world";
//将上述字符串写入文件中
write(fd, wbuf, strlen(wbuf));
//此时文件光标是在文件的末尾位置
//需求是:读取文件中的 world
//lseek(fd, 6, SEEK_SET); //从文件开头位置向后偏移6个字节
lseek(fd, -5, SEEK_END); //从文件结束位置向前偏移5个字节
//定义接收数据容器
char rbuf[5] = "";
int res = read(fd, rbuf, sizeof(rbuf)); //从文件中读取数据放入rbuf中
write(1, rbuf, res); //向1号文件描述符中写入数据,之前读取多少,现在写入多少
//关闭文件
close(fd);
return 0;
}
标准I/O与文件I/O的对比
|------------|----------------------|------------------------|
| 特性 | 标准I/O | 文件I/O |
| 抽象级别 | 高(流) | 低(文件描述符) |
| 缓冲 | 自动缓冲 | 无缓冲(可手动设置) |
| 可移植性 | 高(ANSI C标准) | 低(POSIX标准,Unix-like系统) |
| 性能 | 小数据量好(缓冲减少系统调用) | 大数据量或特殊场景好 |
| 错误处理 | 通过返回值+errno | 通过返回值+errno |
| 文件定位 | fseek, ftell, rewind | lseek |
| 格式化I/O | 支持(printf, scanf系列) | 不支持 |
| 二进制I/O | fread/fwrite | read/write |
何时使用标准I/O?
-
常规文件操作:大多数情况下,标准I/O是更好的选择
-
需要格式化输入输出:printf/scanf系列函数非常方便
-
可移植性要求高:需要在不同平台运行
-
小量频繁的I/O:缓冲机制减少系统调用开销
-
简单的文本处理:行缓冲模式对文本文件友好
何时使用文件I/O?
-
需要底层控制:如设置文件描述符标志
-
特殊文件类型:设备文件、管道、socket
-
需要文件锁定:flock或fcntl锁定
-
需要获取文件元数据(状态):fstat
-
高性能场景:大数据量、直接I/O、内存映射
文件锁定与获取文件状态的API感兴趣的可以继续参照man手册玩玩,此处就不进行过多赘述了**。**
结语
那么关于IO基础部分的讲解就到这里了。
我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。
**无限进步,**我们下次再见。
---⭐️ 封面自取 ⭐️---

