11 输入和输出

11.1 引言

I/O流库提供了针对文本和数值的输入输出功能。这种格式化和非格式化的I/O都带有缓冲。它不但是类型安全的,而且可以象支持内置类型一样,支持用户自定义类型。

文件系统库提供了操作文件和目录的基本工具。

ostream类型将有类型的对象转换为字符(字节)流:

有类型的对象
字符(字节)流
'c'
123
(123,45)
ostream
流缓冲
某处

istream类型将字符(字节)流转换为有类型的对象:

有类型的对象
字符(字节)流
'c'
123
(123,45)
某处
流缓冲
istream

在istream和ostream上的任何操作,都是类型安全且类型敏感的,甚至可以被扩展,以支持用户自定义的类型。

其它形式的用户交互,比如图形用户界面(GUI),需要借助其它库的支持。这些库并不是标准库的一部分。

I/O流库可用于二进制数据的I/O,支持不同的字符类型和区域设置,还可使用高级缓冲策略。这些内容涉及有关I/O流的高级话题。

I/O流既可用于针对文件的I/O,也可用于针对内存的I/O,甚至可以向标准库的string对象,或从该对象中,写入或读取格式化的数据。

所有的I/O流类都带有析构函数,用于释放所持有的资源,如内存缓冲区、文件句柄等。它们是资源获取即初始化(RAII)的典范。

11.2 输出

<ostream>头文件中,I/O流库为所有的内置类型都定义了输出操作。而且,为用户自定义类型定义输出操作也很简单。“<<”是输出(流插入)操作符,作用于ostream类型的对象。I/O流库预定义了两个ostream类型的对象,cout表示标准输出流,cerr表示标准出错流。默认情况下,输出(写入)到cout的值被转换为一个字符序列。例如下面的代码可将整数10以十进制的形式输出:

此代码先将字符1放到标准输出流中,接着又放入字符0。

实现该功能的另一种等价的写法是:

不同类型值的输出可以一种很直观的方法组合在一起。例如:

执行这段代码,将在标准输出上看到:

重复书写“cout <<”难免令人感到厌倦。幸运的是,输出表达式的结果,即输出操作符(<<)函数的返回值,是对输出流对象的引用,因此可在此基础上继续执行输出操作。例如:

这段代码的执行结果,与上一段代码完全一样。

字符常量是被单引号引起的一个字符。注意,输出一个字符的结果就是其字符形式,而非其数值形式。例如:

字符'b'的值是98('b'的ASCII编码值是98),因此这段代码的输出是:a98c。

11.3 输入

<istream>头文件中,I/O流库为所有的内置类型都定义了输入操作。而且,为用户自定义类型定义输入操作也很简单。“>>”是输入(流提取)操作符,作用于istream类型的对象。“>>”操作符的右操作数决定了输入什么类型的值,以及输入的值被保存到哪里。I/O流库预定义了一个istream类型的对象——cin,表示标准输入流。默认情况下,被cin输入(读取)值是其右操作数对象的字符串形式。例如下面的代码可将一个整数以十进制形式输入到指定的变量中:

类似地,下面的代码可用于读取一个双精度浮点数:

与输出的情况类似,输入表达式的结果,即输入操作符(>>)函数的返回值,是对输入流对象的引用,因此可将多个输入操作链接起来。例如:

在输入的过程中,如果遇到与目标数据类型不相匹配内容,比如在读取整数的过程中遇到非数字字符,会终止读取。默认情况下,起始空白字符(空格、制表或换行)会被跳过,中间的空白字符会被作为多个数据项的分隔符。

如果需要输入一个字符串,最简单的方法是使用string类型的对象。例如:

默认情况下,空白字符会终止输入。例如输入“hello world”,实际读取到str中的只有“hello”。

getline函数可以读取一整行字符串,并丢弃位于行尾的换行符。例如:

因为位于行尾的换行符被丢弃了,所以接下来从cin的输入会从下一行开始。

使用格式化I/O通常不那么容易出错,且更有效率,比逐个操作字符的代码量要少得多。特别地,istream会把内存管理和范围检查处理得很好。借助格式化I/O,向字符串流和内存流写入格式化内容,或者从字符串流和内存流读取格式化内容,也会非常方便。

标准库的string对象有一个非常好的性质,它可以自动扩展空间以容纳被放入的内容。这样就无需预先计算被格式化后的文本需要占用多大内存,既不会浪费有限的内存资源,又能有效地规避内存溢出的风险。

11.4 I/O状态

每个iostream对象都用状态。可以通过对状态的检查,判断流操作是否成功。流状态检查常被作为值序列读取循环的控制条件。例如:

