高级程序设计

23 - 输入输出

2020-04-30 14:00 CST
2020-05-02 22:17 CST
CC BY-NC 4.0

输入/输出概述

输入/输出(I/O)是程序的一个重要组成部分:

  • 程序运行所需要的数据往往要从外设得到
  • 程序的运行结果通常也要输出到外设中去

在C++中,输入输出操作不是语言定义的功能,而是由具体的实现作为标准库的功能来提供。

在C++中,输入输出是一种基于字节流的操作:

  • 在进行输入操作时,可把输入的数据看成逐个字节地从外设流入到计算机内部(内存);
  • 在进行输出操作时,则把输出的数据看成逐个字节地从内存流出到外设。

在C++的标准类库中,除了提供基于字节流的输入输出操作外,为了方便使用还提供了用于C++基本数据类型数据的输入/输出操作,在这些操作的内部实现了基本数据类型与字节流之间的转换。另外,还可以对<<>>进行重载,以实现对自定义类型的数据(对象)进行输入/输出操作。

C++输入输出的实现途径

  • 过程式:通过C语言保留下来的函数库中的函数来实现,如printfscanf等;
  • 面向对象:通过C++的I/O类库中的类/对象来实现,如coutcin、运算符等。

printfscanf的缺陷:

  • 类型不安全:不是强类型,不利于类型检查,会导致类型相关的运行错误。
  • 如格式串描述与数据不一致导致运行时刻出现错误(类型、个数不一致)

coutcin的优势:

  • 不需要额外制定数据的类型和个数,由数据本身决定,避免了类型和个数相关的错误。

I/O的分类

  • 面向控制台的I/O
    • 从标准输入设备(键盘等)获取数据
    • 把结果从标准输出设备(显示器等)输出
  • 面向文件的I/O
    • 从外存文件获取数据
    • 把结果保存到外存文件中
  • 面向字符串变量的I/O
    • 从程序中的字符串变量中获取数据
    • 把结果保存到字符串变量中

C++的I/O类库中基本的类

ios
  + istream
  | +-----------------+-- iostream
  | | ifstream        |     + fstream
  | \ istrstream      |     \ strstream
  |                   |
  \ ostream           |
    +-----------------+
    + ostream
    \ ostrstream

在进行输入/输出时,首先创建某个I/O类的对象,然后调用该对象类的成员函数进行基于字节流的输入/输出操作。

istream类和ostream以及它们的派生类分别重载了操作符抽取操作符>>和插入操作符<<,用它们可以进行基本类型数据的输入/输出操作。

面向控制台的I/O

I/O类库中预定义了四个I/O对象,可以直接利用这些对象进行控制台的输入/输出操作:

  • cin:标准输入(通常为键盘)
  • cout:标准输出(通常为显示器)
  • cerrclog:对应输出程序错误信息的设备(cerr不带缓冲,clog带缓冲,通常对应显示器,不受输出重定向的影响)

输出格式控制

为了对输出格式控制,可以通过输出一些操纵符(manipulator)来实现,例如:

#include <iostream>
#include <iomanip>
std::cout << std::hex << 10 << std::endl;

常用输出操作符:

  • endl:输出换行符并冲洗缓冲区
  • flush:使输出缓存中的内容立即输出
  • dec/oct/hex:修改输出的数字进制
  • setprecision(int n):设置浮点数的精度
  • setiosflags/resetiosflags(long flags):设置/取消输出格式,flags取值为ios::scientific(指数形式)、ios::fixed(小数形式)等

对于浮点数:

  • 当输出格式为ios::scientificios::fixed时,setprecision用于设置浮点数小数的位数;
  • 当输出格式为自动方式时,setprecision用于设置浮点数有效数字的个数,输出个数根据有效数字自动确定。

除了通过插入操作符进行输出以外,还可以用ostream类提供的一些基于字节流的操作来进行输出,如ostream::putostream::write等。

控制台输入

