第10章 C++输入输出流总结

C++输入流和输出流

C 语言它有一整套完成数据读写(I/O)的解决方案:

  • 使用 scanf()、gets() 等函数从键盘读取数据,使用 printf()、puts() 等函数向屏幕上输出数据;
  • 使用 fscanf()、fgets() 等函数读取文件中的数据,使用 fprintf()、fputs() 等函数向文件中写入数据。

要知道,C 语言的这套 I/O 解决方案也适用于 C++ 程序,但 C++ 并没有“偷懒”,它自己独立开发了一套全新的 I/O 解决方案,

  • “标准 I/O”,
  • “文件 I/O”,

本质上来说,C++ 的这套 I/O 解决方案就是一个包含很多类的类库(作为 C++ 标准库的组成部分),这些类常被称为“流类”。

C++ 的开发者认为,数据输入和输出的过程也是数据传输的过程,数据像水一样从一个地方流动到另一个地方,所以, C++ 中,将此过程称为“流”,实现此过程的类称为“流类”

  • istream:常用于接收从键盘输入的数据;
  • ostream:常用于将数据输出到屏幕上;
  • ifstream:用于读取文件中的数据;
  • ofstream:用于向文件中写入数据;
  • iostream:继承自 istream 和 ostream 类,因为该类的功能兼两者于一身,既能用于输入,也能用于输出;
  • fstream:兼 ifstream 和 ofstream 类功能于一身,既能读取文件中的数据,又能向文件中写入数据。

cerr 常用来输出警告和错误信息给程序的使用者,clog 常用来输出程序执行过程中的日志信息(此部分信息只有程序开发者看得到,不需要对普通用户公开)。

cout、cerr 和 clog 之间的区别如下:

  1. cout 除了可以将数据输出到屏幕上,通过重定向,还可以实现将数据输出到指定文件中;而 cerr 和 clog 都不支持重定向,它们只能将数据输出到屏幕上
  2. cout 和 clog 都设有缓冲区,即它们在输出数据时,会先将要数据放到缓冲区,等缓冲区满或者手动换行(使用换行符 '\n' 或者 endl)时,才会将数据全部显示到屏幕上;而 cerr 则不设缓冲区,它会直接将数据输出到屏幕上

值得一提的是,类似 cin、cout、cerr 和 clog 这样,它们都是 C++ 标准库的开发者创建好的,可以直接拿来使用,这种在 C++ 中提前创建好的对象称为内置对象。实际上,<iostream> 头文件中还声明有处理宽字符的 4 个内置对象,分别为 wcin、wcout、wcerr 以及 wclog

istream 和 ostream 类提供了很多实用的函数,cin、cout、cerr 和 clog 作为类对象,当然也能调用。  表 1 罗列了 cin 对象常用的一些成员方法以及它们的功能:

表 1 C++ cin 输入流对象常用成员方法
成员方法名功能
getline(str,n,ch) 从输入流中接收 n-1 个字符给 str 变量,当遇到指定 ch 字符时会停止读取,默认情况下 ch 为 '\0'
get() 从输入流中读取一个字符,同时该字符会从输入流中消失
gcount()  返回上次从输入流提取出的字符个数,该函数常和 get()、getline()、ignore()、peek()、read()、readsome()、putback() 和 unget() 联用。
peek() 返回输入流中的第一个字符,但并不是提取该字符
putback(c)  将字符 c 置入输入流(缓冲区)
ignore(n,ch) 从输入流中逐个提取字符,但提取出的字符被忽略,不被使用,直至提取出 n 个字符,或者当前读取的字符为 ch
operator>> 重载 >> 运算符,用于读取指定类型的数据,并返回输入流对象本身。

表 2 罗列了 cout、cerr 和 clog 对象常用的一些成员方法以及它们的功能:

表 2 C++ cout 输出流对象常用成员方法
成员方法名功能
put() 输出单个字符
write() 输出指定的字符串
tellp() 用于获取当前输出流指针的位置
seekp() 设置输出流指针的位置
flush() 刷新输出流缓冲区
operator<< 重载 << 运算符,使其用于输出其后指定类型的数据。
#include <iostream>
using namespace std;
int main() {
    char url[30] = {0};
    //读取一行字符串
    cin.getline(url, 30);
    //输出上一条语句读取字符串的个数
    cout << "读取了 "<<cin.gcount()<<" 个字符" << endl;
    //输出 url 数组存储的字符串
    cout.write(url, 30);
    return 0;
}

 (个人:cin.getline会自动的在读取的n-1个字符末尾添加上'\0')

C++ cout.put():输出单个字符

put() 方法专用于向输出流缓冲区中添加单个字符,其语法格式如下:

ostream&put(char c);

其中,参数 c 为要输出的字符。

可以看到,该函数会返回一个 ostream 类的引用对象,可以理解返回的是 cout 的引用。这意味着,我们可以像下面这样使用 put() 函数:

cout.put(c1).put(c2).put(c3);

put() 函数的参数可以是字符或字符的 ASCII 代码(也可以是一个整型表达式)。  