for循环持续从cin读取整数到ints中,直到遇到非整数字符(比如'a')为止。这段代码的关键是,“cin >> n”操作返回一个对cin对象的引用。该对象在布尔上下文中的值因其状态而异。若cin对象的状态正常,并可继续执行后续输入,则该布尔值为true,循环继续,否则为false,循环退出。

一般而言,I/O状态包含了执行读写操作所需要的全部信息,例如格式化信息、是否发生错误、是否读至文件末尾、使用何种缓冲区等。用户可以人为设置状态表示发生了错误,或在错误不严重的情况下予以清除。假设要将从标准输入读到的整数序列求和,遇到文件尾或“end”字符串则停止读取并输出计算结果,其它情况则报告错误:

通过Windows的标准输入模拟文件尾的方法是,于新行开始处按下<Ctrl+C>组合键。

11.5 用户自定义类型的I/O

除了支持内置类型和标准库string对象的I/O,iostream还允许程序员为自己的类型定义I/O操作。例如有一个名为Entry的自定义类型,表示电话本中的一个条目,包含姓名和电话号码两个数据项:

可以定义一个针对该类型的输出操作符函数,以形如“{"Zhang Fei", 13910110072}”的格式打印该类型的对象:

一个用户自定义的输出操作符函数,名为“operator<<”,接受输出流对象的引用作为其第一个参数,第二个参数为所要输出的自定义类型对象的常引用,根据所期望的输出格式编写函数体中的代码,最后再返回输出流对象的引用。这样就可以象输出任何内置类型的数据一样输出Entry类型的对象了。例如:

相对于输出,输入操作符函数的定义则要复杂得多,它必须检查格式是否正确,并处理可能发生的错误。例如:

一个用户自定义的输入操作符函数,名为“operator>>”,接受输入流对象的引用作为其第一个参数,第二个参数为所要输入的自定义类型对象的引用,根据所要求的输入格式编写函数体中的代码,最后再返回输入流对象的引用。这样就可以象输入任何内置类型的数据一样输入Entry类型的对象了。例如:

注意,形如“is >>”的操作会跳过空白字符,而get函数则不会:

上图中的“_”代表一个空白字符。上面的代码会忽略姓名双引号以外的空白字符,而姓名双引号以内的空白字符,则作为姓名字符串的一部分,被如实地读取出来。

11.6 输出格式化

I/O流库和format库提供了很多操作来控制输入和输出的格式。I/O流库与C++语言的历史一样悠久,它更专注于对算术数字的格式化。而format库则比较新,它由C++20提供,可以类似printf函数的风格,格式化各种类型数据的组合。输出格式化还提供对Unicode字符集的支持。

11.6.1 流式格式化

最简单的格式化方式,就是使用格式控制符(manipulator)。它们被定义在<ios>、<istream>、<ostream>和<iomanip>(带参数的格式控制符)等几个头文件中。例如下面的代码分别以默认(十进制)格式、十六进制格式、八进制格式和十进制格式,输出一个整数:

也可以显式设置浮点数的输出格式。例如:

精度是在显示浮点数时用于控制数字位数的一个整数。

格式控制符格式显示例如精度
fixed定点小数整数部分、小数点和小数部分123.456000小数部分的位数
scientific科学计数法一位整数部分、小数点、小数部分、字符“e”和指数1.234560e+02小数部分的位数
defaultfloat默认根据可用空间的大小,自动选择最佳形式123.456最多显示总位数

精度的默认值为6,通过precision函数可以人为设置精度。浮点值并非简单截断,而是遵循四舍五入规则。precision函数不影响整数显示。例如:

上面这些格式控制符,都是为流式格式化一系列值而设计的,因此它们都是有粘性的,即一经设置就会一直起作用,直到被显式改变。

还有一些格式控制符,可用于设置显示区域的大小(占几个字符宽度)和对齐方式等。

除了基本数字,“<<”操作符还支持诸如duration、time_point、year_month_date、weekday、month、zoned_time等日期时间类型数据的输出。例如:

这会输出:

标准库也同样定义了可用于其它数据类型的“<<”操作符,以支持诸如complex、bitset、错误代码、指针等类型数据的输出。流式I/O可以被扩展,即针对用户自定义的数据类型,提供专门的“<<”操作符函数。

C++17:十六进制浮点数字面量

11.6.2 printf风格的格式化

很多人坚信,printf是C语言里最受欢迎的函数,也是C语言获得成功的重要因素之一。例如:

类似这样“在一个格式化字符串后面紧跟一系列参数”的格式化风格,从BCPL时代就被C语言采纳,并一直延续至今。更有甚者,时至今日,仍有许多现代编程语言竞相步其后尘。自然,printf函数也是C++标准库的一部分,但它缺乏类型安全,可扩展性也不佳,不能很方便地处理用户自定义的数据类型。

<format>头文件中,标准库提供了类型安全但同样缺乏可扩展性的printf风格的格式化机制。这个机制的核心是一个名为format的函数,该函数的返回值是一个表示格式化结果的string对象。例如:

格式化字符串中的普通字符,会被原样放到该函数返回的string对象中。格式化字符串中形如“{ ... }”的占位符,指定了按位置对应的后续参数,将以何种方式被插入到该string对象中。

an int
{ ... }
and a string '
{ ... }
'.
123
Hello!
an int
123
and a string '
Hello!
'.

最简单的格式化占位符由一对形如“{}”的空花括号组成,它会将参数列表中的下一个参数直接以默认“<<”操作符的方式输出到结果字符串中。上例中的整数字面量123,输出为其十进制形式的“123”,而字符串字面量“Hello!”,也直接输出为其本来的形式“Hello!”。

在表示格式化占位符的一对花括号中,添加其它格式化标记,可以刻画更多格式化细节。例如下面的代码分别以默认(十进制)格式、十六进制格式、八进制格式、十进制格式和二进制格式,输出一个整数:

每个格式化标记必须以“:”作为前缀。格式化标记“b”表示以二进制格式输出整数。流式格式化并不直接支持整数的二进制格式输出。

在默认情况下,格式化字符串中的占位符,与后面的参数,按顺序一一对应,也可以人为指定与每个占位符对应的参数。例如:

花括号中的数字代表参数的序号。为了维持最佳的C++风格,参数序号从零开始。借助这种方法,甚至可以多次使用同一个参数。例如:

按指定顺序对应参数的能力非常重要,尤其在需要对不同自然语言文本做格式化的场合,比如国际化。

针对浮点数的格式化标记包括:“f”表示定点小数格式、“e”表示科学计数法格式、“a”表示十六进制格式、“g”表示默认格式。例如:

输出结果与使用流式格式化的情形基本一致,除了十六进制格式浮点数开头没有“0x”前缀。

输出浮点数可以指定精度。例如:

与流式格式化不同,format函数的格式化标记并不具有粘性,因此需要为每个被输出的数据单独指定。

与流式格式化类似,在format函数的格式化标记中,也可以设置显示区域的大小和对齐方式等。此外,format函数也支持诸如duration、time_point、year_month_date、weekday、month、zoned_time等日期时间类型数据的格式化。例如:

此外,format函数提供了一种迷你语言,包含大约60种格式化标记,用以详尽地控制日期和时间的格式。例如:

这会输出:

所有与日期和时间有关的格式化标记都以“%”开头。

大量的格式化标记,在带来空前的灵活性的同时,也增加了更多的犯错误的机会。如果在运行时发生了格式化错误,format函数会抛出format_error异常。例如:

format函数接受变长参数表形式的被格式化数据,vformat函数则可以接受向量形式的被格式化数据。例如:

这更灵活。例如:

也更容易出错。例如:

与format函数直接返回格式化结果不同,format_to函数将格式化结果写入一个由迭代器定义的缓冲区。例如:

如果直接使用流的缓冲区或将缓冲区用于其它输出设备,可能会获得更加显著的性能提升。

C++20:基于format和vformat的printf风格格式化

11.7 流

标准库直接支持下列流:

此外,还可以定义自己的流,比如与特定通信信道关联的流。

所有标准库流都是不可复制的,因此只能以引用的形式传递流类型的参数和返回值。

所有标准库流都是模板化的类,其类型参数为某种形式的字符,比如char或者wchar_t。ostream和wostream实际上分别是basic_ostream<char>和basic_ostream<wchar_t>的别名。

11.7.1 标准流

标准库预定义了四个标准流对象:

11.7.2 文件流

<fstream>头文件中,标准库提供了用于读写文件的流:

打开文件,准备写入。例如:

通过检查流对象的状态判断文件是否被成功打开。

打开文件,准备读取。例如:

文件一旦被成功打开,就可以象普通ostream对象(如cout)一样,向ofs对象输出(<<)数据。例如:

或象普通istream对象(如cin)一样,从ifs对象输入(>>)数据。例如:

借助文件流,还可以设置文件的读写位置,或对文件的打开方式做更精细化的控制。

