
一、标准IO的理解
- IO的理解,I : input , 读, 输入, 接收;O : output , 写, 输出, 发送
- 标准IO库是C语言的标准库。
- 不仅在Linux系统,在很多操作系统上都实现了标准I/O库。
- 标准I/O库由ANSI C标准说明 , 是C语言的标准库
- 标准I/O库处理很多细节,如缓存分配、以优化长度执行I/O等,这样使用户不必关心如何选择合适的块长度。
- 标准I/O在系统调用(文件IO)函数基础上构造的,它便于用户使用。
- 标准I/O库及其头文件stdio.h为底层I/O系统调用提供了一个通用的接口。
1、文件指针
FILE指针:每个被打开的文件,操作系统都在内存中开辟一个区域,用来存放被打开文件的有关信息,这些信息是保存在一个结构体类型的变量中,该结构体类型是由系统定义的,取名为FILE。FILE类型定义如下:

- 标准I/O库的所有操作都是围绕流(stream)来进行的,在标准I/O中,流用FILE *来描述。
- 标准I/O库是由Dennis Ritchie在1975年左右编写的。
2、流(stream)
- 定义:所有的I/O操作仅是简单的从程序移进或者移出,这种字节流,就称为流。
- 分类:文本流/二进制流。
- **文本流定义:在流中处理的数据是以字符出现。**在文本流中,'\n'被转换成换行符LF的ASCII码是0AH。而当输出时,0AH被转换成'\n',数字2001在文本流中的表示方法为 '2' '0' '0' '1',ASCII: 50 48 48 49
- 二进制流定义:流中处理的是二进制序列。 若流中有字符,则用一个字节的二进制。ASCII码表示;若是数字,则用对应的二进制数表示,对'\n'不进行变换。数字2001在二进制流中的表示方法为 00000111 11010001。
3、缓冲文件系统(高级磁盘IO)
- 目的:尽量减少使用read/write的调用。
- 定义:操作系统自动的在内存中为每一个正在使用的文件开辟一个缓冲区,从内存向磁盘输出数据必须先送到内存缓冲区,装满缓冲区在一起送到磁盘中去。从磁盘中读数据,则一次从磁盘文件将一批数据读入到内存缓冲区中,然后再从缓冲区逐个的将数据送到程序的数据区。
- 类比景区的摆渡车
- 分类:全缓存,行缓存,不缓存。
4、非缓冲文件系统 (低级磁盘IO)
- 定义:依靠于操作系统,通过操作系统的功能对文件进行读写,是系统级的输入输出。
- 没有缓存的概念。
- 类比城市的公交车
5、标准I/O (stdin,stdout,stderr)
标准I/O预定义3个流,他们可以自动地为进程所使用。

二、打开流 fopen
1、fopen打开流
cpp
FILE *fopen(const char *pathname, const char *mode);
函数的功能:打开一个文件 , 并返回一个流(文件指针)
函数的参数:【pathname】 : 要打开的文件名 , 例如"/home/linux/1.txt"。【mode】: 打开文件的方式
|------------|-------------------------------------------------------------------|
| 命令 | 解释 |
| b | binary的缩写, 表示以二进制的形式打开文件, 操作的是二进制流 |
| r或rb | 打开只读文件,该文件必须存在,文件不存在报错。 |
| r+或r+b | 打开可读写的文件,该文件必须存在。 |
| w或wb | 打开只写文件,若文件存在则文件长度清为0,即会擦些文件以前内容。若文件不存在则建立该文件。 |
| w+或w+b或wb+ | 打开可读写文件,若文件存在则文件长度清为零,即会擦些文件以前内容。若文件不存在则建立该文件。 |
| a或ab | 以附加的方式打开只写文件。若文件不存在,则会建立该文件, 如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。 |
| a+或a+b或ab+ | 以附加方式打开可读写的文件。若文件不存在,则会建立该文 件,如果文件存在,写入的数据会被加到文件尾后,即文件原先 的内容会被保留。 |
函数返回值:成功返回FILE 类型的指针(内存中创建的结构体首地址)。失败返回NULL指针并设置errno, 设置errno后可以使用perror 打印出错信息
2、实例fopen打开一个文件
源文件fopen_test.c,依旧是使用VS Code进行编程,需要安装下面的几个插件

