C++重温笔记(十): C++输入输出流

1. 写在前面

c++在线编译工具,可快速进行实验: https://www.bejson.com/runcode/cpp920/

这段时间打算重新把c++捡起来, 实习给我的一个体会就是算法工程师是去解决实际问题的,所以呢,不能被算法或者工程局限住,应时刻提高解决问题的能力,在这个过程中,我发现cpp很重要, 正好这段时间也在接触些c++开发相关的任务,所有想借这个机会把c++重新学习一遍。 在推荐领域, 目前我接触到的算法模型方面主要是基于Python, 而线上的服务全是c++(算法侧, 业务那边基本上用go),我们所谓的模型,也一般是训练好部署上线然后提供接口而已。所以现在也终于知道,为啥只单纯熟悉Python不太行了, cpp,才是yyds。

和python一样, 这个系列是重温,依然不会整理太基础性的东西,更像是查缺补漏, 不过,c++对我来说, 已经5年没有用过了, 这个缺很大, 也差不多相当重学了, 所以接下来的时间, 重温一遍啦 😉

资料参考主要是C语言中文网光城哥写的C++教程,然后再加自己的理解和编程实验作为辅助,加深印象,当然有些地方我也会通过其他资料进行扩充。 关于更多的细节,还是建议看这两个教程。

今天这篇文章整理C++关于输入和输出的操作,也就是我们所熟知的"流"操作, 我发现学习哪个语言,都需要学习它的I/O操作, 毕竟这是我们读数据和写数据的前提呀, C++也不例外,通常,我们在C++中使用cin输入流实现数据输入, cout输出流实现数据输出(输入和输出流本质上是已经定义好的类对象), 但是, 这只是流里面的冰山一小小角, 其实C++输入流和输出流不仅实现基本的输入输出操作, 通过类内部成员函数, 还可以满足特殊场景的输入输出需求, 这又是一个很长很长的故事…

主要内容:

  • C++输入流和输出流
  • C++输出单个字符(put)和字符串(write)
  • C++的tellp和seekp方法详解
  • C++ cout的格式化输出
  • C++输入输出重定向
  • C++管理输出缓冲区
  • C++读取单个字符(get)和读入字符串(getline)
  • C++跳过指定字符(ignore)及查看输入流中的下一个字符(peek)
  • C++ cin如何判断输入结束?
  • C++处理输入输出错误
  • 小总

Ok, let’s go!

2. C++输入流和输出流

C语言有一套完成数据读写的解决方案:

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

这套I/O方案在C++也同样适用,不过C++还独立开发了一套全新I/O解决方案, 这套解决方案是我们所说的"流类"组成的类库。 整个流类以及它们的关系如下:
在这里插入图片描述
这些流类的功能也可以见名知意:

  • istream: 接收从键盘输入的数据
  • ostream: 数据输出到屏幕
  • ifstream: 读文件中的数据
  • ofstream: 向文件写数据
  • iostream: istream和ostream类功能合体,既可以从键盘输入,也可以输出到屏幕
  • fstream: ifstream和ofstream类功能合体,既能读取文件数据,又能向文件写数据

之前学习的cin是istream对象, cout是ostream对象, 它们都声明在<iostream>中。除了cout, 头文件中还声明了2个对象,叫做cerr和clog, 它们用法和cout一样,只不过cerr常用来输出警告和错误信息, clog常用来输出程序执行中的日志信息。区别如下:

  • cout除了可以将数据输出到屏幕,还可以通过重定向,实现数据输出到指定文件; 而cerr和clog不支持重定向,只能将数据输出到屏幕
  • cout和clog都有缓冲区, 它们输出数据时, 会先将数据放到缓冲区,等缓冲区满或手动换行时(换行符\n),才会将数据全部显示到屏幕;cerr没有缓冲区,会直接将数据输出到屏幕。

其他的,这哥仨无不同。

std::cout << "cout:" << "wuzhongqiang" << std::endl;
std::cerr << "cerr:" << "wuzhongqiang" << std::endl;
std::clog << "clog:" << "wuzhongqiang" << std::endl

注意, 这里的cin, cout, cerr, clog等不是C++关键字,而是流对象。 另外,这里既然谈到了缓冲区, 我有些好奇,所以就先对缓冲区进行了下学习。