文件流并没有提供诸如修改文件名、删除文件、复制文件、访问目录等功能。这要涉及与文件系统有关的操作。

11.7.3 字符串流

<sstream>头文件中,标准库提供了用于读写字符串的流:

例如:

ostringstream中的内容,可以通过view或str函数,获得string_view类型的视图或string类型的拷贝。

ostringstream的一个最常见的应用,是先通过它对需要输出的数据进行格式化,然后再将格式化的结果渲染到GUI(图形用户界面)组件上。

<<
str
渲染
数据
ostringstream
string
GUI

反过来,也可以先从GUI组件上收集用户输入的字符串,并将其放入一个istringstream对象,再通过它格式化输入到用于存储数据的变量中。

收集
str
>>
GUI
string
istringstream
数据

stringstream既可用于从字符串中读取数据,也可用于将数据写入到字符串中。例如下面的函数用于在两种都有字符串表示的类型间实现转换:

“ws”是一个针对输入流的格式控制符,表示跳过连续的空白字符,直到遇到第一个非空白字符或文件尾为止。调用该函数,实现类型转换。例如:

只有当函数模板的类型参数无法被推断出来,同时又没有指定默认值时,才需要显式指定它。如果函数模板的模板参数表为空,则可以省略表示模板参数表边界的一对尖括号。

这个例子充分展现了,组合使用语言特性和标准库工具,提高代码通用性和易用性的方法。

11.7.4 内存流

从早期的C++开始,就有为用户设计的内存流,直接通过流来读写内存。这类流的最古老的实例,比如strstream,数十年前就已经被废弃了。作为它们的替代品,ospanstream、ispanstream和spanstream,在C++23之前还没有被正式吸纳为官方标准。尽管如此,它们在很多C++实现中已经可以使用了。在GitHub上,也能找到它们的第三方实现。

ospanstream的行为与ostringstream非常相似,初始化方法也基本一致,除了ospanstream可以接受span而非string作为构造参数。例如:

如果在插入流的过程中发生了缓冲区溢出,那么流状态将会变为failure。

类似地,ispanstream的行为与istringstream也很相似。例如:

11.7.5 同步流

在多线程系统中,对I/O流的操作可能会变得非常不可靠,除非:

osyncstream保证每个针对它的写操作都能顺利完成,且结果符合预期,即便有其它线程也试图写入。例如:

当该函数在多个线程中被同时调用时,可能引发竞争,最终导致奇怪的输出。借助osyncstream,可以避免这种情况的发生。例如:

当该函数在多个线程中被同时调用时,不会相互影响。但如果其它线程仍然直接使用cout,则因竞争而导致的混乱依然会发生。因此,要么在所有线程中统一使用osyncstream,要么借助其它同步机制,确保任何时候只有一个线程写入输出流。

为多线程建立同步,需要一些技巧。只要有可能,应尽量避免在多线程间共享对象,包括I/O流对象,如cout等。

11.8 C风格的I/O

C++标准库继承了C标准库的I/O函数,如printf、scanf等。这些函数通常都不是类型安全的,而且也不支持用户自定义的数据类型,因此不建议使用它们。继续使用它们的唯一理由,可能源于对代码简洁性的追求,为此承担适度且可控的安全风险,从某方面讲也是值得的。

如果决定不再使用C风格的I/O,同时又很在意I/O的性能,可执行如下调用:

默认情况下,标准库的iostream对象,如cin、cout等,会因为要兼容C风格的I/O而牺牲一部分性能。执行以上调用,可以避免这方面的兼容性开销。

如果对printf风格的格式化情有独钟,不妨使用format函数取而代之。它类型安全、易读易用,且灵活迅速。

11.9 文件系统

大多是操作系统都有文件系统的概念,提供对文件形式的持久化存储的管理和访问。不幸的是,不同文件系统的属性和操作方式存在很大差异。为了解决这个问题,标准库的文件系统库,为大多数文件系统的大多数工具,提供了统一的封装。只要使用<filesystem>头文件,就能够以可移植的方式实现:

另外,C++的文件系统库也支持Unicode字符集。

C++17:文件系统

11.9.1 路径

考虑如下代码:

请注意,文件系统作为一种系统级的全局资源,可能会被多个程序(进程)同时访问。即使刚刚通过断言确定了文件存在,也不能保证接下来判断文件是否为普通文件时,该路径还依然有效。

path是一个非常复杂的类,有能力处理各种各样的字符集,并能与不同类型的操作系统和文件系统保持兼容。特别地,它还可以接受来自main函数的命令行参数。例如:

path直到使用时才被检查其有效性。过早检查,即便有效,也不能保证使用时还有效。

path可用于打开文件。例如:

除了path以外,<filesystem>头文件提供了用于遍历目录及查询文件属性的类:

用于遍历目录的类说明
path文件或目录的路径
filesystem_error访问文件系统过程中可能抛出的异常
directory_entry目录条目
directory_iterator用于遍历目录迭代器
recursive_directory_iterator用于遍历目录及其子目录的迭代器

下面的代码用于遍历给定目录及其子目录中的所有条目:

如果不想遍历给定目录下子目录中的内容,可以使用directory_iterator。如果想以字典顺序输出目录条目,可以将遍历所得路径字符串放入一个vector,排序后再输出。

path类提供了很多有用的公有操作:

path类的公有操作说明
value_type符合文件系统自然编码的字符类型,POSIX系统为char,Windows系统为wchar_t
string_typestd::basic_string<value_type>的别名
const_iteratorconst双向迭代器,迭代器的元素类型value_type为path
iteratorconst_iterator的别名
p=q将q赋值给p
p/=q用系统定义的路径分隔符连接p和q
p+=q不使用路径分隔符,直接连接p和q
s=p.native()获得p的原生格式字符串的引用
s=p.string()获得p的原生格式字符串的副本
s=p.generic_string()获得p的通用格式字符串的副本
q=p.filename()获得p的文件名部分
q=p.stem()获得p中不带扩展名的文件名部分
q=p.extension()获得p的文件扩展名部分(含“.”)
i=p.begin()获得p的起始迭代器
i=p.end()获得p的终止迭代器
p==q、p!=q不等和相等性比较
p<q、p<=q、p>q、 p>=q基于字典序的小于、小于等于、大于、大于等于比较
os<<p、is>>p插入到输出流和从输入流提取
p=u8path(s)从UTF-8编码的string中生成path

(p和q是path类型的对象,s和i分别是string和迭代器类型的对象)

例如:

filename、stem、extension等函数的返回值都path类型对象,但可以被转换为string对象。如果将它们直接插入到输出流,会在实际的路径、文件名、扩展名字符串的两端添加一对双引号。例如:

的输出可能会是下面这个样子:

而如果写成:

则会输出:

这样的写法:

实际等价于:

但凡涉及到文件名约定、自然语言和字符编码的问题,都不会是个简单问题。标准库的文件系统库,通过适度的抽象,极大地简化了对这类问题的处理,而且具有很好的可移植性。

11.9.2 文件和目录

文件系统提供了许多针对文件和目录的操作。自然地,不同操作系统中的不同文件系统,提供了不同的操作集。标准库提供了一套可以操作任何文件系统的统一接口:

文件系统操作说明
exists(p)p所表示文件或目录是否存在
copy(p,q)从p向q复制文件或目录,出错抛异常
copy(p,q,e)从p向q复制文件或目录,出错输出错误码
copy_file(p,q)从p向q复制文件,出错抛异常
b=create_directory(p)创建新目录,p中的中间目录必须已经存在
b=create_directories(p)创建新目录,p中的中间目录如果不存在,则一并创建
p=current_path()获取当前工作目录
current_path(p)设置当前工作目录
s=file_size(p)获取文件大小(以字节为单位)
b=remove(p)删除文件或空目录

(p和q是path类型的对象,e、s和b分别是error_code、整型和布尔型的变量,b为true、false表示成功或失败)

许多操作拥有可接受更多参数(如操作系统权限描述字等)的重载版本。

与copy类似,所有操作其实都有两种报告错误的方式:抛出filesystem_error类型的异常,或输出error_code类型的错误码。如果操作失败属于正常情况,则使用错误码,而如果操作失败被视作一种意外,则最好抛出异常。

通常情况下,判断文件属性,最简单、直接的方法,就是调用查询函数。文件系统库可以识别一些常见的文件类型,并将它识别不了的类型统一标注为“其它类型”。常见的文件类型如下表所示:

用于判断文件类型的函数说明
is_block_file(p)p是一个块设备文件吗?
is_character_file(p)p是一个字符设备文件吗?
is_regular_file(p)p是一个普通文件吗?
is_directory(p)p是一个目录吗?
is_empty(p)p是一个空文件或目录吗?
is_symlink(p)p是一个符号链接文件吗?
is_fifo(p)p是一个有名管道文件吗?
is_socket(p)p是一个本地套接字文件吗?
is_other(p)p是一个其它类型的文件吗?
status_known(p)p是一个状态未知的文件吗?

(p是path或file_status类型的对象)

11.10 建议