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); // 注意,传入的一定是变量的地址

对空白字符的处理:

  1. 缓冲区开头:丢弃空白字符(包括空格、Tab、换行符),直到第一个非空白字符才认为是第一个数据的开始。
  2. 缓冲区中间:开始读取第一个数据后,一旦遇到空白字符(非换行符), 就认为读取完毕一次。遇到的空白字符残留在缓冲区,直到下一次被读取或刷新。例如输入字符串this is test,则会被认为是3个字符串。
  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);

对空白字符的处理:

  1. 所有空格、Tab等空白字符均被读取,不忽略。
  2. 按下回车键时,缓冲区末尾的换行符被丢弃,字符串末尾没有换行符\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);

对空白字符的处理:

  1. 所有空格、Tab等空白字符均被读取,不忽略。
  2. 按下回车键时,缓冲区末尾的换行符也被读取,字符串末尾将有一个换行符\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);

对空白字符的处理:

  1. 所有空格、Tab、换行等空白字符,无论在缓冲区开头、中间还是结尾,均会被读取,不忽略。
  2. 因为只读取一个字符,所以如果输入多于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 风格的输入输出(在不手动取消同步的情况下)。

cinstdin一样是行缓冲,即遇到换行符时才会将数据同步到输入缓冲区。

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.getlinegetline(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()

字符串写入指定输出流,可以是文件流、stdoutstderr等。stderr是标准错误流,它是无缓冲的,会立即输出到屏幕,而不是等待换行符才输出。

用法示例:

fputs("hello world", stdout);    // 不会立即输出
fputs("hello world\n", stdout); // 立即输出
fputs("hello world", stderr); // 立即输出

fgets()一样,fputs()不会主动操作换行符。如果希望立即输出,需要自己加上换行符\n

fputc() & putc()

一个字符写入指定输出流,可以是文件流、stdoutstderr等。

用法示例:

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

coutostream类的一个实例。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){}

总结

标准输出相比输入来说较为简单。需要注意的是stdoutcout行缓冲的,而stderrcerr无缓冲的。

C++ 流的高级用法请参考其他资料。

C/C++标准输入输出函数终极最全解析(不全捶我)的更多相关文章

  1. ARM内核全解析,从ARM7,ARM9到Cortex-A7,A8,A9,A12,A15到Cortex-A53,A57

    转自: ARM内核全解析,从ARM7,ARM9到Cortex-A7,A8,A9,A12,A15到Cortex-A53,A57 前不久ARM正式宣布推出新款ARMv8架构的Cortex-A50处理器系列 ...

  2. Android系统init进程启动及init.rc全解析

    转:https://blog.csdn.net/zhonglunshun/article/details/78615980 服务启动机制system/core/init/init.c文件main函数中 ...

  3. Google Maps地图投影全解析(3):WKT形式表示

    update20090601:EPSG对该投影的编号设定为EPSG:3857,对应的WKT也发生了变化,下文不再修改,相对来说格式都是那样,可以到http://www.epsg-registry.or ...

  4. C#系统缓存全解析(转载)

    C#系统缓存全解析 对各种缓存的应用场景和方法做了很详尽的解读,这里推荐一下 转载地址:http://blog.csdn.net/wyxhd2008/article/details/8076105

  5. 【凯子哥带你学Framework】Activity界面显示全解析

    前几天凯子哥写的Framework层的解析文章<Activity启动过程全解析>,反响还不错,这说明“写让大家都能看懂的Framework解析文章”的思想是基本正确的. 我个人觉得,深入分 ...

  6. iOS Storyboard全解析

    来源:http://iaiai.iteye.com/blog/1493956 Storyboard)是一个能够节省你很多设计手机App界面时间的新特性,下面,为了简明的说明Storyboard的效果, ...

  7. 【转载】Fragment 全解析(1):那些年踩过的坑

    http://www.jianshu.com/p/d9143a92ad94 Fragment系列文章:1.Fragment全解析系列(一):那些年踩过的坑2.Fragment全解析系列(二):正确的使 ...

  8. (转)ASP.NET缓存全解析6:数据库缓存依赖

    ASP.NET缓存全解析文章索引 ASP.NET缓存全解析1:缓存的概述 ASP.NET缓存全解析2:页面输出缓存 ASP.NET缓存全解析3:页面局部缓存 ASP.NET缓存全解析4:应用程序数据缓 ...

  9. jQuery&nbsp;Ajax&nbsp;实例&nbsp;全解析

    jQuery Ajax 实例 全解析 jQuery确实是一个挺好的轻量级的JS框架,能帮助我们快速的开发JS应用,并在一定程度上改变了我们写JavaScript代码的习惯. 废话少说,直接进入正题,我 ...

  10. jQuery Ajax 全解析

    转自:http://www.cnblogs.com/qleelulu/archive/2008/04/21/1163021.html 本文地址: jQuery Ajax 全解析 本文作者:QLeelu ...