缓冲区,又称缓存,是内存空间的一部分。 即在内存空间中预留一定存储空间,用来缓冲输入和输出数据的。根据其对应是输入还是输出设备, 分为输入和输出缓冲区。
在这里插入图片描述
缓冲区有三种类型:

  • 全缓冲: 当填满标准I/O缓存后才进行实际I/O操作,典型代表是对磁盘文件读写
  • 行缓冲: 当在输入和输出遇到换行符时, 执行真正的I/O操作。典型代表是键盘输入数据, 我们输入的数据先存放在缓冲区,等按下回车换行时,才进行实际I/O操作。
    • 既然说到cin上,就顺便解释下标准输入缓冲区, 这个放在上大学的时候,是没理解的了。
    • 当我们从键盘输入字符串的时候, 需要敲一下回车键才能将这个字符串送入缓冲区, 而这个回车键,会转换成一个'\n', 也被存储在cin缓冲区,并且这个东西也被当成一个字符。
    • cin读取数据时,是从缓冲区读取, 缓冲区为空, cin的成员函数会阻塞等待数据到来, 而一旦缓冲区有数据,就触发成员函数去读取数据
  • 不带缓冲: 就像上面的cerr这种的,有出错信息尽快抛出来


缓冲区会刷新的四种情况: 缓冲区满,执行flush, 执行endl, 关闭文件

当然, 上面提到cin, cout, cerr和clog都是类对象,其实istream和ostream还提供了很多实用的函数, 供这几个类对象调用。 下面盘点下, 这些我们在日常中也常用,但其实有时候并不知道他们的区别。

cin对象常用的一些成员方法以及功能:

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

cout哥仨常用的一些成员方法及功能

成员方法名功能
put()输出单个字符
write()输出指定字符串
tellp()用于获取当前输入流指针的位置
seekp()设置输出流指针位置
flush()刷新输出流缓冲区
operator<<重载<<运算符,使其用于输出其后指定类型数据

看个例子:

int main()
{
    char url[10] = {0};
    
    // 读取一行字符串
    cin.getline(url, 10);
    // 输出上面读取字符个数
    cout << cin.gcount() << endl;
    // 输出出来
    cout.write(url, 10);
    return 0;
}

// 此时输入hello world
// 给到url的是hello wor   cin.gcount=9, 把9个字符给到url

3. C++输出单个字符(put)和字符串(write)

3.1 put()函数

put()成员函数用于向输出流缓冲区添加单个字符, 函数原型如下:

ostream&put(char c);

可以看到, 该函数返回一个ostream类的引用对象, 可以理解为cout的引用,所以这玩意可以拼接输出:

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

应用实例:

cout.put('a');   // a
cout.put(65+32);  // a
cout.put(97);  // a

cout.put(71).put(79).put(79). put(68).put('\n'); // GOOD

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

OK, 这个put()函数,看似很简单, 但我有个问题,就是为啥要有这玩意呢? 你要说输出单个字符, 我cout<<难道实现不了吗? 这个问题, 勾起了我的好奇心, 首先, cout.put()是可以将字符的ASCII码转成字符直接输出的, 下面可以看这两个的不同:

cout << 71 << endl;  // 71
cout.put(71) << endl;  // G

int b = 'a';
cout << b << endl;  // 97
cout.put(b) << endl; // a

当然,为了这个问题, 我还特意查了下《C++ Primer Plus》,发现put()函数其实和历史有关,在C++2.0之前版本中, C++语言用int值表示字符串常量,比如下面这句话,是没法输出字符的:

cout << 'W' << endl;   // 87

char ch = 'W';  // 在早期版本中, 会从常量'W'中复制左边8位给到ch

'W’的编码87会存储在一个16位或者32位的单元中, 而char变量一般占8位。 所以对于cout, ch和’W’是天壤之别的,虽然存储的值可能相同。 所以那时候,如果想打印出字符来, 就需要

cout.put('W') << endl;  // W

不过C++2.0之后, C++字符常量存储已经改成了char类型,不是int类型了,所以cout可以正确处理字符常量。 put()函数我感觉用的并不是很多了现在。

3.2 write()函数

write()方法用于向输出缓冲区添加指定的字符串, 格式:

ostream&write(const char * s,streamsize n);  // s用于指定某个长度至少为n的字符数组或字符串, n表示输出前n个字符

这个函数同样返回了一个ostream类的引用对象,可以连着输出:

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

下面演示一下这个方法, 这个函数感觉还是很强大的, 在C++没有切片的情况下,不一定能直接想到cout<<的替代方法。比如下面这个:

#include <iostream>
#include <iostream>
#include <cstring>
using namespace std;
int main(){
    
    
    const char * w1 = "hello";
    const char * w2 = "world";
    int len = strlen(w1);
    
    for (int i = 0; i < len+5; i++){
        cout.write(w1, i);
        cout << endl;
    }
    
    return 0;
}

// 输出结果
h
he
hel
hell
hello
hello
hellow
hellowo
hellowor

如果不用python的这种切片, 这种对一个字符串,先输出前1个字符,再输出前2个字符,依次类推输出, 用cout<<一时还想出怎么搞,但write()函数就可以轻松搞定。

但通过这个例子, 至少有两点能够看出来, 第一个就是write()方法不会遇到空字符自动停止打印字符,而只是打印指定数目的字符,即使超出字符串的边界。看循环边界的len+5, 这显然已经超出了w1的范围, 但还是会打印, 打印到w2里面去了。 当然,这是我故意这么写的, 之所以用const限制,就是因为这样能使得w1和w2在内存中能连着放, 可以看的清晰些, 这是第二点。

当然, write()方法, 也可以用于数值数据:

int main(){
    long val = 2397923872389;
    cout.write((char *) &val, sizeof(long));   // 厦O.
    return 0;
}

这里会发现输出的是乱码, 这是因为这个强转操作, 不会将数字转成相应的字符, 而是传输内存中的位表示,4字节的long值,将作为4个独立字节传输。 输出设备把每个字节的ASCII码进行解释,所以,可能出来乱码。 但write()方法确实给数值数据存储在文件中提供了一种简洁, 准确的方式, 后面会整理, 但这个方法确实是很重要的一个方法。

4. C++的tellp和seekp方法详解

cout输出普通数据(也包括cout.put()cout.write()), 数据都会先放到流缓冲区, 待缓冲区刷新,数据才会输出到指定位置。

ostream类中的tellp()seekp()成员方法, 是帮助我们修改暂存在输出流缓冲区里面的数据的。

4.1 tellp()成员方法

tellp()用于获取当前输出缓冲区中最后一个字符所在的位置, 语法如下:

streampos tellp();

tellp()不需要传任何参数, 会返回一个streampos类型值。

streampos是fpos类型的别名,通过自动类型转换,可直接赋值给一个整形变量。即可以用一个整形变量接收该函数返回值。

注意,当输出流缓冲区中没有任何数据时,该函数返回的整形值为 0;当指定的输出流缓冲区不支持此操作,或者操作失败时,该函数返回的整形值为 -1

下面我做了一个实验:

#include <iostream>
#include <fstream>
#include <cstring>
int main()
{
    std::ofstream outfile;
    
    outfile.open("test.txt");
    const char *str = "hello world";
    for (int i = 0; i < strlen(str); i++){
        outfile.put(str[i]);
        
        // 获取当前输出流
        long pos = outfile.tellp();
        std::cout << pos << " ";  // 1 2 3 4 5 6 7 8 9 10 11 
        
    }
    return 0;
}

程序每次向输出缓冲区放入字符, pos都表示当前字符的位置。

这里另外想补充的一点,就是,一开始上面这个程序我尝试, 输出屏幕的时候进行定位,发现会报错。 结果搜了下, 感觉这个函数是用于在文件操作中定位内置指针位置的,一般在写文件的时候用。 另外,还有个和他类似的函数叫tellg(), 这个是用于读文件的时候获取内置指针的位置。

总而言之:当我们读取一个文件,并要知道内置指针的当前位置时,应该使用tellg();当我们写入一个文件,并要知道内置指针的当前位置时,应该使用tellp().

4.2 seekp()成员方法

seekp()方法用于指定下一个进入输出缓冲区的字符所在的位置。

比如上面的hello world输出的时候,我们知道最后一个d的位置是11, 此时,如果继续向缓冲区存入数据, 则下一个字符所在位置应该是12, 但借助这个方法,我们可以手动指定下一个字符存放的位置。

