嵌入式Linux入门具备:C语言基础与基本驱动学习(2):Linux GIibc IO基础

标准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
w+ 以可读、可写方式打开文件;如果文件存在,将文件长度截断为0;如果文件不存在,则创建文件。 `O_RDWR
a 以只写方式打开文件,进行追加内容(在文件末尾写入);如果文件不存在,则创建文件。 `O_WRONLY
a+ 以可读、可写方式打开文件,进行追加内容(在文件末尾写入);如果文件不存在,则创建文件。 `O_RDWR

读文件和写文件

当使用 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 输出无符号十六进制表示的整数,xX 区别在于字母大小写 printf("%x\n", 123); 输出: 7b printf("%X\n", 123); 输出: 7B
f/F double 输出浮点数,fF 区别在于字母大小写,默认保留小数点后 6 位数 printf("%f\n", 520.1314); 输出: 520.131400 printf("%F\n", 520.1314); 输出: 520.131400
e/E double 输出以科学计数法表示的浮点数,eE 区别在于字母大小写 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;对于 xX 类型,输出前缀 0x0X。对于浮点数类型,强制输出小数点。
0 数字 0 当输出数字(非 cs 类型)时,在输出前补 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 对于 gG,表示最大有效数字位数。
字符串 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 charunsigned char 类型,输出字符。
h short int, unsigned short int short intunsigned short int 类型,输出整数。
l long int, unsigned long int long intunsigned long int 类型,输出整数。
wint_t wchar_t 宽字符类型(wint_twchar_t),用于宽字符处理。
ll long long int, unsigned long long int long long intunsigned long long int 类型,输出整数。
L long double long double 类型,输出浮点数。
j intmax_t, uintmax_t intmax_tuintmax_t 类型,输出整数。
z size_t, ssize_t size_tssize_t 类型,输出无符号或有符号整数。
t ptrdiff_t ptrdiff_t 类型,表示指针差值。

格式化输入是类似的,这里不加以赘述了。

相关推荐
岳不谢10 分钟前
VPN技术-VPN简介学习笔记
网络·笔记·学习·华为
海害嗨23 分钟前
阿里巴巴官方「SpringCloudAlibaba全彩学习手册」限时开源!
学习·开源
平头哥在等你1 小时前
求一个3*3矩阵对角线元素之和
c语言·算法·矩阵
小A1591 小时前
STM32完全学习——使用SysTick精确延时(阻塞式)
stm32·嵌入式硬件·学习
尹蓝锐2 小时前
C语言-11-18笔记
c语言
ahadee2 小时前
蓝桥杯每日真题 - 第18天
c语言·vscode·算法·蓝桥杯
就爱六点起2 小时前
C/C++ 中的类型转换方式
c语言·开发语言·c++
小A1592 小时前
STM32完全学习——使用标准库点亮LED
stm32·嵌入式硬件·学习
猫猫的小茶馆2 小时前
【C语言】指针常量和常量指针
linux·c语言·开发语言·嵌入式软件
朝九晚五ฺ2 小时前
【Linux探索学习】第十五弹——环境变量:深入解析操作系统中的进程环境变量
linux·运维·学习