cout.put(65 + 32);
cout.put(97);

上面两行代码都输出字符 a,因为 97 是字符 a 的 ASCII 代码。(个人:可以看到,cout.put不会自动换行)  

除了使用 cout.put() 函数输出一个字符外,还可以用 putchar() 函数输出一个字符。putchar() 函数是C语言中使用的,在 <stdio.h> 头文件中定义,C++保留了这个函数,在 <iostream> 头文件中定义。  

C++ cout.write():输出字符串

write() 成员方法专用于向输出流缓冲区中添加指定的字符串,初学者可以简单的理解为输出指定的字符串。其语法格式如下: 

ostream&write(const char * s,streamsize n);

其中,s 用于指定某个长度至少为 n 的字符数组或字符串;n 表示要输出的前 n 个字符。

可以看到,该函数会返回一个 ostream 类的引用对象,可以理解返回的是 cout 的引用。这意味着,我们可以像下面这样使用 write() 方法:

cout.write(c1, 1).write(c2, 2).write(c3, 3);

使用这种方式我们可以连续输出。  

#include <iostream>
using namespace std;
int main() {
    const char * str = "https://www.baidu.com";
    cout.write(str, 4);
    return 0;
}

#include <iostream>
using namespace std;
int main() {
    cout.write("https://", 8).write("abc", 3).write(".efg", 4);
    return 0;
}

C++ cout.tellp()和cout.seekp()方法

通过前面章节的学习我们知道,无论是使用 cout 输出普通数据,用 cout.put() 输出指定字符,还是用 cout.write() 输出指定字符串,数据都会先放到输出流缓冲区,待缓冲区刷新,数据才会输出到指定位置(屏幕或者文件中)。  值得一提的是,当数据暂存于输出流缓冲区中时,我们仍可以对其进行修改。ostream 类中提供有 tellp() 和 seekp() 成员方法,借助它们就可以修改位于输出流缓冲区中的数据

C++ tellp()成员方法  

In C++ file handling, the tellp() function is used with output streams, and returns the current put position of the pointer in the stream. It returns an integer data type, representing the current position of the stream pointer,其语法格式如下: 

#include <iostream>
#include <sstream>
int main()
{
    std::ostringstream s;
    std::cout << s.tellp() << '\n';
    s << 'h';
    std::cout << s.tellp() << '\n';
    s << "ello, world ";
    std::cout << s.tellp() << '\n';
    s << 3.14 << '\n';
    std::cout << s.tellp() << '\n' << s.str();
}

C++ seekp()成员方法

Sets the position where the next character is to be inserted into the output stream. 其他的解释为:

Sets the output position indicator of the current associated streambuf object.

seekp() 方法有如下 2 种语法格式:
//指定下一个字符存储的位置
ostream& seekp (streampos pos);
//通过偏移量间接指定下一个字符的存储位置   
ostream& seekp (streamoff off, ios_base::seekdir way);

其中,各个参数的含义如下:

pos - absolute position to set the output position indicator to.
off - relative position (positive or negative) to set the output position indicator to.
dir - defines base position to apply the relative offset to. It can be one of the following constants:
Constant Explanation
beg the beginning of a stream
end the ending of a stream
cur the current position of stream position indicator

同时,seekp() 方法会返回一个引用形式的 ostream 类对象,这意味着 seekp() 方法可以这样使用:  

cout.seekp(23) << "当前位置为:" << cout.tellp();

例子:(个人:通过这个例子,可以看出,ios_base::beg=0,ios_base::end为流最后一个元素的下一个位置)

#include <sstream>
#include <iostream>

int main()
{
    std::ostringstream os("hello, world");
    os.seekp(7);
    os << 'W';
    os.seekp(0, std::ios_base::end);
    os << '!';
    os.seekp(0,std::ios_base::beg);
    os << 'H';
    std::cout << os.str() << '\n';
}

C++ cout格式化输出

C++ 通常使用 cout 输出数据,和 printf() 函数相比,cout 实现格式化输出数据的方式更加多样化。

  • 一方面,cout 作为 ostream 类的对象,该类中提供有一些成员方法,可实现对输出数据的格式化
  • 另一方面,为了方面用户格式化输出数据,C++ 标准库专门提供了一个 <iomanip> 头文件,该头文件中包含有大量的格式控制符(严格意义上称为“流操纵算子”),使用更加方便。 (个人:manipulation,操纵)

C++ cout成员方法格式化输出

ostream 类中还包含一些可实现格式化输出的成员方法,这些成员方法都是从 ios 基类(以及 ios_base  类)中继承来的,cout(以及 cerr、clog)也能调用。  表 1 罗列了 ostream 类中可实现格式化输出的常用成员方法,以及它们各自的用法。 

表 1 ostream 类的成员方法
成员函数说明
flags(fmtfl) 当前格式状态全部替换为 fmtfl。注意,fmtfl 可以表示一种格式,也可以表示多种格式。
precision(n) 设置输出浮点数的精度为 n。
width(w) 指定输出宽度为 w 个字符。
fill(c) 在指定输出宽度的情况下,输出的宽度不足时用字符 c 填充(默认情况是用空格填充)。
setf(fmtfl, mask) 在当前格式的基础上,追加 fmtfl 格式,并删除 mask 格式。其中,mask 参数可以省略。
unsetf(mask) 在当前格式的基础上,删除 mask 格式。