seekp() 方法有如下 2 种语法格式:

//指定下一个字符存储的位置
ostream& seekp (streampos pos);
//通过偏移量间接指定下一个字符的存储位置   
ostream& seekp (streamoff off, ios_base::seekdir way);

## 
off: 相对于way位置的偏移量, 可以是正数可以是负数
way: 指定偏移位置,即从哪里计算偏移量, 三种选择
	ios::beg: 文件开头开始计算
	ios::end: 文件末尾开始计算
	ios::cur: 当前位置开始计算

seekp()返回的是引用形式的ostream对象,所以这东西还可以查看缓冲区里面某一位置上的字符

cout.seekp(pos);

看下面的例子:

int main() {
    //定义一个文件输出流对象
    std::ofstream outfile;
    //打开 test.txt,等待接收数据
    outfile.open("test.txt");
    const char *str = "hello world";
    //将 str 字符串中的字符逐个输出到 test.txt 文件中,每个字符都会暂时存在输出流缓冲区中
    for (int i = 0; i < strlen(str); i++){
        outfile.put(str[i]);
    }
    std::cout << outfile.tellp() << std::endl;  // 11
    
    outfile.seekp(6);   
    // 等价于 outfile.seekp(6, ios::beg)  outfile.seekp(-6, ios::cur) outfile.seekp(-6, ios::end)
    
    std::cout << "新插入的位置: " << outfile.tellp() << std::endl;  // 新插入的位置: 6
    
    const char *newstr = "C++";
    outfile.write(newstr, 3);
    std::cout << outfile.tellp() << std::endl;  // 9
    
    // 关闭文件之前, 刷新outfile输出缓冲区, 使所有字符由缓冲区流入test.txt文件
    outfile.flush();
    
    // 读入看看
    std::ifstream File("test.txt");    
    char s[10];    
    File.read(s,9);    
    std::cout << File.tellg() << std::endl;  // 9
    std::cout.write(s, 10);  // hello C++
    return 0;
}

这里首先发现的第一个事情, tellp()是输出缓冲区末尾的位置, 这个末尾的意思不是最后一个字符位置其实,一开始缓冲区没有数据的时候, 此时tellp()指向0位置, 当插入一个字符之后, tellp()就后移了一下到了1位置,依次类推。当把hello world这11个字符全部插入, tellp()指向了第11个位置, 但注意hello world存放到了0~10. 所以准确的说,这个tellp()应该是最后一个字符的后面一个待插入字符位置。 这样,上面的结果才能说得通。

第二个点,就是tellg()函数, 这里也演示了一下用法, 这个是在文件输入中获取当前的指针位置,由于hello C++也是存储到了0~8,所以tellg()这里和tellp()一样,其实也是最后一个字符后面一个位置。

5. C++ cout格式化输出

某些实际场景中, 可能需要一定的格式输出数据, 比如保留几位小数等, C语言里面的printf()在输出数据时, 可以通过设定一些合理格式控制符, 来达到指定格式输出数据的目的。 比如%.2f, %#X表示十六进制等, 具体可以看这篇文章

C++的cout在输出数据时, 实现格式化输出的方式更加多样, 一方面cout作为ostream类的对象, 该类中提供一些成员方法,可实现对输出数据格式化, 另一方面, C++专门提供了一个<iomanip>头文件, 这里面包含大量格式控制符。但这个没有涉及到原理性的东西,并且没有必要死记硬背, 会查即可,所以这个在这里也不整理, 可以直接看文档

6. C++输入输出重定向

什么是重定向? 默认情况下, cin只能接收从键盘输入的数据, cout也只能将数据输出到屏幕上。 但通过重定向, cin可以将指定文件作为输入源, cout可以将原本要输出到屏幕上的数据写到指定文件。

C++实现重定向常用方式有3种:

6.1 freopen()函数实现重定向

这个函数的定义在<stdio.h>头文件,C语言标准库中的函数,专门用于重定向输入流(scanf(),gets())和输出流(printf,puts)。 但这个函数也可以对C++中的cincout重定向。