使用抽取操作符读入数据,在输入的各个数据之间用空白符[ \t\n]隔开。

  • 输入前先跳过空白符
  • 输入时碰到空白符或当前类型不允许的字符结束

还可以通过一些操作符来控制输入的行为,如setw(n)限制字符串读入的长度为$n-1$个字符(因为最后一个是\0)。

除了抽取操作符意外还可以使用istream类的基于字节流的成员函数输入,如istream::getistream::getlineistream::read等。

输入/输出操作符重载

class A;
friend istream &operator>>(istream &in, const A &a);
friend ostream &operator<<(ostream &out, const A &a);

派生类可以通过定义一个虚函数,在函数重载中调用虚函数来实现多态。

面向文件的I/O

需求:

  • 程序运行结果有时需要永久性地保存起来,以供其他程序或本程序下一次运行时使用。
  • 程序运行所需要的数据也常常需要从其他程序或本程序上一次运行所保存的数据中获得。

用于保存永久性数据的设备称为外部存储器,如磁盘、磁带、光盘等。在外存中保存数据的方式有文件、数据库等。(数据库不也是用文件保存的嘛XD)

文件的基本概念

在C++中,把文件看成是由一系列字节组成的字节串。对文件中数据的操作通常是逐个字节顺序进行,因此称为流式文件。

每个打开的文件都有一个隐藏的位置指针,它指出文件的当前读写位置。进行读/写操作时,每读入/写出一个字节,文件位置指针会自动往后移动一个字节的位置。

文件数据的存储方式

  • 文本方式:只包含可显示字符和有限的几个控制字符的编码,一般用于存储具有行结构的文本数据。
  • 二进制方式:包含任意的没有显式含义的纯二进制字节,一般用于存储无显式结构的数据。

文件的读写过程

  • 打开文件:把程序内部的一个表示文件的变量/对象和外部的一个具体文件关联起来,并创建内存缓冲区。
  • 文件读写:存取文件中的内容。
  • 关闭文件:把暂存在内存缓冲区的内容写入到文件中,并归还打开文件时申请的内存资源。

文件输出

县创建一个ofstream类的对象并建立与外部文件的联系。

// 直接方式
ofstream file("path_to_file", ios::out);
// 间接方式
ofstream file;
file.open("path_to_file", ios::out);

打开方式(二进制或):

  • ios::out:(默认打开方式)打开文件用于写操作。如果外部文件已经存在则先清除内容,否则先创建外部文件。
  • ios::app:打开文件用于添加(偏移量指针设置在文件末尾)。如果文件不存在则先创建文件。
  • ios::binary:按二进制方式打开方式。如果不按照二进制方式打开文件,而以文本方式打开文件,当输出\n时会根据平台自动对换行符进行转换,如Windows中会实际输出\r\n

打开文件时,还需要对文件打开操作进行判断:

if (!file.is_open()) ...;
if (file.fail()) ...;
if (!file) ...;

文件输出操作结束时,要使用成员函数close关闭文件,把文件内存缓冲区的内容写入到磁盘中。程序正常结束时系统也会自动关闭打开的文件。

文件输入

创建ifstream类对象并与外部文件建立联系。打开方式ios::inios::binary。打开文件后也需要判断打开是否成功。

读取数据过程中有时需要使用成员函数fail判断是否正确读入了数据(尤其是在文件末尾处)。

文件输入/输出

如果需要打开一个既能读入数据、也能输出数据的文件,需要创建fstream类的对象。

打开文件时,文件打开方式应该为ios::in | ios::out(可以在任意位置写入)或ios::in | ios::app(只能在文件末尾写入)。

要实现随机读写数据,需要显式指出读写的位置(调整偏移量):

istream &istream::seekg(<位置>);
istream &istream::seekg(<偏移量>, <参照位置>);
streampos istream::tellg(); // 获得指针位置

ostream &ostream::seekp(<位置>);
ostream &ostream::seekp(<偏移量>, <参照位置>);
streampos ostream::tellp(); // 获得指针位置

参照位置可以是ios::begios::curios::end