其中,对于表 1 中 flags() 函数的 fmtfl 参数、setf() 函数中的 fmtfl 参数和 mask 参数以及 unsetf() 函数 mask 参数,可以选择表 2 中列出的这些值。

表 2 fmtfl 和 mask 参数可选值
标 志作 用
ios::boolapha 把 true 和 false 输出为字符串
ios::left 输出数据在本域宽范围内向左对齐
ios::right 输出数据在本域宽范围内向右对齐
ios::internal 数值的符号位在域宽内左对齐,数值右对齐,中间由填充字符填充
ios::dec 设置整数的基数为 10
ios::oct 设置整数的基数为 8
ios::hex 设置整数的基数为 16
ios::showbase 强制输出整数的基数(八进制数以 0 开头,十六进制数以 0x 打头)
ios::showpoint 强制输出浮点数的小点和尾数 0
ios::uppercase 在以科学记数法格式 E 和以十六进制输出字母时以大写表示
ios::showpos 对正数显示“+”号
ios::scientific 浮点数以科学记数法格式输出
ios::fixed 浮点数以定点格式(小数形式)输出
ios::unitbuf 每次输出之后刷新所有的流

举个例子:

#include <iostream>
using namespace std;
int main()
{
    double a = 1.23;
    //设定后续输出的浮点数的精度为 4
    cout.precision(4);
    cout <<"precision: "<< a << endl;
    //设定后续以科学计数法的方式输出浮点数
    cout.setf(ios::scientific);
    cout <<"scientific:"<< a << endl;
    return 0;
}

值得一提的是,当调用 unsetf() 或者 2 个参数的 setf() 函数时,为了提高编写代码的效率,可以给 mask 参数传递如下 3 个组合格式:

  • ios::adjustfield:等价于 ios::left | ios::right | ios::internal;
  • ios::basefield:等价于 ios::dec | ios::oct | ios::hex;
  • ios::floatfield:等价于 ios::scientific | ios::fixed。

举个例子:

#include <iostream>
using namespace std;
int main()
{
    double f = 123;
    //设定后续以科学计数法表示浮点数
    cout.setf(ios::scientific);
    cout << f << '\n';
//    //删除之前有关浮点表示的设定
    cout.unsetf(ios::floatfield);
    cout << f;
    return 0;
}

使用流操纵算子格式化输出

表 3 罗列了 <iomanip> 头文件中定义的一些常用的流操纵算子,它们都可用于格式化输出。 

表 3 C++ 流操纵算子
流操纵算子作  用
*dec 以十进制形式输出整数 常用
hex 以十六进制形式输出整数

oct

================

以八进制形式输出整数

====================================================

fixed 以普通小数形式输出浮点数

scientific

================

以科学计数法形式输出浮点数

=====================================================

left 左对齐,即在宽度不足时将填充字符添加到右边

*right

================

右对齐,即在宽度不足时将填充字符添加到左边

=====================================================

setbase(b) 设置输出整数时的进制,b=8、10 或 16
setw(w) 指定输出宽度为 w 个字符,或输入字符串时读入 w 个字符。注意,该函数所起的作用是一次性的,即只影响下一次 cout 输出
setfill(c) 在指定输出宽度的情况下,输出的宽度不足时用字符 c 填充(默认情况是用空格填充)

setprecision(n)

================

设置输出浮点数的精度为 n。

在使用非 fixed 且非 scientific 方式输出的情况下,n 即为有效数字最多的位数,如果有效数字位数超过 n,则小数部分四舍五人,或自动变为科学计 数法输出并保留一共 n 位有效数字。

在使用 fixed 方式和 scientific 方式输出的情况下,n 是小数点后面应保留的位数。

=====================================================

setiosflags(mask) 在当前格式状态下,追加 mask 格式,mask 参数可选择表 2 中的所有值。

resetiosflags(mask)

================

在当前格式状态下,删除 mask 格式,mask 参数可选择表 2 中的所有值。

=====================================================

boolapha 把 true 和 false 输出为字符串 不常用

*noboolalpha

================

把 true 和 false 输出为 0、1

=====================================================

showbase 输出表示数值的进制的前缀

*noshowbase

================

不输出表示数值的进制.的前缀

=====================================================

showpoint 总是输出小数点

*noshowpoint

================

只有当小数部分存在时才显示小数点

====================================================

showpos 在非负数值中显示 +

*noshowpos

===============

在非负数值中不显示 +

===================================================

uppercase 十六进制数中使用 A~E。若输出前缀,则前缀输出 0X,科学计数法中输出 E

*nouppercase

===============

十六进制数中使用 a~e。若输出前缀,则前缀输出 0x,科学计数法中输出 e。

==================================================

internal 数值的符号(正负号)在指定宽度内左对齐,数值右对 齐,中间由填充字符填充。