#include<iostream>  
using namespace std;
int a[100];
int main(){
	// 标准输入流重定向到abc.in文件中
	freopen("abc.in","r",stdin);
	// 标准输出流重定向到abc.out文件中
	freopen("abc.out","w",stdout);
	int n;
	cin>>n;
	for(int i=1; i<=n; i++) 
		cin>>a[i];        // 这个在abc.in文件中读取
	for(int i=n; i>=1; i--)
		cout<<a[i]<<" ";   // 输出到abc.out文件中
	
	// 关闭重定向
	fclose(stdin);
	fclose(stdout);
	return 0;
}

6.2 rdbuf()函数实现重定向

rdbuf()函数定义在<ios>头文件, 专门用于实现C++输入输出流重定向。

语法格式有两种:

streambuf *rdbuf() const;   // 返回一个指向当前缓冲区的类
streambuf *rdbuf(streambuf *sb);  // 将sb指向的缓冲区设置为当前流的新缓冲区,并返回一个指向旧缓冲区的对象

第二个函数好好理解下, 是sb指向的缓冲区设置为当前流的新缓冲区,但返回的是一个指向原先缓冲区的对象。streambuf是C++标准库中用于表示缓冲区的类,该类的指针对象用于代指某个具体的流缓冲区。

看个例子:

#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());
    cin >> a;   // 从input.txt文件读入
    
    // 用rdbuf()重新定向, 返回旧输出流缓冲区指针
    oldcout == cout.rdbuf(fout.rdbuf());
    cout << a << endl;   // 写入out.txt
    
    // 还原标准输入输出流, 恢复键盘输入和输出
    cin.rdbuf(oldcin);
    cout.rdbuf(oldcout);
    
    // 打开的文件要手动关闭
    fin.close();
    fout.close();
    return 0;
}

6.3 控制台实现重定向

这个的意思是在控制到执行.exe的时候, 通过后面添加参数的方式实现重定向。

比如写个程序:

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

此时编译链接, 会生成一个demo.exe的可执行文件。 然后再控制台执行这个可执行文件的时候,后面指定上参数:

C:\Users\mengma>D:\demo.exe <in.txt >out.txt

执行后会发现,控制台没有任何输出。这是因为,我们使用了"<in.txt"对程序中的 cin 输入流做了重定向,同时还用 ">out.txt"对程序中的 cout 输出流做了重定向。

7. C++管理输出缓冲区

每个输出流都管理一个缓冲区,用来保存程序读写的数据。比如下面代码:

cout << "hello world";

字符串可能立即打印,也有可能os先保存到缓冲区,然后再打印。

有了缓冲机制,os可以将程序多个输出操作组合成单一的系统级写操作。 这样可以带来性能提升,因为写操作可能很耗时。

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

  1. 程序正常结束,作为main()函数的return操作的一部分, 缓冲刷新被执行。
  2. 缓冲区满时, 需要刷新缓冲区
  3. 使用操纵符如endl来显式刷新缓冲区
  4. 在每个输出操作之后,可以用操纵符unitbuf设置流的内部状态,来清空缓冲区。 默认情况下,对cerr是设置unitbuf的,因此写到cerr得的内容都是立即刷新
  5. 一个输出流可能被关联到另一个流。这种情况下,当读写被关联的流时, 关联到的流的缓冲区会被刷新。 默认情况下,cin和cerr都关联到cout。 因此读cin或者写cerr都会导致cout的缓冲区被刷新。

后三个详细理解下。

7.1 操纵符显示刷新

之前使用过操作符endl, 它完成换行并刷新缓冲区的工作。 IO库中还有两个类似操作符flush和ends:

  1. flush刷新缓冲区,但不输出任何额外的字符。 值得一提,cout 所属 ostream 类中还提供有 flush() 成员方法,它和 flush 操纵符的功能完全一样,仅在使用方法上( cout.flush() )有区别。
  2. ends向缓冲区插入一个空字符, 然后刷新缓冲区

比如:

cout << "hi!" << endl;  //输出hi和一个换行,然后刷新缓冲区
cout << "hi!" << flush;  //输出hi,然后刷新缓冲区,不附加任何额外字符 等价于cout << "hi!"; cout.flush();
cout << "hi!" << ends;  //输出hi和一个空字符,然后刷新缓冲区

7.2 unitbuf操作符

