加载中...
C++和C标准输入输出同步
第1节:“流”的基本概念与应用
第2节:C++和C标准输入输出同步
第3节:C++和C的格式化输入输出
第4节:重温文件流和字符串流
第5节:为自定义类型重载流操作
第6节:三大关联知识点综合强化复习
第7节:从零开始,实现日志流
第8节:“我的日志流”终版
课文封面

为什么C++和C的标准输入输出不同步时,数据会混乱?同步会带来多大性能损失?为什么说这个损失通常不用太在乎?

0. 视频

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++程序会有两个输入队列、两个输出队列。

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的方式输入 i cin >> j; // 用C++的方式输入 j cout << 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% 在乎