C/C++的一众输入输出函数的区别常常搞得人晕头转向,二者之中又以输入函数更加令人头疼。本文尝试整理C/C++的各种输入输出函数。
由于输入涉及空格、换行符的读取、忽略等问题,因此输入比输出更麻烦。所以本文将以输入为主线,对应的输出用法是类似的。
水平有限,如有疏漏,欢迎提出。
标准输入流
C 标准输入
C语言使用标准输入输出函数,需要包含头文件<stdio.h>
。而在 C++ 中,只要包含头文件<iostream>
,就完全可以使用这些 C 中的输入输出函数。
标准输入流及对缓冲区的理解
stdin
是一个文件描述符(Linux)或句柄(Windows),它在 C 程序启动时就被默认分配好。在 Linux 中一切皆文件,stdin
也相当于一个可读文件,它对应着键盘设备的输入。因为它不断地被输入,又不断地被读取,像流水一样,因此通常称作输入流。
stdin
是一种行缓冲I/O。当在键盘上键入字符时,它们首先被存放在键盘设备自身的缓存中(属于键盘硬件设备的一部分)。只有输入换行符时,操作系统才会进行同步,将键盘缓存中的数据读入到stdin
的输入缓冲区(存在于内存中)。所有从stdin
读取数据的输入流,都是从内存中的输入缓冲区读入数据。当输入缓冲区为空时,函数将被阻塞。
若无特殊说明,以下所有的“缓冲区”均是指内存中的stdin
输入缓冲区。用户程序中自定义的buffer
数组、str
数组等,将称作“数组”、“变量”,以免产生混淆。
scanf()
按照特定格式从stdin
读取输入。
用法示例:
char str[100];
int a;
scanf("%s %d", str, &a); // 注意,传入的一定是变量的地址
对空白字符的处理:
- 缓冲区开头:丢弃空白字符(包括空格、Tab、换行符),直到第一个非空白字符才认为是第一个数据的开始。
- 缓冲区中间:开始读取第一个数据后,一旦遇到空白字符(非换行符), 就认为读取完毕一次。遇到的空白字符残留在缓冲区,直到下一次被读取或刷新。例如输入字符串
this is test
,则会被认为是3
个字符串。 - 缓冲区末尾:按下回车键时,换行符
\n
残留在缓冲区。换行符之前的空格可以认为是中间的空白字符,处理同上。
注意,格式控制符只会读取正确类型的变量。如果输入格式不正确,比如在%d
处输入了一个字符a
,则会使读取中断,即后续不读取任何变量。
格式控制符说明:
类型 | 类型输入 | 参数的类型 |
---|---|---|
%d | 十进制整数 | int * |
%u | 无符号十进制整数 | unsigned int * |
%o | 八进制整数 | int * |
%x | 十六进制整数 | int * |
%f、%e、%g | 浮点数 | float * |
%lf、%le、%lg | 双精度浮点数 | double * |
%c | 单个字符(含空白字符) | char * |
%s | 字符串 | char * |
%% | 读 % 符号 |
注意,%c
是一个比较特殊的格式符号,它将会读取所有空白字符,包括缓冲区开头的空格、Tab、换行符,使用时要特别注意。
scanf()
的读取也没有边界,所以并不安全。C11 标准提供了安全输入scanf_s()
。
scanf()
对应的输出函数是printf()
。
gets() - 不建议
按下回车键时,从stdin
读取一行。
用法示例:
char str[100];
gets(str);
对空白字符的处理:
- 所有空格、Tab等空白字符均被读取,不忽略。
- 按下回车键时,缓冲区末尾的换行符被丢弃,字符串末尾没有换行符
\n
,缓冲区也没有残留的换行符\n
。
注意,gets()
不能指定读取上限,因此容易发生数组边界溢出,造成内存不安全。C11 使用了gets_s()
代替gets()
,但有时编译器未必支持,因此总体来说不建议使用gets()
函数来读取输入。
gets()
对应的输出函数是puts()
。
fgets()
从指定输入流读取一行,输入可以是stdin
,也可以是文件流,使用时需要显式指定。
读取文件流示例:
char str[100];
memset(str, 0, sizeof(str));
int i = 1;
FILE *fp = fopen("...test.txt", "r");
if (fp == NULL) {
printf("File open Error!\n");
exit(1);
}
while (fgets(str, sizeof(str), fp) != NULL)
printf("line%d [len %d]: %s", i++, strlen(str), str);
fclose(fp);
读取stdin示例:
char str[100];
memset(str, 0, sizeof(str));
int i = 1;
while (fgets(str, sizeof(str), stdin) != NULL)
printf("line%d [len %d]: %s", i++, strlen(str), str);
对空白字符的处理:
- 所有空格、Tab等空白字符均被读取,不忽略。
- 按下回车键时,缓冲区末尾的换行符也被读取,字符串末尾将有一个换行符
\n
。例如,输入字符串hello
,再按下回车,则读到的字符串长度为6
。
fgets()
函数会自动在字符串末尾加上\0
结束符。
第 2 个参数n
指定了读取的最大长度。函数读到n-1
个字符(包括换行符\n
)就会停止,并在末尾加上\0
结束符。剩余字符将残留在缓冲区。
建议使用fgets()
完全替代gets()
。
fgets()
对应的输出函数是fputs()
。
fgetc() & getc()
从指定输入流读取一个字符,输入可以是stdin
,也可以是文件流,使用时需要显式指定。
这两个函数完全等效,getc()
由fgetc()
宏定义而来。不同的是,前述的gets()
和fgets()
相互之间没有关系。
用法示例:
char a, b;
a = fgetc(stdin);
b = getc(stdin);
对空白字符的处理:
- 所有空格、Tab、换行等空白字符,无论在缓冲区开头、中间还是结尾,均会被读取,不忽略。
- 因为只读取一个字符,所以如果输入多于
1
个字符(包括换行符),则它们均会残留在缓冲区。具体地说,如果什么字符都不输入,直接按下回车键,则读取到的是换行符\n
,缓冲区无任何残留;如果输入一个字符如a
,然后按下回车键,则读取到的是字符a
,同时换行符\n
残留在缓冲区。
fgetc()
和getc()
对应的输出函数是fputc()
和putc()
。
getchar()
从stdin
读取一个字符。
getchar()
实际上也由fgetc()
宏定义而来,只是默认输入流为stdin
。
用法示例:
char a;
a = getchar();
getchar()
常常用于清理缓冲区开头残留的换行符。当知道缓冲区开头有\n
残留时,可以调用getchar()
但不赋值给任何变量,即可实现冲刷掉\n
的效果。
getchar()
对应的输出函数是putchar()
。
C++ 标准输入
C++中使用标准输入输出需要包含头文件<iostream>
。一般使用iostream
类进行流操作,其封装很完善,也比较复杂,本文只介绍一部分。
cin
cin
是 C++ 的标准输入流对象,即istream
类的一个对象实例。cin
有自己的缓冲区,但默认情况下是与stdin
同步的,因此在 C++ 中可以混用 C++ 和 C 风格的输入输出(在不手动取消同步的情况下)。
cin
与stdin
一样是行缓冲,即遇到换行符时才会将数据同步到输入缓冲区。
cin
的用法非常多,只列举常用的几种。最常用的就是使用>>
符号(我认为该符号形象地体现了“流”的特点)。
用法示例:
int a, b;
cin >> a >> b;
char str[20];
cin >> str;
cin
对空白字符的处理与scanf
一致。即:跳过开头空白字符,遇到空白字符停止读取,且空白字符(包括换行符)残留在缓冲区。
如果不想跳过空白字符,可以使用流控制关键词noskipws
(no skip white space),但这只对单个字符有效(类似于scanf
中的%c
)。
char c;
cin >> noskipws >> c;
注意,cin
对象属于命名空间std
,如果想使用cin
对象,必须在 C++ 文件开头写using namespace std
,或者在每次用到的时候写成std::cin
。
cin.get()
读取单个或指定长度的字符,包括空白字符。
用法示例:
char a, b;
char str[20];
// 读取一个字符,读取失败时返回0,多余字符残留在缓冲区(包括换行符)
a = cin.get();
// 读取一个字符,读取失败时返回EOF,多余字符残留在缓冲区(包括换行符)
cin.get(b);
// 在遇到指定终止字符(参数3)前,至多读取n-1个(参数2)字符
// 当不指定终止字符时,默认为换行符\n
// 如果输入的字符个数小于等于n-1(不含终止字符),则终止字符不残留在缓冲区
// 如果输入的字符个数多于n-1(不含终止字符),则余下字符将残留在缓冲区
cin.get(str, sizeof(str), '\n');
cin.get()
读取单个字符时,类似于 C 中的fgetc()
,对空白字符的处理也与其一致。cin.get()
读取的字符也可以赋值给整型变量。
cin.get()
读取指定长度个字符时,类似于 C 中的fgets()
,但在换行符的处理上不同。它们都不会使换行符残留在缓冲区,但fgets()
会将缓冲区末尾的换行符\n
也写入字符串,而cin.get()
会丢弃缓冲区末尾的\n
。即:当输入test
时,用fgets()
读取得到的字符串长度为5
,用cin.get()
读取得到的字符串长度为4
。
cin.getline()
读取指定长度的字符,包括空白字符。
用法示例:
char str[20];
cin.getline(str, sizeof(str)); // 第3个参数也可以指定终止字符
cin.getline()
与cin.get()
指定读取长度时的用法几乎一样。区别在于,如果输入的字符个数大于指定的最大长度n-1
(不含终止符),cin.get()
会使余下字符残留在缓冲区,等待下次读取;而cin.getline()
会给输入流设为 Fail 状态,在主动恢复之前,无法再进行正常输入。
getline()
getline()
并不是标准输入流istream
的函数,而是字符串流sstream
的函数,只能用于读取数据给string
类对象,使用时也需要包含头文件<string>
。
如果使用getline()
读取标准输入流的数据,需要显式指定输入流。
用法示例:
string str;
getline(cin, str);
getline()
会读取所有空白字符,且缓冲区末尾的换行符会被丢弃,不残留也不写到字符串结尾。同时,由于string
对象的空间是动态分配的,所以会一次性将缓冲区读完,不存在读不完残留在缓冲区的问题。
需要注意的是,假如缓冲区开头就是换行符(比如可能是上一次cin
残留的),则getline()
会直接读取到空字符串并结束,不会给键盘输入的机会。所以这种情况下要注意先清除开头的换行符。
总结
在 C 中,建议使用scanf()
进行格式化读取,用fgets()
读取整行,用fgetc()
或getchar()
读取单个字符。
在 C++ 中,建议使用cin >>
进行格式化读取,而cin.get()
、cin.getline
、getline(string)
有各自的适用情况。
注意fgets()
和cin.get()
在对换行符的清理方面有所区别。
标准输出流
C 标准输出
标准输出流及对缓冲区的理解
相应于输入流的stdin
,输出流也有其默认的文件描述符stdout
,对应着命令行终端(Windows 中称为控制台)的显示。此外,还有对应错误输出的stderr
,默认也是终端的显示。它们都可以被重定向到文件中以便持久保存和查看,在此不作赘述。
stdout
也是行缓冲I/O,它与stdin
类似也有三者之间的数据同步:从用户程序到stdout
的输出缓冲区,由用户程序决定;从stdout
的输出缓冲区到终端的显示,只有缓冲区末尾遇到换行符\n
才会进行。如果输出缓冲区末尾没有换行符\n
,是不会打印显示输出的。
例如以下程序:
// 程序 1
int main(int argc, char* argv[])
{
printf("Hello World!\n");
while(1){}
return 0;
}
// 程序 2
int main(int argc, char* argv[])
{
printf("Hello World!");
while(1){}
return 0;
}
// 程序 3
int main(int argc, char* argv[])
{
printf("Hello World!")
return 0;
}
// 程序 1
int main(int argc, char* argv[])
{
printf("Hello World!\nABCDE");
while(1){}
return 0;
}
程序 1 中,printf()
输出内容的最后有换行符\n
,所以将在屏幕上输出Hello World!
并换行,然后进入while(1)
循环阻塞住。
程序 2 中,把\n
去掉了,此时终端不会显示任何内容。因为程序进入死循环后,没有机会向stdout
中写入\n
使其清空缓冲。
程序 3 中,虽然没有写入换行符,但是依然能够在终端打印Hello World!
(只是没有换行)。这是因为程序结束时会自动清空缓冲区。(除此之外,当缓冲区被填满时也会自动清空)
程序 4 能够进一步加深对行缓冲的理解。它在程序 1 的基础上,在换行符之后又加上了几个字符。运行可以发现终端只打印了Hello World!
并换行,而没有打印ABCDE
。
输出函数通常没有针对对空格、制表符的特殊行为,比输入要简单一些。特殊的处理一般只有换行符。
printf()
按照特定格式将stdout
缓冲区的内容打印到终端。
用法示例:
printf("Number a = %d", a); // 十进制整数
printf("Number b = %.2f", b); // 浮点数,保留两位小数
printf("String s = %s", s); // 字符串
printf()
的写法与scanf()
十分相像。区别在于scanf()
中一般只有格式控制字符,而没有其他普通字符,而printf()
中常常是在一串字符中把要替换的内容写为格式控制字符,从而形成格式化输出的效果。
puts()
将字符串和一个尾随的换行符\n
写入到stdout
的缓冲区。根据行缓冲的性质,终端也会立即进行打印显示。
用法示例:
puts("hello"); // 立即输出hello并换行
puts()
对换行符的处理与gets()
“相反”。gets()
会自动丢弃一个换行符,而puts()
则是自动写入一个换行符。
fputs()
将字符串写入指定输出流,可以是文件流、stdout
或stderr
等。stderr
是标准错误流,它是无缓冲的,会立即输出到屏幕,而不是等待换行符才输出。
用法示例:
fputs("hello world", stdout); // 不会立即输出
fputs("hello world\n", stdout); // 立即输出
fputs("hello world", stderr); // 立即输出
与fgets()
一样,fputs()
不会主动操作换行符。如果希望立即输出,需要自己加上换行符\n
。
fputc() & putc()
将一个字符写入指定输出流,可以是文件流、stdout
或stderr
等。
用法示例:
char c = 'q';
fputc(c, stdout);
c = '\n';
putc(c, stdout);
fputc()
和putc()
只是把字符写入stdout
,没有任何额外操作。因此如果希望立即输出,需要自己加上换行符\n
。
putchar()
将一个字符写入到标准输出流stdout
。
用法示例:
char c = 'x';
putchar(c);
同上,putchar()
不操作换行符。如果希望立即输出,需要自己加上换行符\n
。
fflush()
该函数的功能是强制刷新缓冲区,将数据立即写到对应的文件(或设备)。其参数可以是文件流指针,也可以是stdout
。
用法示例:
fputs("Hello World!", stdout);
fflush(stdout);
while (1);
上面的程序在进入死循环前,会输出Hello World!
字符串到屏幕。
注意:不能够将
fflush()
用于stdin
!这可能导致不可预料的后果。
C++ 标准输出
cout
cout
是ostream
类的一个实例。cout
是行缓冲的。
用法示例:
char str[] = "hello world";
cout << "str: " << str << endl;
插入endl
对象时,将立即清空输出缓冲区并显示,然后输出一个换行符\n
。
也有cout.put()
等函数,不常用。
cerr
cerr
是标准错误流,也是ostream
类的一个实例,并默认输出设备为显示屏上的命令行终端。它默认与stderr
同步。
cerr
是非缓冲的,即插入数据时会立即输出。
用法示例:
char str[] = "File open FAILED!";
cerr << "[Error] " << str;
clog
clog
是标准日志流,也是ostream
类的一个实例,并默认输出设备为显示屏上的命令行终端。
clog
是有缓冲的,但具体的刷新条件没有找到资料。实测以下代码是可以输出在屏幕的:
clog << "Failed!";
while(1){}
总结
标准输出相比输入来说较为简单。需要注意的是stdout
和cout
是行缓冲的,而stderr
和cerr
是无缓冲的。
C++ 流的高级用法请参考其他资料。