如果想在每次输出操作后都刷新缓冲区,我们可以使用 unitbuf 操作符,它告诉流在接下来的每次写操作之后都进行一次 flush 操作。而 nounitbuf 操作符则重置流, 使其恢复使用正常的系统管理的缓冲区刷新机制:

cout << unitbuf;  //所有输出操作后都会立即刷新缓冲区
//任何输出都立即刷新,无缓冲
cout << nounitbuf;  //回到正常的缓冲方式

如果程序异常终止, 输出缓冲区是不会被刷新的。

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

7.3 关联输入和输出流

当一个输入流被关联到一个输出流时, 任何试图从输入流读取数据的操作都会先刷新关联的输出流。 标准库的cout和cin关联在一起,所以执行:

cin >> name;

会导致cout的缓冲区被刷新。

交互式系统通常应该关联输入流和输出流。这意味着所有输出,包括用户提示信息,都会在读操作之前被打印出来。

tie()函数可以用来绑定输出流:

ostream* tie ( ) const;  //返回指向绑定的输出流的指针。
ostream* tie ( ostream* os );  //将 os 指向的输出流绑定的该对象上,并返回上一个绑定的输出流指针。

我们可以将一个istream对象关联到另一个ostream, 也可以将一个ostream关联到另一个ostream。

cin.tie(&cout);  //仅仅是用来展示,标准库已经将 cin 和 cout 关联在一起

//old_tie 指向当前关联到 cin 的流(如果有的话)
ostream *old_tie = cin.tie(nullptr);  // cin 不再与其他流关联

//将 cin 与 cerr 关联,这不是一个好主意,因为 cin 应该关联到 cout
cin.tie(&cerr);  //读取 cin 会刷新 cerr 而不是 cout

cin.tie(old_tie);  //重建 cin 和 cout 间的正常关联

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

8. C++读取单个字符(get)和读入字符串(getline)

上面整理的输出, 下面我们看输入部分。

int get();

get()函数是istream类的成员函数, 用于从输入流中读入一个字符,返回该值字符的ASCII码。如果碰到输入的末尾, 就返回EOF。

EOF是End of File的缩写。 istream类从输入流读取数据的成员函数, 在把输入数据都读取完后,再进行读取就返回EOF。 EOF是iostream类中定义的一个整型常量,值为-1

这个让我想起了这个语句:

int c;
while (c = cin.get() != EOF){
	cout.put(c);
}

get()函数不会跳过空格,制表符,回车等特殊字符,所有字符都能被输入。

如果要读取文件的字符, 可以使用上面重定向的知识:

int c;
freopen("test.txt", "r", stdin);
while ((c=cin.get()) != EOF){
	cout.put(c);
}

那么,如果想读入一行字符串怎么办呢?

这时候,就可以用getline()函数。

istream &getline(char *buf, int bufSize); // 从输入流读取bufSize-1个字符到缓冲区buf,或遇到\n位置。 函数会自动在buf读入数据的结尾添加\0
istream &getline(char *buf, int bufSize, char delim); // 这个是读到delim字符位置, 而不是读到\n了,并且\n和delim字符都不会读入到buf,但会被从输入流中取走

两个函数返回值就是函数所作用的对象的引用。如果输入流中\n或delim之前的字符个数达到bufSize, 就会导致读入出错,结果是: 虽本次读入已经完成,但之后的读入会失败。

从输入流读入一行,可以用上面第一个, cin>>str这个不行,因为这种读法遇到行中的空格或制表符就会停止,因此不能保证str读入的是整行。 像get, getline这种,也称为非格式化输入方法。因为它们只是读取字符输入,并不会跳过空白,也不会转换数据格式。

下面看一个例子:

char szBuf[20];
int n = 120;

// 如果输入流中一行字符超过5个,就会出错
if (!cin.getline(szBuf, 6))
	cout << "error" << endl;
cout << szBuf << endl;

// 测试下还能不能读入了
cin >> n;
cout << n << endl;

// clear能清楚cin内部的错误,使之恢复正常
cin.clear()
cin >> n;
cout << n << endl;

测试一:

在这里插入图片描述
这个没有任何问题, 因为一开始ab cd,这一行输入流中字符没超过5, getline不会出错,下面的都能正常读入。