cpp
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
FILE *fp;
// 程序运行需要2个参数 ./01-fopen filename
if (argc != 2)
{
printf("错误:运行程序时请带入参数(./fopen filename)\n");
exit(-1);
}
// argv[0] = ./01-fopen
// argv[1] = filename
// fp = fopen(argv[1],"r") ; //只读方式打开文件, 文件不存在, 报错
// fp = fopen(argv[1],"r+") ; //可读可写方式打开文件, 文件不存在, 报错
// fp = fopen(argv[1],"w") ; //只写方式打开文件, 文件不存在则创建文件, 文件存在清空文件内容
// fp = fopen(argv[1],"w+") ; //可读可写方式打开文件, 文件不存在则创建文件, 文件存在清空文件内容
// fp = fopen(argv[1],"a") ; //只写方式打开文件, 文件不存在则创建文件, 文件存在不清空文件内容, 光标移动文件末尾
fp = fopen(argv[1], "a+"); // 可读可写方式打开文件, 文件不存在则创建文件, 文件存在不清空文件内容, 光标移动文件末尾
if (fp == NULL)
{
perror("fopen");
exit(-1);
}
return 0;
}

三、关闭流 fclose
cpp
int fclose(FILE *stream);
函数功能:关闭一个已经打开的文件(流)
函数参数:stream : 要关闭文件的流
函数返回值:成功返回: 返回为0;失败返回 : EOF(end of file) , 并设置errno
cpp
#include <stdio.h>
int main(int argc, char const* argv[])
{
FILE* fp = NULL;
int i = 0;
char filename[128];
while(1)
{
sprintf(filename, "file%d", i);
printf("filename: %s\n", filename);
fp = fopen(filename, "w");
if(fp == NULL)
{
perror("fopen");
return -1;
}
i++;
}
return 0;
}
在Ubuntu上一个进程最多可以打开1024个文件,这个程序需要在ubuntu的终端种执行,这个程序不能使用ssh登录并运行,因为ssh的最大打开文件个数比较多。

【分析】最多创建了file0 ... file1020 , 一共是1021个文件, 系统默认给我们打开了3个文件, 分别是stdin, stdout,stderr, 因此一个进程最多可以打开文件的个数为 1024 。
四、读写流 fgetc/fgets/fread
- 调用fopen()成功打开流之后,可在三种不同类型的非格式化I/O中进行选择,对其进行读、写操作:
- **每次一个字符的I/O。**使用fgetc()/fputc()一次读或写一个字符,如果流是带缓存的,则标准I/O函数处理所有缓存。
- **每次一行的I/O。**使用fgets()和fputs()一次读或写一行。每行都以一个新行符终止。当调用fgets()时,应说明能处理的最大行长。
- **直接I/O。**fread()和fwrite()函数支持这种类型的I/O。每次I/O操作读或写某种数量的对象,而每个对象具有指定的长度。这两个函数常用于从二进制文件中读或写一个结构。
1、每次读一个字符 fgetc
cpp
int fgetc(FILE *stream);
函数功能:从已经打开的流(文件指针)中读取一个字符
函数参数:【stream】要读文件的流
函数返回值:成功返回读回来的字符;失败返回EOF(end of file)
2、实例fgetc实现cat
cpp
#include <stdio.h>
int main(int argc, char const* argv[])
{
if(argc != 2)
{
printf("Usage: %s filename\n", argv[0]);
return -1;
}
FILE* fp = fopen(argv[1], "r");//只读方式打开文件, 文件不存在, 报错
if(fp == NULL)
{
perror("fopen");
return -1;
}
char ch;
while((ch = fgetc(fp)) != EOF)//读取文件内容, 直到文件结束
{
printf("%c", ch);
}
fclose(fp);//关闭文件
return 0;
}