注意:“流操纵算子”一栏带有星号 * 的格式控制符,默认情况下就会使用。例如在默认情况下,整数是用十进制形式输出的,等效于使用了 dec 格式控制符。  

和 cout 成员方法的用法不同,下面程序演示了表 3 中这些格式控制符的用法:

#include <iostream>
#include <iomanip>
using namespace std;
int main()
{
    //以十六进制输出整数
    cout << hex << 27 << endl;
    //删除之前设定的进制格式,以默认的 10 进制输出整数
    cout << resetiosflags(ios::basefield)<< 27 << endl;

    double a = 123;
    //以科学计数法的方式输出浮点数
    cout << scientific << a << endl;
    //删除之前设定的科学计数法的方法
    cout << resetiosflags(ios::scientific) << a << endl;
    return 0;
}

注意,在设置了某标志,又要设置其他与之矛盾的标志时,就应该用 resetiosflags 清除原先的标志。 

C++输入输出重定向(3种方法)

C++ freopen()函数实现重定向 

 freopen() 定义在<stdio.h>头文件中,是 C 语言标准库中的函数,专门用于重定向输入流(包括 scanf()、gets() 等)和输出流(包括 printf()、puts() 等)。值得一提的是,该函数也可以对 C++ 中的 cin 和 cout 进行重定向

#include <iostream>    //cin、cout
#include <string>      //string
#include <stdio.h>     //freopen
using namespace std;
int main()
{
    string name, url;
    //将标准输入流重定向到 in.txt 文件
    freopen("in.txt", "r", stdin);
    cin >> name >> url;

    //将标准输出重定向到 out.txt文件
    freopen("out.txt", "w", stdout);
    cout << name << "\n" << url;
    return 0;
}

C++ rdbuf()成员函数实现重定向 

rdbuf() 函数的语法格式有 2 种,分别为:  

streambuf * rdbuf() const;
streambuf * rdbuf(streambuf * sb);

streambuf 是 C++ 标准库中用于表示缓冲区的类,该类的指针对象用于代指某个具体的流缓冲区。  

  • 其中,第一种语法格式仅是返回一个指向当前流缓冲区的指针
  • 第二种语法格式用于将 sb 指向的缓冲区设置为当前流的新缓冲区,并返回一个指向旧缓冲区的对象。  
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
    //打开 in.txt 文件,等待读取
    ifstream fin("in.txt");
    //打开 out.txt 文件,等待写入
    ofstream fout("out.txt");
    streambuf *oldcin;
    streambuf *oldcout;
    char a[100];
    //用 rdbuf() 重新定向,返回旧输入流缓冲区指针
    oldcin = cin.rdbuf(fin.rdbuf());
    //从input.txt文件读入
    cin >> a;
    //用 rdbuf() 重新定向,返回旧输出流缓冲区指针
    oldcout = cout.rdbuf(fout.rdbuf());
    //写入 out.txt
    cout << a << endl;

    //还原标准输入输出流
    cin.rdbuf(oldcin); // 恢复键盘输入
    cout.rdbuf(oldcout); //恢复屏幕输出
    //打开的文件,最终需要手动关闭
    fin.close();
    fout.close();
    return 0;
}

C++通过控制台实现重定向

以上 2 种方法,都是从代码层面实现输入输出流的重定向。除此之外,我们还可以通过控制台实现输入输出的重定向。在控制台中使用 > 或者 < 实现重定向的方式,DOS、windows、Linux 以及 UNIX 都能自动识别

#include <iostream>
#include <string>
using namespace std;
int main()
{
    string name, book;
    cin >> name >> book;
    cout << name << '\n' << book;
    return 0;
}

C++管理输出缓冲区

有了缓冲机制,操作系统就可以将程序的多个输出操作组合成单一的系统级写操作。由于设备的写操作可能很耗时,允许操作系统将多个输出操作组合为单一的设备写操作可以带来很大的性能提升。 

导致缓冲刷新(数据真正写到输出设备或文件)的原因有很多:

  • 程序正常结束,作为 main() 函数的 return 操作的一部分,缓冲刷新被执行
  • 缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区。
  • 我们可以使用操纵符来显式刷新缓冲区

    • endl,它完成换行并刷新缓冲区的工作;
    • flush 刷新缓冲区,但不输出任何额外的字符
    • ends向缓冲区插入一个空字符,然后刷新缓冲区
  • 如果想在每次输出操作后都刷新缓冲区,我们可以使用 unitbuf 操纵符,它告诉流在接下来的每次写操作之后都进行一次 flush 操作。而nounitbuf 操作符则重置流, 使其恢复使用正常的系统管理的缓冲区刷新机制。默认情况下,对cerr是设置unitbuf的,因此写到 cerr 的内容都是立即刷新的
    cout << unitbuf;  //所有输出操作后都会立即刷新缓冲区
    //任何输出都立即刷新,无缓冲
    cout << nounitbuf;  //回到正常的缓冲方式  
  • 一个输出流可能被关联到另一个流。在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。例如,默认情况下,cin 和 cerr 都关联到 cout。因此,读 cin 或写 cerr 都会导致 cout 的缓冲区被刷新。交互式系统通常应该关联输入流和输出流。这意味着所有输出,包括用户提示信息,都会在读操作之前被打印出来。tie() 函数可以用来绑定输出流,它有两个重载的版本:

     第一个版本不带参数,返冋指向关联的输出流的指针。如果本对象当前关联到一个输出流,则返回的就是指向这个流的指针,如果对象未关联到流,则返回空指针。  tie() 的第二个版本接受一个指向 ostream 的指针,将自己关联到此 ostream,即,x.tie(&o) 将流 x 关联到输出流 o我们既可以将一个 istream 对象关联到另一个 ostream,也可以将一个 ostream 关联到另一个 ostream

     在这段代码中,为了将一个给定的流关联到一个新的输出流,我们将新流的指针传递给了 tie()。为了彻底解开流的关联,我们传递了一个空指针每个流同时最多关联到一个流, 但多个流可以同时关联到同一个ostream

