为什么C++和C的标准输入输出不同步时,数据会混乱?同步会带来多大性能损失?为什么说这个损失通常不用太在乎?
0. 课堂视频
C++之“流”-第2课:和C输入输出的同步
1. 理解cin和cout的类型与创建过程
std::cout 是std::ostream类型的一个变量;而 std::ostream是std::basic_ostream<char>模板类的类型别名( typedef )。std::cin是std::isteram类型的一个变量,而std::istream是std::back_istream<cahr>模板类的类型别名。两个模板类中的 “char” 参数,表明二者都是基于普通 字符(char)作为最小输出或输入单位。如果改为 wchar_t,则以UNICODE字符串作为基本输入输出单位。
正如上一节课所说,std::ostream和std::istream都是抽象概念的流,无法直接创建对应的输出或输入流对象。
注意,C++中的“抽象”概念,和 Java 这样更加纯粹的“面向对象”的编程语言有所不同。Java 中的“抽象”,通常使用:“什么实事都不做,只负责定要求” 的接口(interface)表达。C++中有更多不同的方式来表达抽象概念,可以同样“什么事都不做,只负责定要求”的纯虚类,也可以是“做了很多基础的事,但禁用了特定构建方法”的方式。两种方式的共同表现是:不让用户直接创建对象。
std::ostream和std::istream对外开放的构造方法,都要求一个“流缓存区/stream_buf”入参。以输出为例,我们可以:
- 设计并实现一个内存输出缓存区,传入后以得到一个内存输出流的基本功能;
- 设计并实现一个文件输出缓存区,传入后以得到一个文件输出流的基本功能;
- 设计并实现一个网络输出缓存区,传入后以得到一个网络输出流的基本功能;
那么,为std::istream的构造函数传入一个键盘输入流缓存区,就能得到一个标准输入流,即std::cin;而为std::ostream传入一个屏幕输出流缓存区,就能得到一个标准输出流,即std::cout。但实际上,C++程序中的std::cin和std::cout对象,都是C++库自动创建出来的,并且不允许用户手工创建二者。为什么呢?因为对一个程序来说,标准输入设施应该只有一个,标准输出设施也应该只有一个;如果用户自己创建,就挡不住有用户创建出一打标准输入流或标准输出流了。
在Windows的控制台(console)或Linux下的终端(terminal)里,键盘被称为程序的标准输入设备,屏幕被称为程序的标准输出。并且,无论一台电脑接多少个键盘(少见),在逻辑上都会被当作一个键盘;同理,无论一台电脑接多少个屏幕(常见),在逻辑上也都会被当作一个屏幕。因此,cin 和 cout 本质上是一种“单例”,即整个程序中,只能一个标准输入流,一个标准输出流。
这种“一个程序里,某种类型的对象只有一个”的逻辑的实现,有专门的,称为“单例模式”的设计模式来实现。C++实现 cin 和 cout 的单例保障倒很简单:使用默认构造函数(没有任何入参)来创建特定对象,再把该默认构造函数的访问权限设置为私有(private)或保护的(protected),在gc++的实现中使用的是后者。标准库内部可以通过 “友元”加“派生”的方式,实现对基类受保护的默认构造函数的调用。
一旦调用std::ostream的默认构造函数,由于没有入参,也就没有外部传入的输出缓存区,此时标准库将自动创建标准输出流的缓存区,从而创建出标准输出流,即:std::cout对象。标准输入流的创建过程与此类型,同样是调用默认构造函数,然后自行创建、关联和键盘输入缓存区,从而创建出 std::cin。
以上调用过程都是在程序主函数 main() 开始之前,就执行完毕,因此我们的程序在一开始就能够方便地使用std::cin和std::cout。事实上,在 main() 之前我们就可以使用了。如果在 main() 函数之前就开始执特定代码,这是C++的另一个知识点,不在此讲解。
2. 数据输入输出次序冲突问题的出现
到现在,一切看起来很完美:cin和cout是自动创建的,并且各自只会有一份,不会冲突……但是,考虑到C++的一个重要的历史使命:兼容C语言,问题就来了——
C 语言有自己的输入输出机制,并且本质上,底层也需要用到输入或输出缓存区。上一节课我们说过,这个缓存区本质是一个数据队列,一个“有次序保障”的数据队列。C++尽管做到了一个程序只有一个C++输入流或一个C++输出流,但加上C的队列,现在,一个C++程序会有两个输入队列、两个输出队列。
这就有点像现实生活中的某种排队现象:入口或出口只有一个,但人们排了两条队,两条队伍各自的内部数据都有次序保障,但是,当门就在眼前,两条队伍如何通过一个门呢?无论是互相礼让,还是争先抢后,都无法保障复原原始的数据次序。
3. 混合输入,同步对比不同步
代码:
#include <cstdio> // C 语言的标准输入输出库
#include <iostream>using namespace std;int main()
{ios_base::sync_with_stdio(false); // 不同步!!!int i, j;scanf("%d", &i); //用C的方式输入 icin >> j; // 用C++的方式输入 jcout << i << ", " << j << endl;return 0;
}
4. 混合输出,同步对比不同步
代码:
#include <cstdio>
#include <iostream>using namespace std;int main()
{ios_base::sync_with_stdio(false);for (int i=0; i<3; ++i){printf("hello from printf!\n");cout << "hello from cout.\n";}return 0;
}
5. 同步与不同步性能对比
代码:
#include <ctime>
#include <cstdio>
#include <iostream>using namespace std;int main()
{ios::sync_with_stdio(false);clock_t beg = clock();for (int i=0; i<30000; ++i){cout << "hello world.";}clock_t end = clock();cout << "\n" << (end - beg) * 1000 / CLOCKS_PER_SEC << "ms." <<endl;return 0;
}
注意,程序使用 sync_with_stdio(false) 取消 C++和C的标准输入输出同步,该操作是不可逆的,即后续无法通过 sync_with_stdio(true) 恢复 同步。
6. 为什么不用太在乎C++标准输入输出的性能?
C++常用以写以下程序:
类型 | 典型应用 | 描述 | 大致占比 | 输入输出性能 |
---|---|---|---|---|
后台服务或底层组件 | 网络服务、防火墙 | 不直接面向用户,不使用标准输入输出 | 25% | 不在乎 |
GUI程序 | Photoshop、Office、游戏 | 使用系统GUI作为输入输出 | 20% | 不在乎 |
基础工具 | 命令行文件处理工具:压缩、图片处理 | 虽然在命令行运行,但几乎没有输入输出 | 15% | 不在乎 |
简单命令行工具 | 各类命令行客户端程序:libcurl,文件列表 | 低频使用标准输入输出 | 20% | 不在乎 |
非性能敏感的控制台应用 | 用户开发的简单命令行应用,比如处理excel表格 | 性能不敏感 | 15% | 不在乎 |
性能敏感的控制台应用 | 信息学竞赛程序、远程日志监控等 | 性能敏感,大量标准输入输出操作会影响程序性能 | 5% | 在乎 |