3、每次写一个字符 fputc
cpp
int fputc(int c, FILE *stream);
函数功能:把一个字符写入到一个已经打开的流(文件指针)中
函数参数:【c】要写入文件的字符;【stream】要写文件的流
函数返回值:成功返回: 返回写入文件的字符;失败返回 : EOF(end of file)
4、实例fputc和fgetc实现cp
cpp
#include <stdio.h>
int main(int argc, char const* argv[])
{
//argv[0]是文件名
//argv[1]是 src file1
//argv[2]是 dst file2
if(argc != 3)
{
printf("Usage: %s filename\n", argv[0]);
return -1;
}
FILE* fp_src = fopen(argv[1], "r");//只读方式打开文件, 文件不存在, 报错
if(fp_src == NULL)
{
perror("fopen");
return -1;
}
FILE* fp_dst = fopen(argv[2], "w");//只写方式打开文件, 文件不存在则创建,存在则清空
if(fp_dst == NULL)
{
perror("fopen");
return -1;
}
char ch;
while((ch = fgetc(fp_src)) != EOF)//读取文件内容, 直到文件结束
{
fputc(ch, fp_dst);//写入文件
}
fclose(fp_src);
fclose(fp_dst);
return 0;
}

5、每次读一行的字符串 fgets()
cpp
char *fgets(char *s, int size, FILE *stream);
函数功能:从流(文件指针)读取指定长度的字符串到s指定的内存中,从stream中最多读入一个小于size的字符,并将它们存储到指向的内存s中 , 读取在EOF或换行符(\n)之后停止。如果读取了换行符,则将其存储到缓冲区中。使用('\0')存储在缓冲区的最后一个字符之后。fgets遇到\n 停止读入, '\n'会被读取进来, 保存到s中, 最后会加上一个'\0', 做为字符串结束标志 。
函数参数:【s】保存读回来的字符串的内存地址;size : 要读多少个字节;【stream】要读文件的流。
函数返回值:成功返回: 返回保存字符串的内存地址s;失败返回 : 错误时返回NULL, 或者是到达文件末尾时也返回NULL。
6、实例fgets实现cat
cpp
#include <stdio.h>
int main(int argc, char const* argv[])
{
if(argc != 2)
{
printf("Usage: %s filename\n", argv[0]);
return -1;
}
FILE* fp = fopen(argv[1], "r");//只读方式打开文件, 文件不存在, 报错
if(fp == NULL)
{
perror("fopen");
return -1;
}
char buf[1024]={0};
while(fgets(buf,1024,fp) != NULL)//读取文件内容, 直到文件结束
{
printf("%s", buf);
}
fclose(fp);//关闭文件
return 0;
}

7、每次写一行的字符串 fputs()
cpp
int fputs(const char *s, FILE *stream);
函数功能:把s保存的字符串写入到流(文件指针)内
函数参数:【s】要写入流内的字符串;【stream】要写文件的流
函数返回值:成功返回写入流中的字节数, 一个正数;失败返回错误时返回EOF
8、实例使用fgets和fputs实现cp
cpp
#include <stdio.h>
int main(int argc, char const* argv[])
{
//argv[0]是文件名
//argv[1]是 src file1
//argv[2]是 dst file2
if(argc != 3)
{
printf("Usage: %s filename\n", argv[0]);
return -1;
}
FILE* fp_src = fopen(argv[1], "r");//只读方式打开文件, 文件不存在, 报错
if(fp_src == NULL)
{
perror("fopen");
return -1;
}
FILE* fp_dst = fopen(argv[2], "w");//只写方式打开文件, 文件不存在则创建,存在则清空
if(fp_dst == NULL)
{
perror("fopen");
return -1;
}
char buf[1024]={0};
while(fgets(buf,1024,fp_src) != NULL)//读取文件内容, 直到文件结束
{
fputs(buf, fp_dst);//写入文件
}
fclose(fp_src);
fclose(fp_dst);
return 0;
}

9、直接读指定字节数 fread
cpp
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
函数功能:从流(文件指针)中读取指定字节数保存到ptr所指向的内存,可以读文本文件,也可以读二进制文件。fgetc 和fgets 只能读取文本文件 。
函数参数,【ptr】要保存数据的内存地址;【size】每一个元素的字节数;【nmemb】要读取元素的个数;【stream】要读文件的流
函数返回值:成功返回实际读回来的字节数或元素数;失败返回到达文件末尾返回 0,错误时返回 0
10、实例fread实现cat
cpp
#include <stdio.h>
int main(int argc, char const* argv[])
{
if(argc != 2)
{
printf("Usage: %s filename\n", argv[0]);
return -1;
}
FILE* fp = fopen(argv[1], "r");//只读方式打开文件, 文件不存在, 报错
if(fp == NULL)
{
perror("fopen");
return -1;
}
char buf[1024]={0};
while(fread(buf,1,1024,fp) != 0)//读取文件内容, 直到文件结束
{
printf("%s", buf);
}
fclose(fp);//关闭文件
return 0;
}