随机推荐

  1. 《吐血整理》高级系列教程-吃透Fiddler抓包教程(26)-Fiddler如何抓取Android7.0以上的Https包-上篇

    1.简介 众所周知,假如设备是android 7.0+的系统同时应用设置targetSdkVersion >= 24的话,那么应用默认是不信任安装的Fiddler用户证书的,所以你就没法抓到应用 ...

  2. siteServer CMS知识点

    1.结构说明 (1)     网站目录说明: a. 一个SitesServer后台只能建立一个主站,但可以建立多个子站,主站目录就是项目的根目录: b. 而子站的目录呢?是在主站目录下建立相应名称的目 ...

  3. HDU4734 F(x) (数位DP)

    (如此简短的题目给人一种莫名的压迫感......) 题目中定义一个数的权值求解函数:F(x) = An * 2n-1 + An-1 * 2n-2 + ... + A2 * 2 + A1 * 1. 观察 ...

  4. SpringBoot(一) - SpringBoot 初识

    1.创建SpringBoot项目 1.1 使用Spring Initializr 的 Web页面创建项目 创建网址:https://start.spring.io/ 1.2 使用IDEA创建 省略: ...

  5. tensorflow-gpu版本安装及深度神经网络训练与cpu版本对比

    tensorflow1.0和tensorflow2.0的区别主要是1.0用的静态图 一般情况1.0已经足够,但是如果要进行深度神经网络的训练,当然还是tensorflow2.*-gpu比较快啦. 其中 ...

  6. ExceptionHandler配合RestControllerAdvice全局处理异常

    Java全局处理异常 引言 对于controller中的代码,为了保证其稳定性,我们总会对每一个controller中的代码进行try-catch,但是由于接口太多,try-catch会显得太冗杂,s ...

  7. 【linux】 第1回 linux运维基础

    目录 1. 运维的本质 2. 电脑与服务器 2.1 电脑的种类 2.2 服务器种类 2.3 服务器品牌 2.4 服务器尺寸 2.5 服务器内部组成 3. 磁盘阵列 4. 系统简介 5. 虚拟化 6. ...

  8. 0基础90分钟会用PS——GenJi笔记

    数码图像的相关基础概念 1.位图和矢量图 位图 也叫点阵图像,位图使用也称像素的一格一格的小点来描述图像,图放大后我们可以看到像素点 矢量图 根据几何特性来绘制图形,用线段和曲线描述图像,可以是一个一 ...

  9. linux系统配置文件或shell脚本批量注释

    1. 配置文件批量注释 1.1 批量注释 ① 进入命令行模式,按ctrl + v进入 visual block模式,键盘上下箭头选中多行,把需要注释的行标记起来 ② 按大写字母I,再输入注释符:# ③ ...

  10. 【Spring系列】- 手写模拟Spring框架

    简单模拟Spring 生命不息,写作不止 继续踏上学习之路,学之分享笔记 总有一天我也能像各位大佬一样 一个有梦有戏的人 @怒放吧德德 分享学习心得,欢迎指正,大家一起学习成长! 前言 上次已经学习了 ...