如果程序异常终止,输出缓冲区是不会被刷新的。当一个程序崩溃后,它所输出的数据很可能停留在输出缓冲区中等待打印。  当调试一个已经崩溃的程序时,需要确认那些你认为已经输出的数据确实已经刷新了。否则,可能将大量时间浪费在追踪代码为什么没有执行上,而实际上代码已经执行了,只是程序崩溃后缓冲区没有被刷新,输出数据被挂起没有打印而已

cin.get():C++读取单个字符

get() 是 istream 类的成员函数,它有多种重载形式,点击这里,不过本文只介绍最简单最常用的一种:

int get();
  • 此函数从输入流中读入一个字符,返回值就是该字符的 ASCII 码
  • 如果碰到输入的末尾,则返回值为 EOF。  EOF 是 End of File 的缩写。istream 类中从输入流(包括文件)中读取数据的成员函数,在把输入数据都读取完后再进行读取,就会返回 EOFEOF 是在 iostream 类中定义的一个整型常量,值为 -1。 
  • get() 函数不会跳过空格、制表符、回车等特殊字符,所有的字符都能被读入。  
#include <iostream>
using namespace std;
int main()
{
    int c;
    while ((c = cin.get()) != EOF)
        cout.put(c);
    return 0;
}

运行结果:  

http://www.abc.def/computer/programme/vc/↙
http://www.abc.def/computer/programme/vc/
C++ Tutorial↙
C++ Tutorial
^Z↙

程序中的变量 c 应为 int 类型,而不能是 char 类型。在输入流中碰到 ASCII 码等于 0xFF 的字符时(个人:ASCII 码使用指定的7 位或8 位二进制数组合来表示128 或256 种可能的字符。标准ASCII 码也叫基础ASCII码,使用7 位二进制数(剩下的1位二进制为0)来表示所有的大写和小写字母,数字0 到9、标点符号,以及在美式英语中使用的特殊控制字符 。后128个称为扩展ASCII码。许多基于x86的系统都支持使用扩展(或“高”)ASCII。扩展ASCII 码允许将每个字符的第8 位用于确定附加的128 个特殊符号字符、外来语字母和图形符号),cin.get() 返回 0xFF,0xFF 赋值给 c,此时如果 c 是 char 类型的,那么其值就是 -1,即等于 EOF,于是程序就错误地认为输入已经结束。  而在 c 为 int 类型的情况下,将 0xFF 赋值给 c,c 的值是 255(因为符号位为 0,是正数),而非 -1,即除非读到输入末尾,c 的值都不可能是 -1。 

要将文本文件 test.txt 中的全部内容原样显示出来,程序可以如下编写:  

#include <iostream>
using namespace std;
int main()
{
    int c;
    freopen("in.txt", "r", stdin);  //将标准输入重定向为 test.txt
    while ((c = cin.get()) != EOF)
        cout.put(c);
    return 0;
}

cin.getline():C++读入一行字符串(整行数据)

getline() 是 istream 类的成员函数,它有如下两个重载版本: 

istream& getline (char* s, streamsize n );
istream& getline (char* s, streamsize n, char delim );

  • 第一个版本从输入流中读取 n-1 个字符到s,或遇到\n为止(哪个条件先满足就按哪个执行)。函数会自动在 s中读入数据的结尾添加\0。 
  • 第二个版本和第一个版本的区别在于,第一个版本是读到\n为止,第二个版本是读到 delim 字符为止。
  • \n或 delim 都不会被读入 s,但会被从输入流中取走。 
  • 这两个函数的返回值就是函数所作用的对象的引用。
  • 从输入流中读入一行,可以用第一个版本。用cin >> str这种写法是不行的,因为此种读法在碰到行中的空格或制表符时就会停止,因此就不能保证 str 中读入的是整行
  • 如果输入流中\n或 delim 之前的字符个数达到或超过n,就会导致读入出错,其结果是:虽然本次读入已经完成,但是之后的读入都会失败。(个人:也就是n是假定的流里面含有的字符最大总个数(包含delim在内),如果在实际读取流时,在遇到delim之前,已经读取了n个,这和我们的前提假设预设不符,所以,此后流会处于错误状态)

    在上面的输入情况下,程序是正常的。程序运行过程中还可能出现如下情况:

    第 7 行,读入时因字符串超长导致出错,于是第 11 行并没有从输入流读入 n,n 维持了原来的值 120。  第 12 行,调用 istream 的成员函数 clear() 清除 cin 内部的错误标记,此后 cin 又能正常读入了。因此,123456 在第 13 行被读入 n