这个程序有问题, 需要使用fwrite去解决也可以使用清空数组的办法。使用清除数组内容的办法, 不能彻底解决乱码的问题, 最终还是需要使用fwrite(设置nmemb128可以复现该现象)
11、直接写指定字节数 fwrite
cpp
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
函数功能:把ptr保存的数据写入到指定的流中
函数参数:【ptr】要写入文件的数据;【size】每一个元素的字节数;【nmemb】要写元素的个数;【stream】要写文件的流
函数返回值:成功返回实际写入文件的字节数或元素数;失败返回错误时返回 0
12、解决fread的问题
cpp
#include <stdio.h>
int main(int argc, char const* argv[])
{
if(argc != 2)
{
printf("Usage: %s filename\n", argv[0]);
return -1;
}
FILE* fp = fopen(argv[1], "r");//只读方式打开文件, 文件不存在, 报错
if(fp == NULL)
{
perror("fopen");
return -1;
}
char buf[1024]={0};
int nbytes = 0;
while((nbytes = fread(buf,1,1024,fp)) != 0)//读取文件内容, 直到文件结束
{
fwrite(buf,1,nbytes,stdout);//写入文件
}
fclose(fp);//关闭文件
return 0;
}
13、 使用fread和fwrite实现cp命令功能

五、定位流 ftell/fseek
1、定位标准I/O流的两种方式
- 文件的位置可以理解为文件的光标的位置
- ftell()和fseek(): 这两个函数自V7以来就存在了,但是它们都假定文件的位置可以存放在一个长整型中。
- fgetpos()和fsetpos()。这两个函数是新由ANSI C引入的。它们引进了一个新的抽象数据类型fpos_t,它记录文件的位置。
- 需要移植到非UNIX系统上运行的应用程序应当使用fgetpos()和fsetpos()
2、获取文件位置 ftell
cpp
long ftell(FILE *stream);
函数功能:获取文件的位置
函数参数:【stream】要操作的文件指针
函数返回值:成功返回: 文件的位置。失败返回 :错误时返回 -1 , 并设置errno,Otherwise, -1 is returned and errno is set to indicate the error.
fopen打开文件的方式,"r" : 文件位置在开头。"w" : 文件位置在开头。"a" : 文件位置在末尾。
3、实例使用ftell计算文件的大小
cpp
#include <stdio.h>
int main(int argc, char const* argv[])
{
if(argc != 2)
{
printf("Usage: %s filename\n", argv[0]);
return -1;
}
FILE* fp = fopen(argv[1], "a");//追加方式打开文件, 文件不存在则创建,存在则追加, 光标在文件末尾
if(fp == NULL)
{
perror("fopen");
return -1;
}
long offset = ftell(fp);//获取当前文件指针的偏移量
printf("size = %ld\n", offset);
fclose(fp);//关闭文件
return 0;
}

