22 输入/输出

22.1 流

说明:在C语言中,术语流意味着任意输入的源或任意输出的目的地。stdio.h中的许多函数不仅可以处理表示成文件的流,还可以处理所有其它形式的流。
流:流常常表示为磁盘上的文件,但却可以和其它类型的设备相关联:调制解调器、网络端口、打印机、光盘驱动器等。

22.1.1 文件指针

文件指针(file pointer):File *(File定义在stdio.h中)
用途:C程序中流的访问是通过文件指针实现的
限制:操作系统通常会限制在任意某时刻可以打开的流的数量(但是一个程序中可以声明任意数量的File *型变量)。

22.1.2 标准流和重定向

标准流

说明:stdio.h提供,一共3种

文件指针 默认的含义
stdin 标准输入 键盘
stdout 标准输出 屏幕
stderr 标准错误 屏幕

相关的函数:printfscanfputchargetcharputsgets

重定向

说明:某些操作系统(比如UNIX/Linux/DOS)允许通过所谓的重定向(redirextion)机制来改变标准流默认的含义。

分类 说明 备注
输入重定向(input redirection) 使stdin流表示为文件(而非键盘) 程序不回意识到正在从文件读取数据
输出重定向(output redirection) 使stdout流stderr流表示为文件(而飞屏幕) 程序不会意识到正在向文件中写数据
1
2
3
4
5
6
7
8
// 输入重定向
demo < in.dat

// 输出重定向
demo > out.dat

// 合并
demo < in.dat >out.dat

22.1.3 文本文件与二进制文件

说明:stdio.h支持两种类型的文件(文本文件和二进制文件)
存储方式:文本文件和二进制文件都是字节的序列,不同点在于存储的数据类型。

分类 数据类型(假设字符集为ASCII,16位机器) 空间利用率
文本文件(text file) 字符(占一个字节)
二进制文件(binary file) 字符(占一个字节)、整数(两个字节)、浮点数(四个字节)等

结束符:

  • DOS系统:文本文件和二进制文件不同