可以用 getline() 函数的返回值来判断输入是否结束。例如,要将文件 test.txt 中的全部内容(假设文件中一行最长有 10 000个字符)原样显示,程序可以如下编写:

#include <iostream>
using namespace std;
const int MAX_LINE_LEN = 10000;  //假设文件中一行最长 10000 个字符
int main()
{
    char szBuf[MAX_LINE_LEN + 10];
    freopen("test.txt", "r", stdin);  //将标准输入重定向为 test.txt
    while (cin.getline(szBuf, MAX_LINE_LEN + 5))
        cout << szBuf << endl;
    return 0;
}

程序每次读入文件中的一行到 szBuf 并输出。szBuf 中不会读入回车符,因此输出 szBuf 后要再输出 endl 以换行

函数getline()读取一行字符串

(1)	
istream& getline (istream&  is, string& str, char delim);istream& getline (istream&& is, string& str, char delim);
(2)	
istream& getline (istream&  is, string& str);istream& getline (istream&& is, string& str);

// extract to string
#include <iostream>
#include <string>

int main ()
{
    std::string name;

    std::cout << "Please, enter your full name: ";
    std::getline (std::cin,name);
    std::cout << "Hello, " << name << "!\n";

    return 0;
} 

// extract to string
#include <iostream>
#include <string>

int main ()
{
    std::string name("dugu qiubai");
    std::cout<<"the old name is:"<<name<<std::endl;

    std::cout << "Please, enter your full name: ";
    std::getline (std::cin,name);
    std::cout << "the new name is:" << name << "!\n";

    return 0;
}

  

cin.ignore():C++跳过(忽略)指定字符

ignore() 是 istream 类的成员函数,它的原型是: 

istream& ignore (streamsize n = 1, int delim = EOF);

此函数的作用是跳过输入流中的 n 个字符,或跳过 delim 及其之前的所有字符,哪个条件先满足就按哪个执行

该函数常用于跳过输入中的无用部分,以便提取有用的部分。例如,输入的电话号码形式是Tel:63652823,Tel:就是无用的内容。 

#include <iostream>
using namespace std;
int main()
{
    int n;
    cin.ignore(5, 'A');
    cin >> n;
    cout << n<<endl;
    int i=100;
    cin>>i;
    cout<<"i value is:"<<i<<endl;
    return 0;
}

cin.peek():C++查看输入流中的下一个字符

peek() 是 istream 类的成员函数,它的原型是: 

int peek();

  • 此函数返回输入流中的下一个字符,但是并不将该字符从输入流中取走——相当于只是看了一眼下一个字符,因此叫 peek。 
  • cin.peek() 不会跳过输入流中的空格、回车符
  • 在输入流已经结束的情况下,cin.peek() 返回 EOF

(个人:代码中遇到cin.peek();会到输入缓冲区中查看下一个字符,如果输入缓冲区为空,则会要求我们输入)  

#include <iostream>
using namespace std;
int main()
{
   char c = cin.peek();
   cout<<"the cin.peek char is:"<<c<<endl;
   string s;
   cin>>s;
   cout<<"the string is:"<<s<<endl;
    return 0;
}

在输入数据的格式不同,需要预先判断格式再决定如何操作时,peek() 就能起到作用。例如:编写一个日期格式转换程序,输入若干个日期,每行一个,要求全部转换为“mm-dd-yyyy”格式输出。输入的日期格式可以是“2011.12.24”(中式格式),也可以是“Dec 24 2011”(西式格式)。要求该程序对于以下输入数据: 

Dec 3 1990
2011.2.3
458.12.1
Nov 4 1998
Feb 12 2011

输出结果应为:

12-03-1990
02-03-2011
12-01-0458
11-04-1998
02-12-2011

输入数据中的 Ctrl+Z 略去不写,因为输入数据也可能来自于文件。  

#include <iostream>
#include <iomanip>
#include <string>
using namespace std;
string Months[12] = { "Jan","Feb","Mar","Apr","May","Jun","Jul","Aug", "Sep","Oct","Nov","Dec" };
int main()
{
    int c;
    while((c = cin.peek()) != EOF) { //取输入流中的第一个字符进行查看
        int year,month,day;
        if(c >= 'A' && c <= 'Z') { //美国日期格式
            string sMonth;
            cin >> sMonth >> day >> year;
            for(int i = 0;i < 12; ++i)  //查找月份
                if(sMonth == Months[i]) {
                    month = i + 1;
                    break;
                }
        }
        else { //中国日期格式
            cin >> year ;
            cin.ignore() >> month ; //用ignore跳过 "2011.12.3"中的'.'
            cin.ignore() >> day;
        }
        cin.ignore();   //跳过行末 '\n'
        cout<< setfill('0') << setw(2) << month ;//设置填充字符'0',输出宽度2
        cout << "-" << setw(2) << day << "-" << setw(4) << year << endl;
    }
    return 0;
}

 istream 还有一个成员函数 istream & putback(char c),