4、设置文件的位置 fseek
cpp
int fseek(FILE *stream, long offset, int whence);
函数功能:设置文件的位置
函数参数:【stream】要操作的文件指针。【offset】基于 whence 的偏移量, 以字节计算偏移量。【whence】SEEK_SET : 设置文件指定的位置;SEEK_CUR : 基于当前的文件位置;SEEK_END : 基于文件末尾的位置。
函数返回值,成功返回: 0。失败返回 :错误时返回 -1 , 并设置errno,Otherwise, -1 is returned and errno is set to indicate the error.
cpp
// 1. 把文件位置设置到开始
fseek(fp,0,SEEK_SET)
// 2. 把文件位置设置到末尾
fseek(fp,0,SEEK_END )
// 3. 把文件位置设置到末尾的前一个元素
fseek(fp,-1,SEEK_END )
// 4. 基于文件位置的前1个元素
fseek(fp,-1,SEEK_CUR )
// 5. 基于文件位置的后1个元素
fseek(fp,1,SEEK_CUR )
5、fseek合并文件
cpp
#include <stdio.h>
int main(int argc, char const* argv[])
{
if(argc != 4)
{
printf("Usage: %s filename file1 file2 file3\n", argv[0]);
return -1;
}
FILE* fp1 = fopen(argv[1], "r");//只读方式打开文件, 文件不存在报错
if(fp1 == NULL)
{
perror("fopen");
return -1;
}
FILE* fp2 = fopen(argv[2], "r");//只读方式打开文件, 文件不存在报错
if(fp2 == NULL)
{
perror("fopen");
return -1;
}
FILE* fp3 = fopen(argv[3], "w");//只写方式打开文件, 文件不存在则创建,存在则清空
if(fp3 == NULL)
{
perror("fopen");
return -1;
}
// 使用fgetc从file1中读取内容, 写入file3中
char ch;
while((ch = fgetc(fp1)) != EOF)//读取文件内容, 直到文件结束
{
fputc(ch, fp3);//写入文件
}
// 从file2中读取内容, 写入file3中
while((ch = fgetc(fp2)) != EOF)//读取文件内容, 直到文件结束
{
fputc(ch, fp3);//写入文件
}
fclose(fp3);
fclose(fp1);
fclose(fp2);
return 0;
}

6、fseek拆分文件
cpp
#include <stdio.h>
int main(int argc, char const* argv[])
{
if(argc != 4)
{
printf("Usage: %s filename file1 file2 file3\n", argv[0]);
return -1;
}
FILE* fp1 = fopen(argv[1], "r");//只读方式打开文件, 文件不存在报错
if(fp1 == NULL)
{
perror("fopen");
return -1;
}
FILE* fp2 = fopen(argv[2], "w");//只读方式打开文件, 文件不存在报错
if(fp2 == NULL)
{
perror("fopen");
return -1;
}
FILE* fp3 = fopen(argv[3], "w");//只写方式打开文件, 文件不存在则创建,存在则清空
if(fp3 == NULL)
{
perror("fopen");
return -1;
}
//使用fseek和ftell计算file1的大小
fseek(fp1, 0, SEEK_END);//将文件指针移动到文件末尾
long offset = ftell(fp1);//获取当前文件指针的偏移量
printf("size = %ld\n", offset);
fseek(fp1, 0, SEEK_SET);//将文件指针移动到文件开头
//使用fgetc函数把file1的前一半复制到file2中
for(int i = 0; i < offset/2; i++)
{
char ch = fgetc(fp1);//读取文件内容
fputc(ch, fp2);//写入文件
}
//使用fgetc函数把file1的前一半复制到file3中
for(int i = 0; i < offset/2; i++)
{
char ch = fgetc(fp1);//读取文件内容
fputc(ch, fp3);//写入文件
}
fclose(fp3);
fclose(fp1);
fclose(fp2);
return 0;
}

7、fseek文件逆序
cpp
#include <stdio.h>
int main(int argc, char const* argv[])
{
if(argc != 2)
{
printf("Usage: %s filename file1\n", argv[0]);
return -1;
}
FILE* fp1 = fopen(argv[1], "r+");//只读方式打开文件, 文件不存在报错
if(fp1 == NULL)
{
perror("fopen");
return -1;
}
//使用fseek和ftell计算file1的大小
fseek(fp1, 0, SEEK_END);//将文件指针移动到文件末尾
long offset = ftell(fp1);//获取当前文件指针的偏移量
printf("size = %ld\n", offset);
fseek(fp1, 0, SEEK_SET);//将文件指针移动到文件开头
int t1, t2;
//使用fseek和ftell函数把file1进行逆序输出到file1中
for(long i = 0; i < offset/2 ; i++)
{
//将光标移动到第i个字符
fseek(fp1, i, SEEK_SET);
//读取光标位置的字符
t1 = fgetc(fp1);
//把光标移动到第offset-i-1个字符
fseek(fp1, offset-i-1, SEEK_SET);
//读取光标位置的字符
t2 = fgetc(fp1);
//把光标移动到i的位置
fseek(fp1, i, SEEK_SET);
//把t2写入到光标的位置
fputc(t2, fp1);
//把光标移动到第offset-i-1个字符
fseek(fp1, offset-i-1, SEEK_SET);
//把t1写入到光标的位置
fputc(t1, fp1);
}
fclose(fp1);
return 0;
}