结束符分类 文本文件 二进制文件
行的结尾 回行符+回车符 回行符
文件末尾 Ctrl+Z(\x1a),但不是必需的(有的编辑器会加上
  • UNIX系统:对文本文件和二进制文件不进行区分
结束符分类 说明
行的结尾 换行符
文件末尾

注意:

  1. 在屏幕上显示文件内容的程序会假设文件为文本文件
  2. 复制文件时如果设定文件为文本文件,只会复制到出现文件末尾符出现的地方

技巧:在无法确定文件是文本文件还是二进制文件时,安全的做法是把文件假设为二进制文件。

22.2 文件操作

22.2.1 打开文件

fopen函数

说明:用流的方式打开文件
原型:stdio.h

1
2
3
4
5
6
/**
* @param {char *} filename 含有要大开文件名的字符串(可能包含文件位置的信息,例如驱动号或路径)
* @param {char *} mode 模式字符串,例如"r"代表只读方式
* @return {FILE *} 文件指针(如果文件不存在或未获得打开文件的许可则返回空指针)
*/

FILE *fopen(const char *filename, const char *mode);

技巧:在DOS中的文件名中含有”\”字符要用”\替代”
注意:永远不能假设可以打开文件,为了确保不回返回空指针,需要测试fopen函数的返回值。

1
FILE *fp = fopen("c:\\project\\test1.dat", r); // 以只读方式打开

22.2.2 模式

说明:模式字符串依据文件是文本文件还是二进制文件分为两大类。
注意:可读且可写的模式(包含+)存在如下限制

  • 调用文件定位函数后,可读才能转换为可写
  • 调用文件定位函数fflush函数后,可写才能转换为可读

文本文件

模式字符串 含义 是否需要文件存在 如果文件存在
r 可读
w 可写
a 追加
r+ 读和写 从文件头开始(追加到头部)
w+ 读和写 截去(覆盖)
a+ 读和写 追加

二进制文件

模式字符串 含义 是否需要文件存在 如果文件存在
rb 可读
wb 可写
ab 追加
r+b或rb+ 读和写 从文件头开始(追加到头部)
w+b或wb+ 读和写 截去(覆盖)
a+b或ab+ 读和写 追加

22.2.3 关闭文件

fclose函数

说明:关闭不再使用的文件
原型:stdio.h

1
2
3
4
5
/**
* @param {FILE *} 文件指针(来自fopen函数或freopen函数)
* @return 0:关闭成功;EOF(stdio.h宏):关闭失败
*/

int fclose(FILE *stream);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#define FILE_NAME "example.dat"
int main () {
FILE *fp;

// 1.大开文件
fp = fopen(FILE_NAME, "r");
if (fp == NULL) {
printf("Cant't open %s\n", FILE_NAME);
exit(EXIT_FAILURE);
}
else {
printf("Opened!\n");
}

// 2.操作文件
// ...
// 3.关闭文件
fclose(fp);
return 0;
}

22.2.4 为流附加文件

freopen函数

说明:为已经打开的流附加一个不同的文件
应用:把文件和一个标准流相关联
原型:stdio.h

1
2
3
4
5
6
7
/**
* @param {char *} filename 文件名
* @param {char *} mode 打开模式
* @param {FILE *} stream 标准流(stdin或stdout或stderr)
* @return {FILE *} 附加成功:文件指针;NULL:打开失败
*/

FILE *freopen(const char *filename, const char *mode, FILE* stream);
1
2
3
4
5
6
7
8
9
// 1.如果stdout通过命令行重定向或者freopen函数已经和其它文件关联,则先关闭与stdout相关联的文件
// fclose(frp); // fp指向是和stdout关联的流(文件)

// 2.打开foo文件,并使此文件和stdout相关联
// 如果无法关闭旧的文件,那么freopen函数会忽略掉错误
frp = freopen("foo", "w", stdout)
if (frp == NULL) {
// 打开失败
}

22.2.5 从命令行获取文件名

1
2
3
4
5
6
7
8
/**
*
* @param {int} argc 实际参数的数量
* @param {[].(char *)} 一个指针数组,argv[0]指向程序的名字,其余指向实际参数
*/

int main (int argc, char *argv[]) {
...
}
1
2
3
4
# argv[0]: demo
# argv[1]: name.dat
# argv[2]: dates.dat.dat
$ demo name.dat dates.dat

22.2.6 程序:检查文件是否可以打开

说明:若文件存在就可以打开进行读入,在运行程序时,用户将给出要检测的文件的名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Checks whether a file can be opened for reading
*/


# include <stdio.h>
int int main(int argc, char const *argv[])
{

FILE *fp;
// 如果没有正确调用,给出使用提示
if (argc != 2) {
printf("usage: canopen filanem\n");
}

// 如果不能以只读方式打开
if ((fp = fopen(argv[1], "r")) == NULL) {
printf("%s can't be opened\n", argv[1]);
return 1;
}

// 如果能以只读的方式打开
printf("%s can be opened\n", argv[1]);
fclose(fp);
return 0;
}
1
$ canopen f1.dat

22.2.7 临时文件

说明:只在程序运行时存在的文件。stdio.h提供了两个函数用来处理临时文件,即tmpfiletmpname

tmpfile函数

说明:产生临时文件,这些临时文件将存到文件关闭时或程序终止时。
原型:stdio.h

1
2
3
4
/**
* @ return {FILE *} 指向临时文件的文件指针(如果创建失败则为NULL)
*/

FILE *tmpfile(void);
1
2
FILE *tmpptr;
Tempptr = tmpfile(); //

tmpfile函数

说明:为临时文件产生名字。
用途:解决tmpfile函数无法知道临时文件的名字的问题。
原型:stdio.h

1
2
3
4
5
6
7
8
/**
* 如果实际参数为NULL,那么tmpnam函数会把文件名存储到静态变量中,并且返回指向此变量的指针;
* 否则,如果提供了字符数组作为参数,函数会把文件名复制到程序员提供的字符数组中。
*
* @param {char *} NULL或者一个字符数组(字符串)
* @return {char *} 指向静态变量(存储着临时文件名)的指针:如果参数为NULL;指向临时文件名的指针(非静态):如果提供了有效的参数
*/

char *tmpnam(const char *);

参数为NULL

1
2
char *filename;
filename = tmpnam(NULL); // 创建临时文件名

参数为字符数组
说明:tmpnam函数会把声称的临时文件名复制到程序员提供的字符数组中,而且仍然会返回指向临时文件名的指针。
注意:作为参数的字符数组长度至少为L_tmpnam,产生的临时文件名的最大数量不能超过TMP_MAX

  • L_tmpnam:在stdio.h中定义的一个宏,保存着临时文件名的字符数组的长度。
  • TMP_MAX:在stdio.h中定义的一个宏,保存着程序执行期间tmpnam函数产生的临时文件名的最大数量。
1
2
char filename[L_tmpnam];
tmpnam(filename);

22.2.8 文件缓冲

说明:缓冲发生在屏幕的后台,而且通常不用担心它的操作。然而,极少的情况下可能需要我们承担更主动的作用,需要使用fflush函数setbuf函数setbuf函数
输入流缓存:从硬盘或磁盘读取,包含来自输入设备(键盘或磁盘)的数据
写入(输出)流缓存:向输出设备(屏幕或磁盘)写入,包含来自

fflush函数

说明:针对输出(写入)流,把缓冲区的内容传递给磁盘(当缓冲区满了或者关闭文件时,缓冲区会自动“清洗”)
原型:stdio.h

1
2
3
4
5
6
7
8
/**
* 把缓冲区的内容传递给磁盘
* 当参数为NULL时,“清洗”所有缓冲区;否则,只清洗和参数指向的文件相关的缓冲区
*
* @param {FILE *} stream 文件指针
* @return {int} 0:成功;EOF:发生错误
*/

int fflush(FILE *stream);
1
2
fflush(fp); // 为fp指向的文件
fflush(NULL); // 清洗全部输出流

setvbuf函数

说明:改变缓冲流的方式,控制缓冲区的大小和位置
原型:stdio.h

1
2
3
4
5
6
7
8
/**
* @param {FILE *} stream 文件指针
* @param {char *} buf 期望缓冲区的地址
* @param {int} 期望缓冲区的类型{_IOFBF|_IOLBF|IONBF}
* @param {size_t} 缓冲区内字节的数量
* @return {int} 0:成功;非零:要求的缓冲区模式无效或无法提供
*/

int setvbuf(FILE *stream, char *buf, int mode, size_t size);

参数要点:

第N个参数 含义 说明
2 缓冲区的地址
3 缓冲区的类型 值为定义在stdio.h中的宏
4 缓冲区的大小 较大的缓冲区可以提供更好的性能,而较小的缓冲区可以节约时间。
缓冲区的类型(宏) 名字 说明
_IOFBF 满缓冲 当缓冲区为空时,从流读入数据;活着当缓冲区满时,向流写入数据
_IONLF 行缓冲 每次从流读入数据活着直接向流写入数据
_IOLBF 无缓冲 每次从流读入数据活着直接向流卸乳数据,而没有缓冲区
缓冲区的存储特点 说明
静态存储期限 存在于程序运行的整个过程中
自动存储期限 允许在它的空间在块退出时被自动重声明(?)
动态分配 在不需要时可以释放缓冲区

限制:必需在打开stream之后,在stream上执行任何操作之前调用setvbuf

1
2
3
4
5
// 1.创建缓冲区
char buffer[N];

// 2.对缓冲区进行设置(必需在打开stream之后,在stream上执行任何操作之前调用setvbuf)
setvbuf(stream, buffer, _IOFBF, N);

22.2.9 其他文件操作

remove函数

说明:根据文件名删除文件
原型:stdio.h

1
2
3
4
5
/**
* @param {char *} filename 文件名
* @return {int} 0:成功;非0:失败
*/

int remove(const char *filename);
1
remove("foo"); // 删除名为foo的文件

rename函数

说明:文件重命名
原型:stdio.h

1
2
3
4
5
6
/**
* @param {char *} old 旧文件名
* @param {char *} new 新文件名
* @return {int} 0:成功;非0:失败
*/

int rename(const char *old, const char *new);

限制:一定要确保在调用rename函数之前文件是关闭的,否则无法对文件重命名。

1
rename("foo", "bar");

22.3 格式化的输入/输出

22.3.1 …printf类函数

printf函数

说明:stdout输出,利用格式串控制输出的形式
原型:stdio.h

1
2
3
4
5
6
/**
* @param {char *} format 格式串
* @param {...*} 对应格式串中的转换说明的参数
* @return {int} 写入的字符数:成功;负值:写入出错
*/

int printf(const char *format, ...);
1
printf("Total: %d\n", total);

fprintf函数


说明:printf函数唯一的不同就是,printf函数始终向标准输出流stdout向中写入,而fprintf函数则向第一个参数说明的流(任何输出流)中写输出。
应用:向标准错误stderr写出错信息。
扩展:stdio.h中还有其他两种函数也可以向流写入格式化的输出,分别是vfprintf函数vprintf函数,而且它们都还依赖stdarg.h(26.1)。
原型:stdio.h

1
2
3
4
5
6
7
/**
* @param {FILE *} stream 文件指针
* @param {char *} format 格式串
* @param {...*} 对应格式串中的转换说明的参数
* @return {int} 写入的字符数:成功;负值:写入出错
*/

int fprintf(FILE *stream, const char *format, ...);
1
2
3
4
5
// 向磁盘中写入
fprintf(fp, "Total:%d\n", total);

// 向stderr中写入
fprintf(stderr, "Error: data file can't be opened.\n")

22.3.2 …printf类函数的转换说明

说明:对已知的转换说明内容进行回顾,并把剩余的内容补充完整。
注意:格式串必需遵守规则编写,许多看似可能的转换说明(%le、%lf、%lg等)实际上是无效的。

转换说明:%# 012.5Lg

% # 0 12 .5 L g
转换说明提示符 ➊标志 ➋最小字段宽度 ➌精度 ➍长度修饰符 ➎转换说明符
必需 可选(可多于一个) 可选 可选 可选 必需

转换说明提示符:标记格式串的开始
➊标志:设置对齐方式、前缀、进制、填充

标志 含义
- 左对齐
+ +作为正数的前缀
空格 空格作为正数的前缀
# 0# 0x(X) # 0(8进制)、# 0x(X)(16进制),转换说明g(G)转换出的尾部0不能删除
0 除非转换说明为d、i、o、u、x(X)且制定了精度,否则用前导0在字段宽度内进行填充

➋最小字段宽度:值为有效整数*

  • 有效整数:**字符数少于最小字段宽度时对字符填充,默认右对齐(在左侧填充空格);大于最小字段宽度则完整显示。
  • *:格式串中的n个*对应参数2~参数n-1,可以是宏

➌精度:值为.整数.*(精度的含义即依赖于转换说明符,也依赖于自身的值)

说明符 精度值:.整数 精度值:.*
d、i、o、u、x(X) 最小数字位数(如果数字位数少于精度值,则添加前导0) 最小字段宽度❷中*的含义
e(E)、f 小数点后的数字位数 (同上)
g(G) 最大有效数字位 (同上)
s 最大字符数 (同上)

➍长度修饰符:共3个,只能和一些转换说明符搭配

长度修饰符 转换说明 含义
h 整数(d、i、o、u、x(X)、n short int
l(L) 整数(d、i、o、u、x(X)、n long int
l(L) e(E)、f、g(G) long double

➎转换说明符:当对带有可变实参的函数(比如printf)传参时,会发生默认的实际参数的提升。float会转换为doublechar会转换为int

转换说明符 对应实参类型 格式化后的形式
d、i signed int 十进制形式
o、u、x(X) unsigned int o(8进制)、u(10进制)、x(16进制,a-f来显示)、X(16进制,A-F来显示)
f double 十进制形式(默认的精度为小数点后显示6位)
e(E) double 科学计数法表示的double,默认精度为小数点后显示6位(e表示指数前为e,E表示指数前为E)
g(G) double -4 >= 指数部分 < 精度值则相当于e(对应g)或者E(对应G);否则相当于f
c unsigned int 无符号整数
s 指向字符串的指针 按照void *型显示,达到精度值(如果存在)或空字符(\0)时停止写操作
p * 转化为可显示格式的void *型值
n int *(指向int型数的指针) ...printf类函数返回值(不会输出到屏幕,而是存储到所指向的int型数中)
% 无对应参数 字符串”%”

22.3.3 …printf类函数的转换说明示例

标志➊

Alt text
Alt text

最小字段宽度➋ + 精度➌

Alt text

转换说明➎

Alt text
Alt text

最小字符宽度❷和精度❸中*

1
2
3
4
5
6
7
8
9
// 最小字符宽度❷和精度❸中*的用法示例
int i = 12345;

printf("%6.4d", i); // ..1234
printf("%*.4d", 6, i); // ..1234
printf("%6.*d", 4, i); // ..1234
printf("%*.*d", 6, 4, i); // ..1234
printf("%*.*d", WIDTH, I, i); // ..1234
printf("%*.*d", page_width/num_cols, I, i); // ..1234

转换说明pn

1
2
// p
printf("%p\n", (void *)ptr); // 显示指针ptr的值(可能会以8进制或16进制形式显示)
1
2
3
// n
int len;
printf("%d%n", 123, &len); // 将printf函数显示的字符数存储到len

22.3.4 …scanf类函数

scanf函数

说明:stdin(键盘)读入内容,根据格式串中的转换说明进行转换并存储在指针指定的位置上。
原型:stdio.h

1
2
3
4
5
6
/**
* @param {char *} format 格式串
* @param {...*} 匹配并转换后的内容对应的存储位置
* return {int} 读入并赋值给实参的数据项数量:成功;EOF:失败
*/

int scanf(const chat *format, ...);
1
2
3
4
// 循环读取一串整数,在首个"?"处停止
while (scanf("%d", &I) == 1) {
//
}

fscanf函数

说明:scanf函数stdin读入数据,而fscanf函数则从它自己的第一个实参所指定的流中妇孺内容。
原型:stdio.h

1
2
3
4
5
6
/**
* @param {FILE *} 指定的流
* @param {char *} format 格式串
* @return {int} 读入并赋值给实参的数据项数量:成功;EOF:失败
*/

int scanf(FILE *stream, const char *format, ...);

22.3.5 …scanf类函数的格式化字符串

比较 ...printf类函数 ...scanf类函数
格式串的作用 转换数据形式并拼接 模式匹配和数据类型转换
数据源 指定的实参(不定参数部分)的值 stdin(键盘输入)
数据源类型 多种类型 字符

格式串

例子:ISBN %d-%d-%ld-%d

ISBN 空格 %d %d %ld %d
ISBN 空白字符(0或多个) 一个整数 一个整数 一个长整数 一个整数
允许整数前存在空白字符 允许整数前存在空白字符 允许整数前存在空白字符 允许整数前存在空白字符
转换说明

说明:类似...printf函数格式串中中的转换说明。
特点:

  • 大多数转换说明会在输入项的开始出跳过空白字符(%[、%c、%n除外)
  • 转换说明从来不回跳过尾部的空白字符(遇到换行符时,不会读区之并停止匹配返回)
空白字符

说明:格式串中的一个(或多个)空白字符匹配0个或多个输入流中的空白字符。

非空白字符

说明:非空白字符和输入流中相同字符进行匹配(%除外)

22.3.6 …scanf类函数的转换说明

转换说明:%*12Ld

% 12 L d
转换说明提示符 ➊赋值屏蔽 ➋最小字段宽度 ➌长度修饰符 ❹转换说明符
必需 可选 可选 可选 必需

❶赋值屏蔽(assignment suppression):使用符号*,匹配空白符之外的连续字符,直到遇到空白符为止。

  • 匹配的数据项会被读入,但不会被赋值给变量
  • 用*匹配到的数据相不回包含在...scanf类函数返回的计数中

❷最大字段宽度:限制转换说明匹配的输入项的字符数量(不计算跳过的空白符),达到限制的字符数量后便停止当前输入项的转换。
❸长度修饰符:共3个,只能和一些转换说明符搭配,同时长度修饰符的选择取决于要存储为相匹配实参所指向的变量类型。

长度修饰符 转换说明 对应实参(指针)的类型
h 整数(d、i、o、u、x(X)、n short int
l(L) 整数(d、i、o、u、x(X)、n long int
l e(E)、f、g(G) double
L e(E)、f、g(G) long double

❹转换说明符:

转换说明符 对应实参指针应当指向类型 匹配的字符
d 整数 十进制整数
i 整数 自动判断进制(0打头:8进制;0x(X)打头:16进制;否则十进制)
o unsigned int 八进制整数
u unsigned int 十进制整数
x(X) unsigned int 十六进制整数
e(E)、f、g(G) float float型小数
s char *(自动在末尾添加\0) 一系列非空白字符
[ char *(自动在末尾添加\0) 来自扫描集合的非空字符序列。扫描集合可以包含任何字符集,特别的是如果扫描集合中包含],则要放在首位,例如[]abc][abc]:表示匹配只含有字母a、b、c的字符序列;[^abc]:表示匹配a、b、c都不存在的字符序列)
c char *(指定❷最大字段宽度n,则在末尾添加\0,否则不添加) 指定❷最大字段宽度n,则匹配n个字符,否则旧就匹配一个字符
p void * ...printf类函数可以打印出的指针值(地址)
n int(不指定❸长度修饰符)、short int(❸长度修饰符h)、long int(❸长度修饰符l) 不匹配任何字符,因而也不回影响...scanf类函数的放回值(对应的变量存储的是到目前为止已经读入的字符数)
% char 匹配字符%

strtol函数(26.2.1)

说明:将字符串根据参数base来转换成长整型数
原型:stdio.h

1
2
3
4
5
/**
* @param {char *} nptr 要转换的字符串
* @param {int} base 基数(0或2~36)
*/

long int strtol(const char *nptr,char **endptr,int base);

...scanf类函数转换说明符之间的对应关系

转换说明符 strol的参数base的值
d 10
i 0
o 8
u 10
x(X) 16
e(E)、f、g(G) 10

22.3.7 …scanf函数的示例

转换说明、空白字符、非空白字符组合效果

Alt text

赋值屏蔽和指定字段宽度效果

Alt text

难懂的转换说明:i、[、n

Alt text

22.3.8 检测文件末尾和错误条件

错误指示器(error indicator):打开流时被清除,遇到错误时会被设置。
文件末尾指示器(end of file indicator):打开流时被清除,遇到文件末尾时被设置。
...scanf类函数出错分类:...scanf类函数的返回值小于不定参数(要匹配)的数量时,由3中可能

  1. 提前遇到文件末尾:函数在完全匹配格式串之前遇到了文件末尾
  2. 匹配失败:数据相的格式错误(比如函数在搜索整数的第一个数字期间遇到了一个字母)
  3. 错误:错误的发生超出了函数控制的范围

clearerr函数

说明:清除文件末尾指示器和错误指示器
注意:Q&A某些其他库函数因为副作用可以清除某种指示器或两种都可以清除,所以不回需要经常使用该函数。
原型:stdio.h

1
2
3
4
/**
* @param {FILE *} stream 文件指针
*/

void clearerr(FILE *stream);

feof函数

说明:检测末尾指示器,判断是否已经到达输入流(文件或stdin)末尾。
原型:stdio.h

1
2
3
4
5
/**
* @param {FILE *} 文件指针
* @ return {int} 如果返回非零值,说明已经到达了输入文件的末尾
*/

int feof(FILE *stream);

ferror函数

说明:检测错误指示器,判断输入过程是否发生错误
原型:stdio.h

1
2
3
4
5
/**
* @param {FILE *} stream 文件指针
* @ return {int} 如果返回非零值,说明已经到达了输入过程发生错误
*/

int ferror(FILE *stream);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 搜索文件中以某个整数起始的行并返回行号
*
* @param {char *} filename 文件名
* @param {int *} ptr 指向要将找到的整数存储到变量的指针
* @return {int} 以整数起始的行的行号
*/

int find_int(const char *filename, int *ptr) {
// 1. 打开文件
FILE *fp fopen (filename, "r");
int line = 1;
if (fp == NULL) {
return -1;
}
while (fscanf(fp, "%d", ptr) != 1) {
// 如果是输入错误
if (ferror (fp)) {
fclose(fp);
return -2;
}
// 如果是到达文件末尾
if (feof(fp)) {
fclose(fp);
return -3; // 整数没找到
}
fscanf(fp, "%*[^\n]"); // 跳过一行的其余部分
line++;
}
// 关闭文件
fclose(fp);
return line;
}
1
2
// 调用
n = find_int("foo", &i);

22.4 字符的输入/输出

说明:本节的所有函数用于文本流和二进制流是等效的。

22.4.1 输出函数

说明:...scanf类函数一样,fputc、putc、putchar出现错误都会为流设置错误指示器并返回EOF

fputc函数

说明:向任意流写字符(putcputchar更通用的版本)
原型:stdio.h

1
2
3
4
5
6
/**
* @param {int} c 函数会把char作为int型的值来处理
* @param {FILE *} stream 文件指针
* @ return {int} c转换为unsigned int的值:输出正确;EOF(负整数):输出错误
*/

int fputc(int c, FILE * stream);

putc宏

说明:向任意流写字符(宏实现)
优点和缺点:14.3
原型:stdio.h

1
2
3
4
5
6
/**
* @param {int} c 函数会把char作为int型的值来处理
* @param {FILE *} stream 文件指针
* @ return {int} c转换为unsigned int的值:输出正确;EOF(负整数):输出错误
*/

int putc(int c, FILE *stream);

putchar宏

说明:向标准输出流stdout(屏幕)写一个字符,通常作为宏来实现(底层是fputc,因此性能不如putc)。
原型:stdio.h

1
#define putchat(c) putc((c), stdout);
1
2
3
4
/**
* @param {int} c 函数会把char作为int型的值来处理
* @return {int}
int putchar(int c);

22.4.2 输入函数

注意:Q&A把char型变量与EOF比较可能会产生错误的结果。

getc宏

说明:从指定流中读入一个字符(宏实现)。
原型:stdio.h

1
2
3
4
5
/**
* @param {FILE *} stream 文件指针
* @return {int} 读如的字符的int型值
*/

int getc(File *stream);
1
2
3
4
// 从文件读入字符直到遇到文件末尾
while ((ch = getc(fp)) != EOF) {
...
}

fgetc函数

说明:从指定的流中读如一个字符。
原型:stdio.h

1
2
3
4
5
/**
* @param {FILE *} stream 文件指针
* @return {int} 读如的字符的int型值
*/

int fgetc(FILE *stream);
1
ch = fgetc(fp); // 从fp中读取一个字符

getchar(宏)

说明:从标准输出流stdout(键盘)中获得一个字符(通常是用宏实现)。
原型:stdio.h

1
#define getchar getc(stdin);

22.4.3 程序:复制文件

说明:采用”rb”和”wb”作为文件的模式使fcopy程序既可以复制文本文件也可以复制二进制文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* Copy a file(既可以是文本文件,也可以是二进制文件)
*/

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{

FILE *source_fp, *dest_fp;
int ch;

// 如果没有给出正确的参数
if (argc != 3) {
fprintf(stderr, "usage: fcopy source dest\n");
exit(EXIT_FAILURE);
}

// 打开原始文件
if ((source_fp = fopen(argv[1], "rb")) == NULL) {
fprintf(stderr, "can't open %s\n", argv[1]);
fclose(source_fp);
exit(EXIT_FAILURE);
}

// 打开目标文件
if ((dest_fp = fopen(argv[2], "wb")) == NULL) {
fprintf(stderr, "can't open %s\n", argv[2]);
fclose(source_fp);
exit(EXIT_FAILURE);
}

// 复制内容(一个字符一个字符地)
while ((ch = getc(source_fp)) != EOF) {
putc(ch, dest_fp);
}

fclose(source_fp);
fclose(dest_fp);
return 0;
}
1
$ ./fcopy source.dat dest.dat

22.5 行的输入/输出

说明:读和写行的苦函数(虽然也可以有效地用于二进制文件流,但多数用于文本流)。

22.5.1 输出函数

puts函数(13.3)

说明:向标准输出流stdout写入一串字符(总会在后面添加一个换行符)。
原型:stdio.h

1
2
3
4
5
/**
* @param {char *} s 要输出的字符串(首地址)
* @return {char *}
*/

int *puts(chat *s);
1
puts("Hi, there!"); // 向stdout(屏幕)输出"Hi, there!"

fputs函数

说明:向指定的流输出一串字符(不会在后面自动添加换行符)
原型:stdio.h

1
2
3
4
5
6
/**
* @param {char *} s 要输出的字符串(首地址)
* @param {FILE *} stream 文件指针
* @return {int} 非负的数:成功输出;`EOF`:出现错误
*/

int fput(chat *s, FILE *stream);
1
fputs("Hi, there!", fp); // 向fp输出"Hi. there!"

22.5.2 输入函数

返回值:无论gets还是fgets,如果出现了错误,活着使在存储人和字符之前大道了输入流的末尾,都会返回空指针NULL;否则,返回指向读入字符串的指针。
末尾空字符:两个函数都会在字符串的末尾存储空字符。
技巧:大多数情况下用fgets而不是gets(只有在确保读入的字符正好适合数组大小时才使用),因为后者会超出接收数组范围的可能

gets函数(13.3

说明:从标准输入流stdin中读取一串字符(逐个读取字符,并且把它们存储在字符串中,直到读取到换行符为止,因此不存储换行符)
注意:只有在确保读入的字符正好适合数组大小时才使用,因为会有超出接收数组范围的可能。
原型:stdio.h

1
2
3
4
5
/**
* @param {char *} s 存储位置(首地址)
* @return {char *} 非负的数:成功输出;`EOF`:出现错误
*/

chat *gets(char *s);

fgets函数

说明:从指定的流中读取一串字符(有时会存储换行符)
原型:stdio.h

1
2
3
4
5
6
7
/**
* @param {char *} s 存储位置(首地址)
* @param {int} n 限制读取字符的数量(保证不回超过s的存储能力)
* @param {FILE *} stream 文件字符(要读取的目标流)
* @return {char *} 读取到的字符串
*/
char *fget(char *s, int n, FILE *stream));

优点:

  • 可以从任意流中读取信息
  • gets更加安全,因为可以限制要存储的字符的数量
1
2
// 逐个读入字符,在遇到首个换行符时或已经读入sizeof(str)-1个字符时结束操作
fgets(str, sizeof(str), fp);

22.6 块的输入/输出

说明:freadfwrite允许程序在一次读和写大的数据块(任意数据类型)。
用途:当程序在终止之前使用fwrite函数把数据存储到文件中,稍后,程序可以把数据从文件读入到内存。
注意:Q&A小心使用fread函数和fwrite函数可以用于文本流,但它们主要还是用于二进制的流。

fread函数

说明:从流读入数据块。
原型:stdio.h

1
2
3
4
5
6
7
8
/**
* @param {void *} ptr 读入后数据块存储位置的地址
* @param {size_t} size 数据块的大小(字节)
* @param {size_t} nmemb 数据块的数量
* @param {FILE *} stream 文件指针(要读的文件位置)
* @return {size_t} 实际读入的数据块的数量,如果小于nmemb则说明达到了文件末尾或出现了错误(可用feof和ferror检测)
*/

fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
1
2
// 从fp指向的流位置读出数组并存储到数组a中
n = fread(a, sizeof(a[0]), sizeof(a)/sizeof(a[0]), fp);

fwrite函数

说明:把内存中的数据块复制到流。
原型:stdio.h

1
2
3
4
5
6
7
8
/**
* @param {void *} ptr 存储数据块的地址
* @param {size_t} size 数据块的大小(字节)
* @param {size_t} nmemb 数据块数量
* @param {FILE *} stream 文件指针(要写入的文件位置)
* @return {size_t} 实际写入的数据块的数量,如果小于nmemb则说明达到了文件末尾或出现了错误(可用feof和ferror检测)
*/

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream );
1
2
3
4
5
// 将内存中数组a复制到fp指向的流(磁盘文件)中
fwrite(a, sizeof(a[0]), sizeof(a)/sizof(a[0]), fp);

// 将内存中的结构体(实例)存储到指向的流(磁盘文件)中
fwrite(&s, sizeof(s), 1, fp);

22.7 文件的定位

说明:stdio.h提供了5个函数允许程序确定当前的文件位置或者改变文件位置,通过这些函数可以实现文件的随机访问(任意访问)。
注意:文件定为函数最适合二进制文件,处理文本流可能出现操作系统差异。
文件位置(file position):每个流都由文件位置,可以看作当前访问到的位置。在执行读或者写操作时,文件位置会自动推进。

fseek函数

说明:改变指定流的文件位置,相关的宏有3个

  • SEEK_SET:文件的起始处
  • SEEK_CUR:文件的当前位置
  • SEEK_END:文件的末尾处

注意:fseek函数对流是文本型还是二进制型非常敏感

  • 文本型:参数必须是以下两种情景之一
  1. offset必须为0(即只能移动到文件的起始处或末尾)
  2. whence必须是SEEK_SET,且offset是通过ftell函数获取的(即返回前一次访问到的位置)
  • 二进制型:不要求支持whience是SEEK_END
    原型:stdio.h
1
2
3
4
5
6
7
/**
* @param {FILE *} stream 文件指针
* @param {long int} offset 目标位置距离whence的距离,可以是负值
* @param {int} whence 参照位置,值为SEEK_SET或SEEK_CUR或SEEK_END
* @return {int} 0:成功;非0:产生错误(比如位置不存在)
*/

int fseek(FILE *stream, long int offset, int whence);
1
2
3
4
5
6
7
8
// 文件位置移动到文件起始处
fseek(fp, 0L, SEEK_SET);

// 文件位置移动到文件末尾
fseek(fp, 0L, SEEK_END);

// 文件位置基于当前位置向后移动10个字节
fseek(fp, -10L, SEEK_CUR);

ftell函数

说明:以长整型返回当前文件位置
注意:二进制文件文本文件的返回值情况有所不同

  • 二进制文件:以字节计算返回当前位置
  • 文本文件:不一定按照字节计数

用途:可能会存储返回的值并且稍后将其提供给fseek函数
原型:stdio.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @param {FILE *} stream 文件指针
* @patam {long int} 当前文件位置:成功;-1L:发生错误(同时会把错误码存储到errno中)
*/

long int ftell(FILE *stream);

```

```c
long int file_pos;
...
file_pos = ftell(fp); // 获取当前文件位置
...
fseek(fp, file_pos, SEEK_SET); // 返回到之前存储下的文件位置处

rewind函数

说明:把文件位置设置到文件起始处,几乎等价于fseek(fp, 0L, SEEK_SET),差异是该函数没有返回值,但会为fp清除掉错误指示器

1
2
3
4
/**
* @param {FILE *} stream 文件指针
*/

void rewind(FILE *stream);

fgetpos函数

说明:将指定流的文件位置存储到fpos_t型变量中
原型:

1
2
3
4
5
6
/**
* @param {FILE * restrict} stream 文件指针
* @param {fpos_t * restrict} fpos_t型变量
* @return {int} 0:调用成功;非0:调用失败 (并将错误码存储到errno)
*/

int fgetpos(FILE *restrict stream, fpos_t * restrict pos);

fsetpos函数

说明:为指定流设置文件fpos_t型位置
原型:stdio.h

1
2
3
4
5
6
/**
* @param {FILE *} stream 文件指针
* @param {fpos_t} pos 文件位置
* @return {int} 0:调用成功;非0:调用失败 (并将错误码存储到errno)
*/

int fsetpos(FILE *stream, const fpos_t *pos);
1
2
3
4
5
fpos_t file_pos;
...
fgetpos(fp, &file_pos); // 获取当前文件位置并保存
...
fsetpos(fp, &file_pos); // 设置文件位置到之前保存的位置

程序:修改零件记录文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* modify a file of part records by setting the quantity
*/

# include <stdio.h>
# include <stdlib.h>
# define NAME_LEN 25
# define MAX_PARTS 100
struct part {
int number;
char name[NAME_LEN + 1];
int on_hand;
} inventory[MAX_PARTS];

int num_parts;
int main() {
FILE *fp;
int i;

// 打开清单
if ((fp = fopen("invent.dat", "rb+")) == NULL) {
fprintf(stderr, "can't open inventory file\n");
exit(EXIT_FAILURE);
}

// 读取零件(元素为结构体的数组)
num_parts = fread(inventory, sizeof(struct part), MAX_PARTS, fp);
for (i = 0; i < num_parts; i++) {
inventory[i].on_hand = 0;
}

// 移动到文件开始处
rewind(fp);

// 将修改后的清单数据存储到文件中
fwrite(inventory, sizeof(struct part), num_parts, fp);
fclose(fp);
return 0;
}

22.8 字符串的输入/输出

扩展:还有一个依赖stdarg.h定义va_listvsprintf函数26.1.2

sprintf函数

说明:类似printf函数fprintf函数,唯一的不同是该函数会把输出写入指定字符数组而不是流中(会在末尾添加一个空字符)。
原型:stdio.h

1
2
3
4
5
6
7
/**
* @param {char *} s 字符数组
* @param {char *} format 格式串
* @param {...*} 对应格式串中的转换说明的参数
* @return {int} 写入的字符数(不包括空字符):成功;负值:写入出错
*/

int sprintf(char *s, const *format, ...)
1
sprintf(str, "%d/%d/%d", 9, 20, 94); // str: 9/20/94

sscanf函数

说明:类似scanf函数fscanf函数,唯一的不同是该函数是从字符数组而不是流中读取数据。
原型:stdio.h

1
2
3
4
5
6
/**
* @param {char *} format 格式串
* @param {...*} 匹配并转换后的内容对应的存储位置
* @return {int} 读入并赋值给实参的数据项数量:成功;EOF:失败(找到第一个数据项之前到达)
*/

int sscanf(const char *s, const char *format, ...);
1
2
3
// 使用fgets函数来获取一行输入,然后把此行数据传递给scanf函数进一步处理
fgets(str, sizeof(str));
sscanf(str, "%d%d", &i, &j);
1
2
3
4
5
6
7
// 从字符串中读取日期
if (sscanf(str, "%d/%d/%d", &month, &day, &year) == 3) {
printf("Month: %d, day: %d, year: %d\n", month, day, year);
}
else if (sscanf(str, "%d-%d-%d", &month, &day, &year) == 3) {
printf("Month: %d, day: %d, year: %d\n", month, day, year);
}