例子:

demonstrates the difference between modifying and non-modifying putback()。

#include <sstream>
#include <iostream>

int main()
{
    std::stringstream s1("Hello, world"); // IO stream
    s1.get();
    if (s1.putback('Y')) // modifies the buffer
        std::cout << s1.rdbuf() << '\n';
    else
        std::cout << "putback failed\n";

    std::cout << "--\n";

    std::istringstream s2("Hello, world"); // input-only stream
    s2.get();
    if (s2.putback('Y')) // cannot modify input-only buffer
        std::cout << s2.rdbuf() << '\n';
    else
        std::cout << "putback failed\n";
    s2.clear();

    std::cout << "--\n";

    if (s2.putback('H')) // non-modifying putback
        std::cout << s2.rdbuf() << '\n';
    else
        std::cout << "putback failed\n";
}

// istream::putback example
#include <iostream>     // std::cin, std::cout
#include <string>       // std::string

int main () {
    std::cout << "Please, enter a number or a word: ";
    char c = std::cin.get();
    std::cout<<"the c is:"<<c<<std::endl;
    if ( (c >= '0') && (c <= '9') )
    {
        int n;
        std::cin.putback (c);
        std::cin >> n;
        std::cout << "You entered a number: " << n << '\n';
    }
    else
    {
        std::string str;
        std::cin.putback (c);
        getline (std::cin,str);
        std::cout << "You entered a word: " << str << '\n';
    }
    return 0;
}

对于上面的例子,也可以在用 get() 函数读取一个字符并判断是中式格式还是西式格式时,将刚刚读取的字符再用 putback() 成员函数放回流中,然后再根据判断结果进行不同方式的读入。

C++ cin判断输入结束(读取结束)

cin 可以用来从键盘输入数据;将标准输入重定向为文件后,cin 也可以用来从文件中读入数据。在输入数据的多少不确定,且没有结束标志的情况下,该如何判断输入数据已经读完了呢? 

  • 从文件中读取数据很好办,到达文件末尾就读取结束了。
  • 从控制台读取数据怎么办呢?总不能把控制台关闭吧?这样程序也运行结束了!其实,在控制台中输入特殊的控制字符就表示输入结束了
    • 在 Windows 系统中,通过键盘输入时,按 Ctrl+Z 组合键后再按回车键,就代表输入结束
    • 在 UNIX/Linux/Mac OS 系统中,Ctrl+D 代表输入结束。 

不管是文件末尾,还是 Ctrl+Z 或者 Ctrl+D,它们都是结束标志;cin 在正常读取时返回 true,遇到结束标志时返回 false,我们可以根据 cin 的返回值来判断是否读取结束。  

cin 判断控制台(键盘)读取结束

#include <iostream>
using namespace std;
int main()
{
    int n;
    int maxN = 0;
    while (cin >> n){  //输入没有结束,cin 就返回 true,条件就为真
        if (maxN < n)
            maxN = n;
    }
    cout << maxN <<endl;
    return 0;
}

istream 类将>>重载为成员函数,而且这些成员函数的返回值是 cin 的引用。准确地说,cin>>n的返回值的确是 istream & 类型的,而 while 语句中的条件表达式的返回值应该是 bool 类型、整数类型或其他和整数类型兼容的类型,istream & 显然和整数类型不兼容,为什么while(cin>>n)还能成立呢?  这是因为,istream 类对强制类型转换运算符 bool 进行了重载,这使得 cin 对象可以被自动转换成 bool 类型。所谓自动转换的过程,就是调用 cin 的 operator bool() 这个成员函数,而该成员函数可以返回某个标志值,该标志值在 cin 没有读到输入结尾时为 true,读到输入结尾后变为 false。对该标志值的设置,在 operator <<() 成员函数中进行。 

如果 cin 在读取过程中发生了错误,cin>>n这样的表达式也会返回 false。例如下面的程序: 

#include <iostream>
using namespace std;
int main()
{
    int n;
    while (cin >> n)
        cout << n << endl;
    return 0;
}

程序本该输入整数,如果输入了一个字母,则程序就会结束。因为,应该读入整数时却读入了字母也算读入出错。 

C++处理输入输出错误

发生输入输出错误的可能情况是无限的!但 C++ 将所有可能的情况归结为四类,称为流状态(stream state)。每种流状态都用一个 iostate 类型的标志位来表示。  

