标准 I/O 虽然是对文 件 I/O 进行了封装,但事实上并不仅仅只是如此,标准 I/O 会处理很多细节,譬如分配 stdio 缓冲区、以优化的块长度执行 I/O 等。
1.标准 I/O 库介绍
通常标准 I/O 库函数相关的函数定义都在头文件 <stdio.h> 中,所以我们需要在程序源码中包含 <stdio.h> 头文件。
标准 I/O 库函数是构建于文件 I/O ( open() 、 read() 、 write() 、 lseek() 、 close() 等)这些系统调用之上的, 譬如标准 I/O 库函数 fopen() 就利用系统调用 open() 来执行打开文件的操作、 fread() 利用系统调用 read() 来执行读文件操作、fwrite() 则利用系统调用 write() 来执行写文件操作等等。
设计库函数是为了提供比底层系统调用更为方便、好用的调用接口,虽然标 准 I/O 构建于文件 I/O 之上,但标准 I/O 却有它自己的优势,标准 I/O 和文件 I/O 的区别如下:
1. 虽然标准 I/O 和文件 I/O 都是 C 语言函数,但是标准 I/O 是标准 C 库函数,而文件 I/O 则是 Linux 系统调用;
2. 标准 I/O 是由文件 I/O 封装而来,标准 I/O 内部实际上是调用文件 I/O 来完成实际操作的;
3. 可移植性:标准 I/O 相比于文件 I/O 具有更好的可移植性,通常对于不同的操作系统,其内核向应 用层提供的系统调用往往都是不同,譬如系统调用的定义、功能、参数列表、返回值等往往都是不 一样的;而对于标准 I/O 来说,由于很多操作系统都实现了标准 I/O 库,标准 I/O 库在不同的操作 系统之间其接口定义几乎是一样的,所以标准 I/O 在不同操作系统之间相比于文件 I/O 具有更好的可移植性。
4. 性能、效率:标准 I/O 库在用户空间维护了自己的 stdio 缓冲区,所以标准 I/O 是带有缓存的,而文件 I/O 在用户空间是不带有缓存的,所以在性能、效率上,标准 I/O 要优于文件 I/O 。
其实标准I/O就从操作得角度来说,我个人认为和C语言对文件操作的流程理解是一样的,如果感兴趣的朋友可以看我下面这篇,或许可以帮助理解:
C语言文件操作https://blog.csdn.net/YYYYYYJJJJJYYYYY/article/details/143994163
下面就直接开始讲解相关的函数
2.fopen()函数
使用 open() 系统调用打开或创建文件,而在标准 I/O 中,我们将使用库函数 fopen()打开或创建文件, fopen() 函数原型如下:
#include <stdio.h>
FILE *fopen(const char *path, const char *mode);
函数参数和返回值解释如下:
path : 参数 path 指向文件路径,可以是绝对路径、也可以是相对路径。
mode : 参数 mode 指定了对该文件的读写权限,是一个字符串,稍后介绍。
返回值: 调用成功返回一个指向 FILE 类型对象的指针( FILE * ),该指针与打开或创建的文件相关联,后续的标准 I/O 操作将围绕 FILE 指针进行。如果失败则返回 NULL ,并设置 errno 以指示错误原因。
参数 mode 字符串类型,取值为如下:
r :以只读方式打开文件。
r+ :以可读、可写方式打开文件。
w :以只写方式打开文件,如果参数 path 指定的文件 存在,将文件长度截断为 0 ;如果指定文件不存在 则创建该文件。
w+ :以可读、可写方式打开文件,如果参数 path 指定 的文件存在,将文件长度截断为 0 ;如果指定文件不存在则创建该文件。
a :以只写方式打开文件,打开以进行追加内容(在文件末尾写入),如果文件不存在则创建该文
件。
a+ :以可读、可写方式打开文件,以追加方式写入(在文件末尾写入),如果文件不存在则创建该文件。
3. fclose()函数
调用 fclose() 库函数可以关闭一个由 fopen() 打开的文件,其函数原型如下:
#include <stdio.h>
int fclose(FILE *stream);
参数 stream 为 FILE 类型指针,调用成功返回 0 ;失败将返回 EOF (也就是 -1 ),并且会设置 errno 来指示错误原因。
4. 读文件和写文件
当使用 fopen() 库函数打开文件之后,接着我们便可以使用 fread()和 fwrite() 库函数对文件进行读、写操作了,函数原型如下:
#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);
库函数 fread() 用于读取文件数据,其参数和返回值含义如下:
ptr : fread() 将读取到的数据存放在参数 ptr 指向的缓冲区中;
size : fread() 从文件读取 nmemb 个数据项,每一个数据项的大小为 size 个字节,所以总共读取数据大小为 nmemb * size 个字节。
nmemb : 参数 nmemb 指定了读取数据项的个数。
stream : FILE 指针。
返回值: 调用成功时返回读取到的数据项的数目(数据项数目并不等于实际读取的字节数,除非参数 size 等于 1 );如果发生错误或到达文件末尾,则 fread() 返回的值将小于参数 nmemb ,那么到底发生了错误 还是到达了文件末尾,fread() 不能区分文件结尾和错误,究竟是哪一种情况,此时可以使用 ferror() 或 feof() 函数来判断。
库函数 fwrite() 用于将数据写入到文件中,其参数和返回值含义如下:
ptr : 将参数 ptr 指向的缓冲区中的数据写入到文件中。
size : 参数 size 指定了每个数据项的字节大小,与 fread() 函数的 size 参数意义相同。
nmemb : 参数 nmemb 指定了写入的数据项个数,与 fread() 函数的 nmemb 参数意义相同。
stream : FILE 指针。
返回值: 调用成功时返回写入的数据项的数目(数据项数目并不等于实际写入的字节数,除非参数 size 等于 1 );如果发生错误,则 fwrite() 返回的值将小于参数 nmemb (或者等于 0 )。
由此可知,库函数 fread() 、 fwrite() 中指定读取或写入数据大小的方式与系统调用 read() 、 write() 不同, 前者通过 nmemb (数据项个数) *size (每个数据项的大小)的方式来指定数据大小,而后者则直接通过一个 size 参数指定数据大小。
5.fseek()函数
库函数 fseek() 的作用类似于之前所提到 的系统调用 lseek() ,用于设置文件读写位置偏移量, lseek() 用于文件 I/O ,而库函数 fseek() 则用于标准 I/O ,其函数原型如下:
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
函数参数和返回值含义如下:
stream : FILE 指针。
offset : 与 lseek() 函数的 offset 参数意义相同。
whence : 与 lseek() 函数的 whence 参数意义相同。
返回值: 成功返回 0 ;发生错误将返回 -1 ,并且会设置 errno 以指示错误原因;与 lseek() 函数的返回值 意义不同,这里要注意!
调用库函数 fread() 、 fwrite() 读写文件时,文件的读写位置偏移量会自动递增,使用 fseek() 可手动设置文件当前的读写位置偏移量。
6.feof()函数
库函数 feof() 用于测试参数 stream 所指文件的 end-of-file 标志,如果 end-of-file 标志被设置了,则调用feof()函数将返回一个非零值,如果 end-of-file 标志没有被设置,则返回 0。 其函数原型如下:
#include <stdio.h>
int feof(FILE *stream);
7.ferror()函数
库函数 ferror() 用于测试参数 stream 所指文件的错误标志,如果错误标志被设置了,则调用 ferror() 函数 将返回一个非零值,如果错误标志没有被设置,则返回 0 。其函数原型如下:
#include <stdio.h>
int ferror(FILE *stream);
8.clearerr()函数
库函数 clearerr() 用于清除 end-of-file 标志和错误标志,当调用 feof() 或 ferror() 校验这些标志后,通常需 要清除这些标志,避免下次校验时使用到的是上一次设置的值,此时可以手动调clearerr() 函数清除标志。 clearerr()函数原型如下:
#include <stdio.h>
void clearerr(FILE *stream);
9.格式化输出
C 库函数提供了 5 个格式化输出函数,包括: printf()、fprintf()、dprintf()、sprintf()、snprintf() ,其函数定义如下:
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int dprintf(int fd, const char *format, ...);
int sprintf(char *buf, const char *format, ...);
int snprintf(char *buf, size_t size, const char *format, ...);
printf()函数:函数调用成功返回打印输出的字符数;失败将返回一个负值!
fprintf()函数:fprintf() 可将格式化数据写入到由 FILE 指针指定的文件中。
dprintf() 函数 :dprintf()可将格式化数据写入到由文件描述符 fd 指定的文件中。
sprintf() 函数 :sprintf()函数将格式化数据存储在由参数 buf 所指定的缓冲区中。
snprintf() 函数 :sprintf()函数可能会发生缓冲区溢出的问题,存在安全隐患,为了解决这个问题,引入了 snprintf() 函数; 在该函数中,使用参数 size 显式的指定缓冲区的大小,如果写入到缓冲区的字节数大于参数 size 指定的大小,超出的部分将会被丢弃!如果缓冲区空间足够大,snprintf() 函数就会返回写入到缓冲区的字符数,与sprintf()函数相同,也会在字符串末尾自动添加终止字符 '\0' 。若发生错误,snprintf() 将返回一个负值!
10.格式化输入
C 库函数提供了 3 个格式化输入函数,包括: scanf()、fscanf()、sscanf() ,其函数定义如下:
#include <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);
scanf() 函数:
直接用法举例吧:
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
当程序中调用 scanf() 的时候,终端会被阻塞,等待用户输入数据,此时我们可以通过键盘输入一些字 符,譬如数字、字母或者其它字符,输入完成按回车即可!接着来 scanf() 函数就会对用户输入的数据进行格 式转换处理。 函数调用成功后,将返回成功匹配和分配的输入项的数量;如果较早匹配失败,则该数目可能小于所提供的数目,甚至为零。发生错误则返回负值。
fscanf() 函数:
int a, b, c;
fscanf(stdin, "%d %d %d", &a, &b, &c);
此时它的作用与 scanf() 就是相同的,因为标准输入文件的数据就是用户输入的数据,譬如通过键盘输 入的数据。 函数调用成功后,将返回成功匹配和分配的输入项的数量;如果较早匹配失败,则该数目可能小于所提供的数目,甚至为零。发生错误则返回负值。
sscanf() 函数:
sscanf() 将从参数 str 所指向的字符串缓冲区中读取数据,作为格式转换的输入数据,所以它也有两个固定参数,字符串 str 和格式控制字符串 format。
char *str = "5454 hello";
char buf[10];
int a;
sscanf(str, "%d %s", &a, buf);
函数调用成功后,将返回成功匹配和分配的输入项的数量;如果较早匹配失败,则该数目可能小于所提供的数目,甚至为零。发生错误则返回负值。
前面语法里面的format:
width : 最大字符宽度;
l ength : 长度修饰符,与格式化输出函数的 format 参数中的 length 字段意义相同。
type : 指定输入数据的类型。
举个例子:
scanf("%4s", buf); //匹配字符串,字符串长度不超过 4 个字符
其实就是引号里面百分号%和类型之间的那些东西,相信大家学过C语言都或多或少都知道这些,不熟悉了查查相关资料即可。
关于缓冲区的函数操作后续再罗列出。
不断学习中,共勉!!!