C/C++标准输入输出终极最全解析(不全捶我)
C/C++的一众输入输出函数的区别常常搞得人晕头转向,二者之中又以输入函数更加令人头疼。本文尝试整理C/C++的各种输入输出函数。
由于输入涉及空格、换行符的读取、忽略等问题,因此输入比输出更麻烦。所以本文将以输入为主线,对应的输出用法是类似的。
水平有限,如有疏漏,欢迎提出。
猛戳这里,在我的Github博客上阅读本篇文章(施工中,求踩orz)
文章目录
标准输入流 C 标准输入
C语言使用标准输入输出函数,需要包含头文件。而在 C++ 中,只要包含头文件,就完全可以使用这些 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类进行流操作,其封装很完善,也比较复杂,本文只介绍一部分。
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类对象**,使用时也需要包含头文件。
如果使用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++ 流的高级用法请参考其他资料。
我来说两句