流状态对应的标志位
标志位意义
badbit 发生了(或许是物理上的)致命性错误,流将不能继续使用。
eofbit 输入结束(文件流的物理结束或用户结束了控制台流输入,例如用户按下了 Ctrl+Z 或 Ctrl+D 组合键。
failbit I/O 操作失败,主要原因是非法数据(例如,试图读取数字时遇到字母)。流可以继续使用,但会设置 failbit 标志
goodbit 一切止常,没有错误发生,也没有输入结束。

ios_base 类定义了以上四个标志位以及 iostate 类型,但是 ios 类又派生自 ios_base 类,所以可以使用 ios::failbit 代替 ios_base::failbit 以节省输入。  

一旦流发生错误,对应的标志位就会被设置,我们可以通过下表列出的函数检测流状态。

C++流状态检测函数及其说明
检测函数对应的标志位说明
good() goodbit 操作成功,没有发生任何错误。
eof() eofbit 到达输入末尾或文件尾。
fail() failbit 发生某些意外情况(例如,我们要读入一个数字,却读入了字符 'x')。
bad() badbit 发生严重的意外(如磁盘读故障)。

不幸的是,fail() 和 bad() 之间的区别并未被准确定义,程序员对此的观点各种各样。但是,基本的思想很简单:

  • 如果输入操作遇到一个简单的格式错误,则使流进入 fail() 状态,也就是假定我们(输入操作的用户)可以从错误中恢复
  • 如果错误真的非常严重,例如发生了磁盘故障,输入操作会使得流进入 bad() 状态。也就是假定面对这种情况你所能做的很有限,只能退出输入。 
#include <iostream>

using namespace std;

int main() {
    int i = 0;
    cin >> i;
    if (!cin) {  //只有输入操作失败,才会跳转到这里
        if (cin.bad()) {  //流发生严重故障,只能退出函数
            cerr << "cin is bad!";
            return -1;
        }
        if (cin.eof()) {  //检测是否读取结束
            cout<<"input has finished"<<endl;
            return 0;
        }
        if (cin.fail()) {  //流遇到了一些意外情况
            cin.clear(); //清除/恢复流状态
            cin.ignore(1024,'\n');//忽略缓冲区中的内容
            cout<<"please input a integer:"<<endl;
            cin>>i;
            cout<<"the input integer is:"<<i<<endl;
            return 0;

        }
    }
    return 0;
}

!cin 可以理解为“cin 不成功”或者“cin 发生了某些错误”或者“ cin 的状态不是 good()”, 这与“操作成功”正好相反。

请注意我们在处理 fail() 时所使用的 cin.clear()。当流发生错误时,我们可以进行错误恢复。为了恢复错误,我们显式地将流从 fail() 状态转移到其他状态,从而可以继续从中读取字符。clear() 就起到这样的作用——执行 cin.clear() 后,cin 的状态就变为 good()。  

下面是一个如何使用流状态的例子。假定我们要读取一个整数序列并存入 vector 中,字符terminator或“文件尾”表示序列结束。Windows 平台按下 Ctrl+Z 组合键,再按下回车键表示到达文件末尾;类Unix系统按下 Ctrl+D 组合键表示到达文件末尾。 

#include <iostream>
#include <vector>
using namespace std;

//从 ist 中读入整数到 v 中,直到遇到 eof() 或终结符
void fill_vector(istream &ist, vector<int> &v, char terminator) {
    for (int i; ist >> i;)v.push_back(i);

    //正常情况
    if (ist.eof()){
        cout<<"meet eof,success!"<<endl;
        return;  //发现到了文件尾,正确,返回

    }


    //发生严重错误,只能退出函数
    if (ist.bad()) {
        cerr<<"cin is bad!";
        return;
    }

    //发生意外情况
    if (ist.fail()) {  //最好清除混乱,然后汇报问题
        ist.clear();  //清除流状态

        //检测下一个字符是否是终结符
        char c;
        ist >> c;  //读入一个符号,希望是终结符
        if (c != terminator) { // 非终结符
            ist.unget(); //放回该符号
            ist.clear(ios_base::failbit);  //将流状态设置为 fail()
            cerr<<"cin is fail!";
            return;
        }
        else{
            cout<<"meet terminator:"<<terminator<<",success!"<<endl;
            return;
        }
    }
}

int main() {

   vector<int> ivec;
   fill_vector(cin,ivec,'*');
   cout<<"the input int is:"<<endl;
   for(auto val:ivec)
       cout<<val<<" ";
   return 0;
}

  • 我们通过调用 ist.clear(ios_base::failbit) 来将流状态设置为 failbit。当 clear() 带参数时,参数中所指出的 iostream 状态位会被置位(进入相应状态),而未指出的状态位会被复位
  • 可以用 unget() 将字符放回 ist,以便 fill_vector() 的调用者可能使用该字符。unget() 函数是 putback() 的简化版本,它依赖于流对象记住最后一个字符是什么

与 istream—样,ostream 也有四个状态:good()、fail()、eof() 和 bad()。输出错误要比输入错误少得多,因此通常不对 ostream 进行状态检测。如果程序运行环境中输出设备不可用、队列满或者发生故障的概率很高,我们就可以像处理输入操作那样,在每次输出操作之后都检测其状态。 

  

posted on 2022-09-24 08:48  朴素贝叶斯  阅读(252)  评论(0编辑  收藏  举报

导航