1
0
wiki/docs/开发/C/IO 函数.md

249 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: IO 函数
title: IO 函数
sidebar_position: 17
data: 2022年3月30日
---
C 语言提供了一些函数,用于与外部设备通信,称为输入输出函数,简称 I/O 函数。输入import指的是获取外部数据输出export指的是向外部传递数据。
## 缓存和字节流
严格地说输入输出函数并不是直接与外部设备通信而是通过缓存buffer进行间接通信。这个小节介绍缓存是什么。
普通文件一般都保存在磁盘上面,跟 CPU 相比磁盘读取或写入数据是一个很慢的操作。所以程序直接读写磁盘是不可行的可能每执行一行命令都必须等半天。C 语言的解决方案,就是只要打开一个文件,就在内存里面为这个文件设置一个缓存区。
程序向文件写入数据时,程序先把数据放入缓存,等到缓存满了,再把里面的数据会一次性写入磁盘文件。这时,缓存区就空了,程序再把新的数据放入缓存,重复整个过程。
程序从文件读取数据时,文件先把一部分数据放到缓存里面,然后程序从缓存获取数据,等到缓存空了,磁盘文件再把新的数据放入缓存,重复整个过程。
内存的读写速度比磁盘快得多,缓存的设计减少了读写磁盘的次数,大大提高了程序的执行效率。另外,一次性移动大块数据,要比多次移动小块数据快得多。
这种读写模式对于程序来说就有点像水流stream不是一次性读取或写入所有数据而是一个持续不断的过程。先操作一部分数据等到缓存吞吐完这部分数据再操作下一部分数据。这个过程就叫做字节流操作。
由于缓存读完就空了,所以字节流读取都是只能读一次,第二次就读不到了。这跟读取文件很不一样。
C 语言的输入输出函数,凡是涉及读写文件,都是属于字节流操作。输入函数从文件获取数据,操作的是输入流;输出函数向文件写入数据,操作的是输出流。
## printf()
`printf()`是最常用的输出函数,用于屏幕输出,原型定义在头文件`stdio.h`,详见《基本语法》一章。
## scanf()
### 基本用法
`scanf()`函数用于读取用户的键盘输入。程序运行到这个语句时,会停下来,等待用户从键盘输入。用户输入数据、按下回车键后,`scanf()`就会处理用户的输入,将其存入变量。它的原型定义在头文件`stdio.h`。
`scanf()`的语法跟`printf()`类似。
```c
scanf("%d", &i);
```
它的第一个参数是一个格式字符串,里面会放置占位符(与`printf()`的占位符基本一致),告诉编译器如何解读用户的输入,需要提取的数据是什么类型。这是因为 C 语言的数据都是有类型的,`scanf()`必须提前知道用户输入的数据类型,才能处理数据。它的其余参数就是存放用户输入的变量,格式字符串里面有多少个占位符,就有多少个变量。
上面示例中,`scanf()`的第一个参数`%d`,表示用户输入的应该是一个整数。`%d`就是一个占位符,`%`是占位符的标志,`d`表示整数。第二个参数`&i`表示,将用户从键盘输入的整数存入变量`i`。
注意,变量前面必须加上`&`运算符(指针变量除外),因为`scanf()`传递的不是值,而是地址,即将变量`i`的地址指向用户输入的值。如果这里的变量是指针变量(比如字符串变量),那就不用加`&`运算符。
下面是一次将键盘输入读入多个变量的例子。
```c
scanf("%d%d%f%f", &i, &j, &x, &y);
```
上面示例中,格式字符串`%d%d%f%f`,表示用户输入的前两个是整数,后两个是浮点数,比如`1 -20 3.4 -4.0e3`。这四个值依次放入`i`、`j`、`x`、`y`四个变量。
`scanf()`处理数值占位符时,会自动过滤空白字符,包括空格、制表符、换行符等。所以,用户输入的数据之间,有一个或多个空格不影响`scanf()`解读数据。另外,用户使用回车键,将输入分成几行,也不影响解读。
```c
1
-20
3.4
-4.0e3
```
上面示例中,用户分成四行输入,得到的结果与一行输入是完全一样的。每次按下回车键以后,`scanf()`就会开始解读,如果第一行匹配第一个占位符,那么下次按下回车键时,就会从第二个占位符开始解读。
`scanf()`处理用户输入的原理是,用户的输入先放入缓存,等到按下回车键后,按照占位符对缓存进行解读。解读用户输入时,会从上一次解读遗留的第一个字符开始,直到读完缓存,或者遇到第一个不符合条件的字符为止。
```c
int x;
float y;
// 用户输入 " -13.45e12# 0"
scanf("%d", &x);
scanf("%f", &y);
```
上面示例中,`scanf()`读取用户输入时,`%d`占位符会忽略起首的空格,从`-`处开始获取数据,读取到`-13`停下来,因为后面的`.`不属于整数的有效字符。这就是说,占位符`%d`会读到`-13`。
第二次调用`scanf()`时,就会从上一次停止解读的地方,继续往下读取。这一次读取的首字符是`.`,由于对应的占位符是`%f`,会读取到`.45e12`,这是采用科学计数法的浮点数格式。后面的`#`不属于浮点数的有效字符,所以会停在这里。
由于`scanf()`可以连续处理多个占位符,所以上面的例子也可以写成下面这样。
```c
scanf("%d%f", &x, &y);
```
`scanf()`的返回值是一个整数,表示成功读取的变量个数。如果没有读取任何项,或者匹配失败,则返回`0`。如果读取到文件结尾,则返回常量 EOF。
### 占位符
`scanf()`常用的占位符如下,与`printf()`的占位符基本一致。
- `%c`:字符。
- `%d`:整数。
- `%f``float`类型浮点数。
- `%lf``double`类型浮点数。
- `%Lf``long double`类型浮点数。
- `%s`:字符串。
- `%[]`:在方括号中指定一组匹配的字符(比如`%[0-9]`),遇到不在集合之中的字符,匹配将会停止。
上面所有占位符之中,除了`%c`以外,都会自动忽略起首的空白字符。`%c`不忽略空白字符,总是返回当前第一个字符,无论该字符是否为空格。如果要强制跳过字符前的空白字符,可以写成`scanf(" %c", &ch)`,即`%c`前加上一个空格,表示跳过零个或多个空白字符。
下面要特别说一下占位符`%s`,它其实不能简单地等同于字符串。它的规则是,从当前第一个非空白字符开始读起,直到遇到空白字符(即空格、换行符、制表符等)为止。因为`%s`不会包含空白字符,所以无法用来读取多个单词,除非多个`%s`一起使用。这也意味着,`scanf()`不适合读取可能包含空格的字符串,比如书名或歌曲名。另外,`scanf()`遇到`%s`占位符,会在字符串变量末尾存储一个空字符`\0`。
`scanf()`将字符串读入字符数组时,不会检测字符串是否超过了数组长度。所以,储存字符串时,很可能会超过数组的边界,导致预想不到的结果。为了防止这种情况,使用`%s`占位符时,应该指定读入字符串的最长长度,即写成`%[m]s`,其中的`[m]`是一个整数,表示读取字符串的最大长度,后面的字符将被丢弃。
```c
char name[11];
scanf("%10s", name);
```
上面示例中,`name`是一个长度为11的字符数组`scanf()`的占位符`%10s`表示最多读取用户输入的10个字符后面的字符将被丢弃这样就不会有数组溢出的风险了。
### 赋值忽略符
有时,用户的输入可能不符合预定的格式。
```c
scanf("%d-%d-%d", &year, &month, &day);
```
上面示例中,如果用户输入`2020-01-01`,就会正确解读出年、月、日。问题是用户可能输入其他格式,比如`2020/01/01`,这种情况下,`scanf()`解析数据就会失败。
为了避免这种情况,`scanf()`提供了一个赋值忽略符assignment suppression character`*`。只要把`*`加在任何占位符的百分号后面,该占位符就不会返回值,解析后将被丢弃。
```c
scanf("%d%*c%d%*c%d", &year, &month, &day);
```
上面示例中,`%*c`就是在占位符的百分号后面,加入了赋值忽略符`*`,表示这个占位符没有对应的变量,解读后不必返回。
## sscanf()
`sscanf()`函数与`scanf()`很类似,不同之处是`sscanf()`从字符串里面,而不是从用户输入获取数据。它的原型定义在头文件`stdio.h`里面。
```c
int sscanf(const char* s, const char* format, ...);
```
`sscanf()`的第一个参数是一个字符串指针,用来从其中获取数据。其他参数都与`scanf()`相同。
`sscanf()`主要用来处理其他输入函数读入的字符串,从其中提取数据。
```c
fgets(str, sizeof(str), stdin);
sscanf(str, "%d%d", &i, &j);
```
上面示例中,`fgets()`先从标准输入获取了一行数据(`fgets()`的介绍详见下一章),存入字符数组`str`。然后,`sscanf()`再从字符串`str`里面提取两个整数,放入变量`i`和`j`。
`sscanf()`的一个好处是,它的数据来源不是流数据,所以可以反复使用,不像`scanf()`的数据来源是流数据,只能读取一次。
`sscanf()`的返回值是成功赋值的变量的数量,如果提取失败,返回常量 EOF。
## getchar()putchar()
**1getchar()**
`getchar()`函数返回用户从键盘输入的一个字符,使用时不带有任何参数。程序运行到这个命令就会暂停,等待用户从键盘输入,等同于使用`scanf()`方法读取一个字符。它的原型定义在头文件`stdio.h`。
```c
char ch;
ch = getchar();
// 等同于
scanf("%c", &ch);
```
`getchar()`不会忽略起首的空白字符,总是返回当前读取的第一个字符,无论是否为空格。如果读取失败,返回常量 EOF由于 EOF 通常是`-1`,所以返回值的类型要设为 int而不是 char。
由于`getchar()`返回读取的字符,所以可以用在循环条件之中。
```c
while (getchar() != '\n')
;
```
上面示例中,只有读到的字符等于换行符(`\n`),才会退出循环,常用来跳过某行。`while`循环的循环体没有任何语句,表示对该行不执行任何操作。
下面的例子是计算某一行的字符长度。
```c
int len = 0;
while(getchar() != '\n')
len++;
```
上面示例中,`getchar()`每读取一个字符,长度变量`len`就会加1直到读取到换行符为止这时`len`就是该行的字符长度。
下面的例子是跳过空格字符。
```c
while ((ch = getchar()) == ' ')
;
```
上面示例中,结束循环后,变量`ch`等于第一个非空格字符。
**2putchar()**
`putchar()`函数将它的参数字符输出到屏幕,等同于使用`printf()`输出一个字符。它的原型定义在头文件`stdio.h`。
```c
putchar(ch);
// 等同于
printf("%c", ch);
```
操作成功时,`putchar()`返回输出的字符,否则返回常量 EOF。
**3小结**
由于`getchar()`和`putchar()`这两个函数的用法,要比`scanf()`和`printf()`更简单,而且通常是用宏来实现,所以要比`scanf()`和`printf()`更快。如果操作单个字符,建议优先使用这两个函数。
## puts()
`puts()`函数用于将参数字符串显示在屏幕stdout并且自动在字符串末尾添加换行符。它的原型定义在头文件`stdio.h`。
```c
puts("Here are some messages:");
puts("Hello World");
```
上面示例中,`puts()`在屏幕上输出两行内容。
写入成功时,`puts()`返回一个非负整数,否则返回常量 EOF。
## gets()
`gets()`函数以前用于从`stdin`读取整行输入,现在已经被废除了,仍然放在这里介绍一下。
该函数读取用户的一行输入,不会跳过起始处的空白字符,直到遇到换行符为止。这个函数会丢弃换行符,将其余字符放入参数变量,并在这些字符的末尾添加一个空字符`\0`,使其成为一个字符串。
它经常与`puts()`配合使用。
```c
char words[81];
puts("Enter a string, please");
gets(words);
```
上面示例使用`puts()`在屏幕上输出提示,然后使用`gets()`获取用户的输入。
由于`gets()`获取的字符串,可能超过字符数组变量的最大长度,有安全风险,建议不要使用,改为使用`fgets()`。