标准IO
标准 I/O 虽然是对文件 I/O 进行了封装,但事实上并不仅仅只是如此,标准 I/O 会处理很多细节,譬如分配 stdio 缓冲区、以优化的块长度执行 I/O 等,这些处理使用户不必担心如何选择使用正确的块长度。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 却有它自己的优势,标准 I/O 和文件 I/O 的区别如下:
虽然标准 I/O 和文件 I/O 都是 C 语言函数,但是标准 I/O 是标准 C 库函数,而文件 I/O 则是 Linux系统调用;
标准 I/O 是由文件 I/O 封装而来,标准 I/O 内部实际上是调用文件 I/O 来完成实际操作的;
-
可移植性:标准 I/O 相比于文件 I/O 具有更好的可移植性,通常对于不同的操作系统,其内核向应用层提供的系统调用往往都是不同,譬如系统调用的定义、功能、参数列表、返回值等往往都是不一样的;而对于标准 I/O 来说,由于很多操作系统都实现了标准 I/O 库,标准 I/O 库在不同的操作系统之间其接口定义几乎是一样的,所以标准 I/O 在不同操作系统之间相比于文件 I/O 具有更好的可移植性。
-
性能、效率: 标准 I/O 库在用户空间维护了自己的 stdio 缓冲区, 所以标准 I/O 是带有缓存的,而文件 I/O 在用户空间是不带有缓存的,所以在性能、效率上,标准 I/O 要优于文件 I/O。
对于标准 I/O 库函数来说,它们的操作是围绕 FILE 指针进行的,当使用标准 I/O 库函数打开或创建一个文件时,会返回一个指向 FILE 类型对象的指针(FILE *) ,使用该 FILE 指针与被打开或创建的文件相关联,然后该 FILE 指针就用于后续的标准 I/O 操作(使用标准 I/O 库函数进行 I/O 操作),所以由此可知,FILE 指针的作用相当于文件描述符,只不过 FILE 指针用于标准 I/O 库函数中、而文件描述符则用于文件I/O 系统调用中。FILE 是一个结构体数据类型,它包含了标准 I/O 库函数为管理文件所需要的所有信息,包括用于实际I/O 的文件描述符、指向文件缓冲区的指针、缓冲区的长度、当前缓冲区中的字节数以及出错标志等。 FILE数据结构定义在标准 I/O 库函数头文件 stdio.h 中。
所谓标准输入设备指的就是计算机系统的标准的输入设备,通常指的是计算机所连接的键盘;而标准输出设备指的是计算机系统中用于输出标准信息的设备,通常指的是计算机所连接的显示器;标准错误设备则指的是计算机系统中用于显示错误信息的设备,通常也指的是显示器设备。
用户通过标准输入设备与系统进行交互, 进程将从标准输入(stdin)文件中得到输入数据,将正常输出数据(譬如程序中 printf 打印输出的字符串) 输出到标准输出(stdout) 文件,而将错误信息(譬如函数调用报错打印的信息)输出到标准错误(stderr) 文件。标准输出文件和标准错误文件都对应终端的屏幕,而标准输入文件则对应于键盘。每个进程启动之后都会默认打开标准输入、标准输出以及标准错误, 得到三个文件描述符, 即 0、 1、2, 其中 0 代表标准输入、 1 代表标准输出、 2 代表标准错误;
在应用编程中可以使用宏 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 分别代表 0、 1、 2,这些宏定义在 unistd.h 头文件中:
/* Standard file descriptors. */ #define STDIN_FILENO 0 /* Standard input. */ #define STDOUT_FILENO1 /* Standard output. */ #define STDERR_FILENO2 /* Standard error output. */
0、 1、 2 这三个是文件描述符,只能用于文件 I/O(read()、 write()等),那么在标准 I/O 中,自然是无法使用文件描述符来对文件进行 I/O 操作的,它们需要围绕 FILE 类型指针来进行,在 stdio.h 头文件中有相应的定义,如下:
/* Standard streams. */ extern struct _IO_FILE *stdin; /* Standard input stream. */ extern struct _IO_FILE *stdout; /* Standard output stream. */ extern struct _IO_FILE *stderr; /* Standard error output stream. */ /* C89/C99 say they're macros. Make them happy. */ #define stdin stdin #define stdout stdou t#define stderr stderr
Tips: struct _IO_FILE 结构体就是 FILE 结构体,使用了 typedef 进行了重命名。所以,在标准 I/O 中,可以使用 stdin、 stdout、 stderr 来表示标准输入、标准输出和标准错误。
用库函数fopen()打开或创建文件, fopen()函数原型如下所示:
#include <stdio.h> FILE *fopen(const char *path, const char *mode);
使用该函数需要包含头文件 stdio.h。函数参数和返回值含义如下:
-
path: 参数 path 指向文件路径,可以是绝对路径、也可以是相对路径。
-
mode: 参数 mode 指定了对该文件的读写权限,是一个字符串,稍后介绍。
-
返回值: 调用成功返回一个指向 FILE 类型对象的指针(FILE *),该指针与打开或创建的文件相关联,后续的标准 I/O 操作将围绕 FILE 指针进行。 如果失败则返回 NULL,并设置 errno 以指示错误原因。参数 mode 字符串类型,可取值为如下值之一:
mode | 说明 | flags 参数取值 |
---|---|---|
r | 以只读方式打开文件。 | O_RDONLY |
r+ | 以可读、可写方式打开文件。 | O_RDWR |
w | 以只写方式打开文件;如果文件存在,将文件长度截断为0;如果文件不存在,则创建文件。 | O_WRONLY | O_CREAT | O_TRUNC |
w+ | 以可读、可写方式打开文件;如果文件存在,将文件长度截断为0;如果文件不存在,则创建文件。 | O_RDWR |O_CREAT O_TRUNC |
a | 以只写方式打开文件,进行追加内容(在文件末尾写入);如果文件不存在,则创建文件。 | O_WRONLY | O_CREAT | O_APPEND |
a+ | 以可读、可写方式打开文件,进行追加内容(在文件末尾写入);如果文件不存在,则创建文件。 | O_RDWR | O_CREAT | O_APPEND |
读文件和写文件
当使用 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 参数指定数据大小。譬如要将一个 struct mystr 结构体数据写入到文件中:
-
可按如下方式写入:fwrite(buf, sizeof(struct mystr), 1, file);
-
当然也可以按如下方式写:fwrite(buf, 1, sizeof(struct mystr), file);
库函数 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()可手动设置文件当前的读写位置偏移量。
-
譬如将文件的读写位置移动到文件开头处:fseek(file, 0, SEEK_SET);
-
将文件的读写位置移动到文件末尾:fseek(file, 0, SEEK_END);
-
将文件的读写位置移动到 100 个字节偏移量处:fseek(file, 100, SEEK_SET);
调用 fread()读取数据时,如果返回值小于参数 nmemb 所指定的值,表示发生了错误或者已经到了文件末尾(文件结束 end-of-file),但 fread()无法具体确定是哪一种情况; 在这种情况下,可以通过判断错误标志或 end-of-file 标志来确定具体的情况。feof()函数库函数 feof()用于测试参数 stream 所指文件的 end-of-file 标志,如果 end-of-file 标志被设置了,则调用feof()函数将返回一个非零值,如果 end-of-file 标志没有被设置,则返回 0。
#include <stdio.h> int feof(FILE *stream);
当文件的读写位置移动到了文件末尾时, end-of-file 标志将会被设置。库函数 ferror()用于测试参数 stream 所指文件的错误标志,如果错误标志被设置了,则调用 ferror()函数将返回一个非零值,如果错误标志没有被设置,则返回 0。其函数原型如下所示:
#include <stdio.h> int ferror(FILE *stream);
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, ...);
这 5 个函数都是可变参函数,它们都有一个共同的参数 format,这是一个字符串,称为格式控制字符串,用于指定后续的参数如何进行格式转换, 所以才把这些函数称为格式化输出,因为它们可以以调用者指定的格式进行转换输出; 学习这些函数的重点就是掌握这个格式控制字符串 format 的书写格式以及它们所代表的意义, 每个函数除了固定参数之外,还可携带 0 个或多个可变参数。printf()函数
用于将格式化数据写入到标准输出; dprintf()和 fprintf()函数用于将格式化数据写入到指定的文件中,两者不同之处在于, fprintf()使用 FILE 指针指定对应的文件、而 dprintf()则使用文件描述符 fd 指定对应的文件; sprintf()、 snprintf()函数可将格式化的数据存储在用户指定的缓冲区 buf 中。printf()函数前面章节内容编写的示例代码中多次使用了该函数,用于将程序中的字符串信息输出显示到终端(也就是标准输出),它是一个可变参函数,除了一个固定参数 format外,后面还可携带 0 个或多个参数。函数调用成功返回打印输出的字符数;失败将返回一个负值!打印“Hello World”:printf("Hello World!\n");打印数字 5:printf("%d\n", 5);
fprintf()可将格式化数据写入到由 FILE 指针指定的文件中,譬如将字符串“Hello World”写入到标准错误:fprintf(stderr, "Hello World!\n");向标准错误写入数字 5:fprintf(stderr, "%d\n", 5);函数调用成功返回写入到文件中的字符数;失败将返回一个负值!
dprintf()可将格式化数据写入到由文件描述符 fd 指定的文件中,譬如将字符串“Hello World”写入到标准错误:dprintf(STDERR_FILENO, "Hello World!\n");向标准错误写入数字 5:dprintf(STDERR_FILENO, "%d\n", 5);函数调用成功返回写入到文件中的字符数;失败将返回一个负值!
sprintf()函数:sprintf()函数将格式化数据存储在由参数 buf 所指定的缓冲区中, 譬如将字符串“Hello World”存放在缓冲区中:
char buf[100];sprintf(buf, "Hello World!\n");
当然这种用法并没有意义,事实上,我们一般会使用这个函数进行格式化转换,并将转换后的字符串存放在缓冲区中,譬如将数字 100 转换为字符串"100",将转换后得到的字符串存放在 buf 中:
char buf[20] = {0}; sprintf(buf, "%d", 100);
sprintf()函数会在字符串尾端自动加上一个字符串终止字符'\0'。
需要注意的是, sprintf()函数可能会造成由参数 buf 指定的缓冲区溢出,调用者有责任确保该缓冲区足够大,因为缓冲区溢出会造成程序不稳定甚至安全隐患!函数调用成功返回写入到 buf 中的字节数;失败将返回一个负值!snprintf()函数sprintf()函数可能会发生缓冲区溢出的问题,存在安全隐患,为了解决这个问题,引入了 snprintf()函数;在该函数中,使用参数 size 显式的指定缓冲区的大小,如果写入到缓冲区的字节数大于参数 size 指定的大小,超出的部分将会被丢弃!如果缓冲区空间足够大, snprintf()函数就会返回写入到缓冲区的字符数,与sprintf()函数相同,也会在字符串末尾自动添加终止字符'\0'。若发生错误, snprintf()将返回一个负值!
以上 5 个函数中的 format 参数应该怎么写,把这个参数称为格式控制字符串,顾名思义,首先它是一个字符串的形式,其次它能够控制后续变参的格式转换。格式控制字符串由两部分组成:普通字符(非%字符) 和转换说明。普通字符会进行原样输出,每个转换说明都会对应后续的一个参数,通常有几个转换说明就需要提供几个参数(除固定参数之外的参数), 使之一一对应,用于控制对应的参数如何进行转换。如下所示:printf("转换说明 1 转换说明 2 转换说明 3", arg1, arg2, arg3);这里只是以 printf()函数举个例子,实际上并不这样用。三个转换说明与参数进行一一对应,按照顺序方式一一对应。每个转换说明都是以%字符开头,其格式如下所示(使用[ ]括起来的部分是可选的) :
%[flags][width][.precision][length]type
-
flags: 标志,可包含 0 个或多个标志;
-
width: 输出最小宽度,表示转换后输出字符串的最小宽度;precision: 精度,前面有一个点号" . ";
-
length: 长度修饰符;
-
type: 转换类型,指定待转换数据的类型。
-
可以看到,只有%和 type 字段是必须的,其余都是可选的。
首先说明 type(类型), 因为类型是格式控制字符串的重中之重,是必不可少的组成部分,其它的字段都是可选的, type 用于指定输出数据的类型, type 字段使用一个字符(字母字符)来表示:
字符 | 对应数据类型 | 含义 | 示例说明 |
---|---|---|---|
d/i | int | 输出有符号十进制表示的整数,i 是老式写法 | printf("%d\n", 123); 输出: 123 |
o | unsigned int | 输出无符号八进制表示的整数(默认不输出前缀0,可在 # 标志下输出前缀0) | printf("%o\n", 123); 输出: 173 |
u | unsigned int | 输出无符号十进制表示的整数 | printf("%u\n", 123); 输出: 123 |
x/X | unsigned int | 输出无符号十六进制表示的整数,x 和 X 区别在于字母大小写 | printf("%x\n", 123); 输出: 7b printf("%X\n", 123); 输出: 7B |
f/F | double | 输出浮点数,f 和 F 区别在于字母大小写,默认保留小数点后 6 位数 | printf("%f\n", 520.1314); 输出: 520.131400 printf("%F\n", 520.1314); 输出: 520.131400 |
e/E | double | 输出以科学计数法表示的浮点数,e 和 E 区别在于字母大小写 | printf("%e\n", 520.1314); 输出: 5.201314e+02 printf("%E\n", 520.1314); 输出: 5.201314E+02 |
g | double | 根据数值的长度,选择以最短方式输出,%f/%e | printf("%g %g\n", 0.000000123, 0.123); 输出: 1.23e-07 0.123 |
G | double | 根据数值的长度,选择以最短方式输出,%F/%E | printf("%G %G\n", 0.000000123, 0.123); 输出: 1.23E-07 0.123 |
s | char * | 字符串,输出字符串中的字符直到终止字符 \0 | printf("%s\n", "Hello World"); 输出: Hello World |
p | void * | 输出十六进制表示的指针 | printf("%p\n", "Hello World"); 输出: 0x400624 |
c | char | 字符型,将输入的数字转换为对应的 ASCII 字符输出 | printf("%c\n", 64); 输出: A |
flags字段的含义如下:
字符 | 名称 | 作用 |
---|---|---|
# | 井号 | 对于 o 类型,输出字符串增加前缀 0;对于 x 或 X 类型,输出前缀 0x 或 0X 。对于浮点数类型,强制输出小数点。 |
0 | 数字 0 | 当输出数字(非 c 或 s 类型)时,在输出前补 0,直到达到指定最小宽度。 |
- | 减号 | 左对齐输出,若宽度不足则在右边填充空格。若同时指定 0 和 - ,- 会覆盖 0 。 |
' ' | 空格 | 输出正数时,在数字前加一个空格,负数则加负号 - 。 |
+ | 加号 | 输出时无论正数还是负数,前面都带符号。正数带 + ,负数带 - 。+ 会覆盖 ' ' (空格)。 |
宽度类型 | 描述 | 示例 |
---|---|---|
数字 | 指定输出的最小宽度,若实际输出位数小于指定宽度,前面会补充空格或 0 。 | printf("%06d", 1000); 输出: 001000 |
* | 不显示宽度数值,宽度由参数列表中的值指定。 | printf("%0*d", 6, 1000); 输出: 001000 |
描述 | 类型 | 示例 |
---|---|---|
数字 | 整型(d , i , o , u , x , X ) | 对于整型,指定输出的最小数字位数,不足时补前导零。 例如 printf("%8.5d", 100); 输出: 00100 |
浮点型(a , A , e , E , f , F ) | 对于浮点数,指定小数点后数字的个数。默认6位。 例如 printf("%.8f", 520.1314); 输出: 520.13140000 | |
g , G | 对于 g 和 G ,表示最大有效数字位数。 | |
字符串 | s | 指定最大输出字符数,超过则截断。 例如 printf("%.5s", "hello world"); 输出: hello |
* | 星号 | 精度由参数列表指定,例如 printf("%.*s", 5, "hello world"); 输出: hello |
长度修饰符指明待转换数据的长度,因为 type 字段指定的的类型只有 int、unsigned int 以及 double 等几种数据类型,但是C 语言内置的数据类型不止这几种,譬如有 16bit 的 short、unsigned short,8bit 的char、unsigned char,也有64bit 的 long long 等,为了能够区别不同长度的数据类型,于是乎,长度修饰符(length)应运而生,成为转换说明的一部分。 length 长度修饰符也是使用字符(字母字符)来表示,结合type 字段以确定不同长度的数据类型:
type | length | 描述 |
---|---|---|
d , i | none | int 类型,输出有符号十进制整数。 |
u , o , x , X | none | unsigned int 类型,输出无符号十进制、八进制、十六进制整数。 |
f , F , e , E , g , G | none | double 类型,输出浮点数,使用小数表示或科学计数法。 |
c | none | char 类型,输出一个字符。 |
s | none | char * 类型,输出字符串。 |
p | none | void * 类型,输出指针的十六进制表示。 |
hh | signed char , unsigned char | signed char 或 unsigned char 类型,输出字符。 |
h | short int , unsigned short int | short int 或 unsigned short int 类型,输出整数。 |
l | long int , unsigned long int | long int 或 unsigned long int 类型,输出整数。 |
wint_t | wchar_t | 宽字符类型(wint_t 或 wchar_t ),用于宽字符处理。 |
ll | long long int , unsigned long long int | long long int 或 unsigned long long int 类型,输出整数。 |
L | long double | long double 类型,输出浮点数。 |
j | intmax_t , uintmax_t | intmax_t 或 uintmax_t 类型,输出整数。 |
z | size_t , ssize_t | size_t 或 ssize_t 类型,输出无符号或有符号整数。 |
t | ptrdiff_t | ptrdiff_t 类型,表示指针差值。 |
格式化输入是类似的,这里不加以赘述了。