测试二:
在这里插入图片描述
这个就出问题了, 第一行的输入ab cd123456k是不符合cin.getline(szBuf, 6)的,所以这个会直接保存,但是呢? 这个函数依然会把ab cd四个字符读入给到szBuf。但后面n这个就不能正常读入了,所以n这个直接是输出默认值120. 当执行cin.clear()之后, 消除错误,恢复正常,此时又能正常读入, 但此时从错误出开始, 读入了123456, 因为n定义的是整数,所以k不会被读进来。

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

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 以换行。

9.C++跳过指定字符ignore及查看输入流中的下一个字符peek

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

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

此函数的作用是跳过输入流中的 n 个字符,或跳过 delim 及其之前的所有字符,哪个条件先满足就按哪个执行。两个参数都有默认值,因此 cin.ignore() 就等效于 cin.ignore(1, EOF), 即跳过一个字符。

该函数常用于跳过输入中的无用部分,提取有用部分。

int n;
cin.ignore(5, 'A');
cin >> n;
cout << n;

// 输入abcde34     跳过5个字符,   n=34
// 输入abA67  先遇到了A, 跳过abA, n=67

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

int peek();

这个函数返回输入流中的下一个字符,但并不会将该字符重输入流中取走。 类似于栈的gettop()

cin.peek()不会跳过输入流中的空格,回车符。输入流已经结束的情况下, cin.peek()返回EOF。

在输入数据的格式不同,需要预先判断格式再决定如何读取输入时,peek() 就能起到作用。

比如编写一个日期转换函数, 输入是若干个日期, 每行一个,有中式格式"2011.12.24"也有西式格式"Dec 24 2011",而输出全部转成"yyyy-mm-dd"的格式。

这时候在读入之前,就需要先试探一下是大写字母开头,还是数字开头,先把西式和中式分开, 然后再cin了。 具体代码如下:

#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){
        cout << char(cin.peek()) << " ";
        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忽略.
            cin.ignore() >> day;
        }
        cin.ignore();  // 跳过末尾的'\n'
        cout << setw(4) << year << "-" << setfill('0') << setw(2) << month << "-" << setw(2) << day << endl;
    }
}

结果如下:
在这里插入图片描述

10. C++ cin如何判断输入结束?

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

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

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

cin>>n的返回值的确是 istream & 类型的,而 while 语句中的条件表达式的返回值应该是 bool 类型、整数类型或其他和整数类型兼容的类型,istream & 显然和整数类型不兼容,为什么while(cin>>n)还能成立呢?

这是因为,istream 类对强制类型转换运算符 bool 进行了重载,这使得 cin 对象可以被自动转换成 bool 类型。所谓自动转换的过程,就是调用 cin 的 operator bool() 这个成员函数,而该成员函数可以返回某个标志值,该标志值在 cin 没有读到输入结尾时为 true,读到输入结尾后变为 false。

如果cin在读取过程中发生了错误, cin>>n表达式也会返回false,比如一个int型的n,输入进去的是个字母。

11. C++处理输入输出错误

这一块目前用的不多, 详细的可以去中文网的文档中看,这里简单整理下C++中会把输入输出时发生的错误归为四类,称为流状态,并且用四个标志位来表示,而每个标志位都对应着检测函数。

检测函数对应的标志位说明
good()goodbit操作成功,没有发生任何错误
eof()eofbit到达输入末尾或文件末尾
fail()failbit发生某些意外错误,比如要读入一个数字,却读入了字符
bad()badbit发生严重错误,比如磁盘读故障

这时候,我们想让程序更加鲁棒的话,就应该考虑到这些问题,并及时采取相应的方案,下面是一个简单例子:

//从 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()) return;  //发现到了文件尾,正确,返回
    //发生严重错误,只能退出函数
    if (ist.bad()){
        error("cin is bad!");  //error是自定义函数,它抛出异常,并给出提示信息
    }
    //发生意外情况
    if (ist.fail()) {  //最好清除混乱,然后汇报问题
        ist.clear();  //清除流状态
        //检测下一个字符是否是终结符
        char c;
        ist>>c;  //读入一个符号,希望是终结符
        if(c != terminator) { // 非终结符
            ist.unget(); //放回该符号
            ist.clear(ios_base::failbit);  //将流状态设置为 fail()
        }
    }
}

12. 小总

这里依然是一张思维导图拎起来:

在这里插入图片描述

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值