面向对象和C++基础—IO篇

13. IO篇

  到了C++中,IO已经变得比C语言好用多了,因为标准库中对于IO流利用面向对象的特性做了很多很多的封装。

(1). 更好用的C++输入输出流

  在C++中,标准库使用类对输入与输出流做了封装,我们不再需要用到stdin和stdout两个文件指针来操作了,比如我们之前使用cout和cin以及重载后的左移右移运算符进行输入和输出。
  在文件读写中为我们会用到ifstream和ofstream,它们也是一样使用左移和右移运算符进行输入和输出的操作,对于字符串的stringstream也是一样,这相较于原来的stdin和stdout和对应的fprintf/fscanf等等函数其实是类似的,但是因为有了运算符重载和多态,他们变得更加方便了。

(2). IO流之间的关系

  Bjarne Stroustrup在C++程序设计语言中把IO流之间的关系表示如下图:
p57

  所有的流派生自ios_base,再由其派生出basic_ios<>,之后再由basic_ios<>和basic_iostream<> 共同派生出basic_istream<>basic_ostream<>,之后的stringstream、fstream之类的都由对应的输入流和输出流派生而来。
  有点复杂,不过你会发现,我们的输入输出流很多具备多态的能力,之后我们会详细介绍一下如何利用IO流的多态完成一些工作。

(3). 标准输入输出与iostream

  首先是标准输入输出,现在大家写C++代码起手式应该都是#include <iostream>了,那么我们用到的coutcin其实就是iostream当中定义的std::ostream和std::istream的对象,那么接下来让我们来看看如何更好地使用std::ostream和std::istream吧。

#1.std::ostream

(I).更多形态的输出

  首先我们来看看这段代码:

std::cout << true << std::endl;

  你觉得它会输出啥?“true”?我们来看看:
p58

  好吧,结果是1,false的结果是0,那怎么办,要是我想要输出true和false咋办呢?我们只要在输出布尔值前再输出一次std::boolalpha即可:

std::cout << std::boolalpha << true << std::endl;

p59

  这样就好了,那这个std::boolalpha能够持续多久呢?

std::cout << std::boolalpha << true << std::endl;
// std::cout << std::noboolalpha;
std::cout << true << std::endl;

  跑跑这段代码试试看,你就明白了,std::noboolalpha是取消输出布尔值名字。

  下一个问题是,在C语言中我们可以用这段代码来输出一个数字的十六进制表示

int a = 120312;
printf("%x\n", a);

  在C++中如果不用printf可以做到吗?当然可以啦,而且我们还有更多输出:

int a{ 120312 };
std::cout << std::hex << a << std::endl;
std::cout << std::oct << a << std::endl;
std::cout << std::dec << a << std::endl;

p60

  这三个对应的就是十六进制八进制十进制的输出,同样,它也会持续到下一个进制格式化常量出现为止。除了以上介绍的几个,还有下面几个常用的格式化常量,以下的这些使用方法同上,它们不需要额外包含其他头文件:

格式化常量意义
fixed浮点格式dddd.dd
scientific科学计数法格式d.ddddEdd
showbase输出八进制数前加前缀0,十六进制前加0x
showpoint总是显示小数点
showpos对正数显示+
hexfloat小数部分和指数部分使用十六进制,指数部分以p开始
(II).iomanip

  iomanip是IO Manipulators的缩写,在这个头文件中定义了很多格式操纵符,例如std::setprecision(n) 可以保留n位有效数字,如果要保留n位小数,则再在前面加上一个std::fixed即可,例如:

double c{ 1.234567891 };
std::cout << std::setprecision(5) << c << std::endl;
std::cout << std::fixed << std::setprecision(5) << c << std::endl;

p61

  就是这样,一个保留n位有效数字,一个保留n位小数。iomanip中还有以下比较常用的格式操纵符:

格式操纵符意义
setbase(b)以b进制输出整数
setfill(int c)将填充字符设置为c
setw(n)设置下一个域宽为n个字符

  setfill和setw可以搭配使用,例如:

std::cout << std::setw(20) << std::setfill('#') << "hello!" << std::endl;

p62

  我们设置域宽为20,然后再设置填充字符为#,之后再输出文本,就会自动完成填充的操作了。

(III).std::cout和std::cin慢吗?

  你的有些OIer同学可能不爱写这个std::endl,有的可能会在代码开头加一条:

ios::sync_with_stdio(0);

  这条代码的意思是关闭与stdio的同步,而std::endl虽然在输出上看起来等价于’\n’,但实际上每次输出std::endl,都会清空输出缓冲区,这二者会比较明显地影响输入和输出的效率。C++在设计标准IO的时候考虑到了与C语言输入输出兼容的问题,所以才要与stdio同步,这也是导致cout和cin速度慢的主要原因。

  让我们来看个例子吧,有这么一道题:EOJ-3532.热河路
p63
p64

  题目本身并不难,如果你能发现规律就很好做了,这道题的关键在于第十个测试点会卡IO时间,如果纯粹采用cin和cout很有可能会超时(其实基本是一定会超时),我第一次提交的代码如下:

#include <iostream>
#include <cmath>
using namespace std;
int main()
{
    int n{0};
    int a{0};
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> a;
        a--;
        bool check{false};
        double temp{sqrt(1+8*a)};
        if ((int)pow((int)temp, 2) == 1+8*a) {
            if ((1+(int)temp)%2 == 0) {
                cout << 1 << endl;
                check = true;
            }
        }
        if (!check) {
            cout << 0 << endl;
        }
    }
    return 0;
}

  结果是:
p65

  多讨厌啊你说是吧,当时我还一直不知道为什么,甚至打算自己重写一个sqrt,之后经过他人点拨才得知,第十个测试点的输入输出特别多,如果不关闭同步会超时,于是把代码改成这样:

#include <iostream>
#include <cmath>
using namespace std;
int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(0);
    int n{0};
    int a{0};
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> a;
        a--;
        bool check{false};
        double temp{sqrt(1+8*a)};
        if ((int)pow((int)temp, 2) == 1+8*a) {
            if ((1+(int)temp)%2 == 0) {
                cout << 1 << '\n';
                check = true;
            }
        }
        if (!check) {
            cout << 0 << '\n';
        }
    }
    return 0;
}

  于是就过了,其实我当时一怒之下直接全部换成scanf和printf直接就过了,最近想起这道题才试着换掉endl,关闭同步,发现也能过,这提示我们一个事情:如果程序涉及到了相当大规模的输入输出时,你可能要考虑关闭与stdio的同步,不过关闭同步之后就一定记住:不要混用scanf和cin以及printf和cout,否则可能会产生不可预测的后果。

#2.std::istream

  std::istream对应的则是我们常用的cin,对于cin也有一些实用的技巧之类的。
  首先是get(),例如我们在C语言中常用的getc()或getchar()函数可以从stdin中获得一个字符,如果流中有未取出的字符,就直接拿出来,否则就等待标准输入设备输入一个字符,get()则是istream的一个方法,我们可以这么做:

char c = cin.get();

  通过这个方式可以从cin中提取出一个字符来,这个倒是经常和getline一起用,例如:

#include <iostream>
#include <string>

int main()
{
    std::string s;
    int a = 0;
    std::cin >> a;
    std::getline(std::cin, s);
    std::cout << s.size() << std::endl;
    return 0;
}

  这里我们先使用cin输入一个a,之后再使用getline获得一整行的输入,流提取是遇到whitespace为止,getline是遇到换行符为止,不过流提取过程中如果接收到了whitespace,它会把这个字符保留在流中不进行其他操作,但是getline会把它收入囊中,这样一来,先cin再getline得到的结果可能就不是我们想要的东西了,例如上面的代码中我们输入一个数字,最后的到的输出是:
p66

  哦不,我们输入一个数字之后这个程序直接结束了,理解一下是这样:输入数字后输入了一个换行,换行被保留在流中,运行到getline()之后它检测到了换行,于是getline()也结束了,所以s是一个空字符串,那怎么办?很简单:

#include <iostream>
#include <string>

int main()
{
    std::string s;
    int a = 0;
    std::cin >> a;
    std::cin.get(); // 加一条cin.get()
    std::getline(std::cin, s);
    std::cout << s.size() << std::endl;
    return 0;
}

p67

  这就对了,我们用cin.get()吃掉了这个多余的换行符,这样程序就可以正常运行了。
  不过有的时候你可能会发现一个cin.get()不够吃,getline()接收到了一个莫名其妙的字符,这可能与CRLF和LF有关,CRLF是回车换行,而LF是换行,在不同系统下采取的换行操作是不同的,有的时候你需要两个,但是有的时候用两个可能会多吞掉一个字符,所以这种情况下你可以用两个getline()来解决这个问题,例如:

#include <iostream>
#include <string>

int main()
{
    std::string s;
    int a = 0;
    std::cin >> a;
    std::getline(std::cin, s); // 加一条getline()
    std::getline(std::cin, s);
    std::cout << s.size() << std::endl;
    return 0;
}

  这样就不会有问题了。当然,除了get()还有很多其他的方法:

方法意义
peek()读取下一个字符,但不提取出来(peek嘛,合理)
unget()撤销上一个字符提取
putback(char c)往输入流里插入一个字符
getline(char* s, std::streamsize count, char_type delim)从流中提取最多count个字符,且直到遇到delim为止
ignore(std::streamsize count = 1, int delim = Traits::eof())从流中抛弃最多count个字符,且直到遇到delim为止,同时delim也会被抛弃

  这是其中的一些方法,比较常用,对于cin,我们还可以使用从ios继承来的一些方法,例如:

方法意义
good()检查是否没有发生错误
eof()检查是否达到了文件末尾
fail()检查是否发生了可恢复的错误
bad()检查是否发生了不可恢复的错误

(4). 文件读写与fstream

  下面一个部分是文件的读取,我们需要用到头文件fstream中定义的ifstream和ofstream分别完成文件的读取和写入操作。

#1.std::filesystem(C++17)

(I).引入filesystem

  C++17中引入了filesystem用于文件和路径的管理,我们首先需要:

#include <filesystem>

  由于filesystem的相关类都处于std命名空间的子空间filesystem中,我们调用的时候需要:

std::filesystem::xxx ...

  这就太麻烦了,即便是using namespace std了还是需要使用filesystem::xxx,所以在这里我们习惯性加上命名空间重命名:

namespace fs = std::filesystem;
(II).原始字符串R

  有这么个问题,我们在输入一个路径的时候可能要用到反斜杠,比如:

C:\Dir1\new_folder\temp\Secret.cpp

  这样看着现在好像并没有什么问题,但如果我们把它以字符串的形式输入的话:

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

int main()
{
    string a{ "C:\Dir1\new_folder\temp\Secret.cpp" };
    cout << a << endl;
    return 0;
}

p68

  6,先不说是不是有警告之类的,这个结果肯定不是我们想要的,其实从我构造的路径你应该已经看出点端倪了:\n是换行符,\t是制表符,其他的有\跟着字符的也会被认为是转义字符,从而有可能会报警告,那怎么办,我们有一种解决方案是:

string a{ "C:\\Dir1\\new_folder\\temp\\Secret.cpp" };

  把每个反斜杠都转义掉,然后结果就是:
p69

  这下是没错了,但每次我都要写两个反斜杠真的很讨厌啊,所以C++引入了原始字符串,我们在字符串前加一个R用来表示后面的字符串是个原始字符串,它的基本使用如下:

R"(一些内容\n\t\r\b\a\\你\好)"

p70

  很好,和我们字符串的内容完全一致,在用的时候要记住,字符串内一定要以圆括号开头,以圆括号结尾,否则会在编译期报错,现在有了原始字符串,我们就可以更好地输入一个路径了:

string a{ R"(C:\Dir1\new_folder\temp\Secret.cpp)" };
(III).path对象

  首先要提到的是path类,在filesystem出现之前,我们一般直接使用字符串表示文件路径,当然,出现了之后你也是可以直接用字符串表示的,但是path类有一些独特的优势能够帮助你更好地完成某些操作,首先我们来看看怎么创建一个path对象:

#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
using namespace std;

int main()
{
    fs::path p{ "test.txt" };
    return 0;
}

  同样,传入一个字符串作为路径名,然后就可以调用path的构造函数构建出path的对象了。
  这看起来好像没有看出什么好处,毕竟连path创建都也需要用字符串来完成,它当然是有好处的,例如path对象有这样一系列方法

成员函数意义
operator/=添加元素到带目录分隔符的路径
operator+=链接两个路径,不加目录分隔符
c_str()返回路径的原生字符串版本
string()返回路径的std::string版本
root_name()若存在则返回路径的根名
root_directory()若存在则返回路径的根目录
root_path()若存在返回路径的根路径
relative_path()返回相对根路径的路径
filename()返回文件名
extension()返回扩展名
empty()检查路径是否为空
is_absolute()检查路径是否为绝对路径
is_relative()检查路径是否为相对路径

  这可要比纯字符串的方式好用太多了,对吧?它能够帮助我们检测路径,探索路径的各种信息,并且还能检测是否是空路径,之后的文件读写中,我们会比较多地用到path对象,不过如果你用的是C++17之前的版本,那你可以直接用字符串代替path传入fstream,这是没问题的

#2.std::ifstream和std::ostream文本文件的读写

(I).准备工作

  要使用ifstream和ofstream,首先我们要引入头文件:

#include <fstream>
(II).基本使用

ifstream类的对象用于从文件读入数据ofstream类的对象用于向文件输出数据,因为ifstream从istream类派生得到,ofstream从ostream类派生得到,因此之前我们介绍的istream和ostream的各种基本用法,ifstream和ofstream也一样成立,例如:

#include <iostream>
#include <fstream>
#include <filesystem>
#include <string>
namespace fs = std::filesystem;

int main()
{
    fs::path p1{ "text.txt" };
    std::ifstream fin{ p1 };
    std::string line;
    while (std::getline(fin, line)) {
        std::cout << line << std::endl;
    }
    return 0;
}

  其中text.txt中的内容为:

Winter transforms the world into a magical wonderland, with a shimmering white coat. Snowflakes gently fall, creating a serene atmosphere and draping the landscape in ethereal beauty.

Playful moments abound as children build snowmen and engage in snowball fights. Laughter and joy fill the air, as friends and family come together to enjoy winter festivities.

Nature's contrasts come alive, with vibrant red berries and evergreen trees standing out against the snowy backdrop. The stillness of the scene offers peace and introspection.

Winter activities like ice skating and sledding bring excitement and adventure. The thrill of gliding on ice or speeding down a snowy slope connects us with nature.

Winter evenings sparkle with twinkling lights, creating a magical ambiance. Cozy indoor spaces and starry skies invite reflection and togetherness.

Winter's enchanting beauty lies in its transformative power and ability to bring joy, awe, and moments of serenity. Let us embrace the magic of snowy winter and cherish its wonders.

  运行结果如下:
p71

  看着很好玩吧?什么流提取getline之类的用法跟cin都是完全一致的,这一部分的内容我就不细说了,我们来说说文件IO的一些特别之处。
  首先对于一个流,不可能是对于一个文件读写完了就得换一个流的,举个例子:和谐号和复兴号都是高铁,它们用的铁道规格完全一致,同样的铁道可以跑这样的两辆车,有一天复兴号要取代和谐号了,那总不能说为了这件事情要把所有铁道全部重新建一遍吧?这样费时费力而且没有必要,我们只需要把和谐号从铁路上拿下来,再把复兴号放上去,就好了,这样车次就完成替换了。 对于文件也是这样,我们只要把这个文件关掉,然后重新打开另一个文件就好了,例如:

int main()
{
    fs::path p1{ "text1.txt" }, p2{ "text2.txt" };
    std::ifstream fin{ p1 };
    std::string line;
    while (std::getline(fin, line)) {
        std::cout << line << std::endl;
    }
    fin.close();
    fin.open(p2);
    while (std::getline(fin, line)) {
        std::cout << line << std::endl;
    }
    return 0;
}

  这样就好了,先调用fstream的close()方法关掉当前文件,然后再使用open() 方法打开某个文件,这样就好了,对于fstream,我们不需要显式完成close()的操作,这和C语言的文件不同。
  因为C++中存在RAII的机制,简单说就是C++保证对象的生命周期开始一定会调用构造函数,结束时一定会调用析构函数,那么我们把文件句柄(用于操作文件的东西,句柄是handler的翻译,我真的很不喜欢这个翻译) 的管理交给fstream,它保证在析构时会关闭文件句柄,从而一定可以保证文件被正常关闭。关于RAII,我会在异常处理篇中再次提到。

(III).读写模式

  在之前的C语言教程中我们曾经提到过读写二进制文件,当时我们说的是把某个结构体对象的数据写入二进制文件,下一次使用的时候还可以从中读取出来
  我们利用ifstream当然也可以完成这个操作,不过使用的方法有点不太一样,首先有这样一些模式:

模式意义
app每次写入前寻位到流结尾
binary以二进制模式打开
in以读打开
out以写打开
trunc在打开时舍弃流的内容
ate打开后立即寻位到流结尾

  其中in和out在ifstream和ofstream对象中用不到,fstream头文件中有一个大一统的类叫做fstream,对应in和out,它可以分别完成读文件写文件的操作,相较于ifstream和ofstream单独使用更加方便,在这里我更习惯分开使用。
  我们这里介绍一下app、ate和binary,首先这些模式的使用方式如下:

int main()
{
    fs::path p1{ "text.txt" };
    std::ofstream fout{ p1, std::ios::app };
    std::string line;
    fout << "A new line!" << std::endl;
    fout.close();
    fout.open(p1, std::ios::ate | std::ios::binary);
    return 0;
}

  在打开文件和创建流的过程中我们都可以使用这些模式,如果有多个模式,我们使用 | 即逻辑或符号进行连接.
  在这里app模式代表append,是在文件末尾追加写入,无论你移动游标位置到哪里,在写入时一定会被定位到文件末尾,并且进行写入。而ate模式代表at end,是将游标定位到文件末尾,而binary模式就是二进制模式,在读和写的过程中都可以采用binary模式完成,二进制模式的内容,我们下一部分继续讲。

(IV).关于游标/指针/指示器

  这个部分的标题看着有点迷惑,其实它就是你打开某个文件编辑器,那个正在闪烁的东西,我习惯于叫它游标,因为指针(cursor)有的时候容易和存储地址的指针(pointer)混淆。
  对于文件IO,我们需要特别注意游标的位置,例如前面提到的app模式,每次写入的时候保证把游标定位到文件末尾,在所有的IO流中,我们都可以对输入流使用tellg和seekg对输出流使用tellp和seekp完成对于游标的处理,其中tell告诉我们当前游标位置,seek则可以指定游标的位置,例如:

#include <iostream>
#include <filesystem>
#include <fstream>
#include <string>
namespace fs = std::filesystem;
using namespace std;

int main()
{
    fs::path p{ "text.txt" };
    ifstream fin{ p };
    string line;
    getline(fin, line);
    cout << line << endl;
    fin.seekg(30);
    getline(fin, line);
    cout << line << endl;
    cout << fin.tellg() << endl;
    return 0;
}

p82

  我们首先读取一行,然后用seekg把游标位置改到30,再读取一行,得到的就不是下一行了,而是从30开始的第一行内容,就如上图所示的那样,对于输出流的tellp和seekg这里就不再演示了。

#3.std::ifstream和std::ostream二进制文件的读写

  因为二进制文件的读写和文本文件的读写差距很大,所以我决定单独用一个部分来讲讲这个。C++中二进制文件的读写是采取以字节流形式读写的方式完成的,也就是说,我们需要把对象的指针转为字符指针,以字符串的形式完成对二进制文件的读写操作,接下来我们仔细说说怎么做到这件事情。

(I).一些魔法和reinterpret_cast

  再提提C语言教程,我们在最后一篇中讲过这样一件事,我们可以强制转换指针类型,然后以新的指针类型读取数据,对应代码是这样的:

#include <stdio.h>
int main()
{
    double c = 1.234, d = 99.81;
    printf("c = %.3f, d = %.3f\n", c, d);
    *(long long*)&c ^= *(long long*)&d;
    *(long long*)&d ^= *(long long*)&c;
    *(long long*)&c ^= *(long long*)&d;
    printf("c = %.3f, d = %.3f\n", c, d);
    return 0;
}

  这里演示了用异或交换两个double数据的方法,我们知道,C++中对应于C的强制类型转换都有对应的形式,例如static_cast,对于指针,我们也有这样的形式

reinterpret_cast<新类型>(表达式);

  它可以完成我们需要的事情,所以上面的代码用C++重写之后就应该是这样:

#include <iostream>
using namespace std;
int main()
{
    double c{ 1.234 }, d{ 99.81 };
    cout << "c = " << c << ", d = " << d << endl;
    *reinterpret_cast<long long*>(&c) ^= *reinterpret_cast<long long*>(&d);
    *reinterpret_cast<long long*>(&d) ^= *reinterpret_cast<long long*>(&c);
    *reinterpret_cast<long long*>(&c) ^= *reinterpret_cast<long long*>(&d);
    cout << "c = " << c << ", d = " << d << endl;
    return 0;
}

p72

  很好,它也完成了对应的工作。

(II).写入和读取

  对于二进制文件,我们需要用到write和read两个函数,眼熟吧?它们对应C语言中的fwrite和fread两个函数,不过C++中的这俩是以fstream对象的方法出现的,我们需要这么调用:

#include <iostream>
#include <fstream>
#include <filesystem>
namespace fs = std::filesystem;

class A
{
public:
    int a;
    int b;
    A() : a(122341), b(5123) {}
    ~A() = default;
};

int main()
{
    fs::path p{ "a.dat" };
    std::ofstream fout{ p, std::ios::binary | std::ios::app};
    A a;
    for (int i = 0; i < 3; i++) {
        fout.write(reinterpret_cast<char*>(&a), sizeof(A));
    }
    fout.close();
    std::ifstream fin{ p, std::ios::binary};
    A b;
    for (int i = 0; i < 3; i++) {
        fin.read(reinterpret_cast<char*>(&b), sizeof(A));
        std::cout << b.a << "\t" << b.b << std::endl;
    }
    return 0;
}

p73

  这很好啊,按照我们的需求完成了的操作,这里再说说write和read的用法,这二者都有两个参数,第一个是字节流/字符指针第二个是字节数,在这里我们把对象指针重新解释为字符指针,然后指定读写特定多个字节数,这样就可以完成读取和写入了。

  读写的规则和文本文件是一致的,因此这里就不再赘述了。所以我们就可以通过重新解释指针为字符指针的方式完成对于二进制文件的读写了。

(5). 字符串流与sstream

  字符串流,是什么?C++中可以把字符串也作为流操作的对象,听起来好像是相当的神奇啊,我们举个例子,你大概就明白了:

#include <iostream>
#include <string>
#include <sstream>
using namespace std;

int main()
{
    string s;
    getline(cin, s);
    for (auto& c : s) {
        if (c == ',') c = ' ';
    }
    istringstream sin{ s };
    string p;
    while (sin >> p) {
        cout << p << endl;
    }
    return 0;
}

p74

  没错,在全部替换为空格后,流中的内容变为了Country road take me home to the place I belong,这时候在调用流提取运算符,它就会同cin一样,从流中一个个提取出来,遇到whitespace就停止当前的接收。

  太美妙了,字符串流用起来和之前的所有流基本都是一样的,同样的,我们也可以构造stringstream来构造字符串(stringstream同时兼备了istringstream和ostringstream的方法,因为多次构造字符串流效率会比较低,因此在这里直接使用stringstream完成操作):

#include <iostream>
#include <string>
#include <sstream>
using namespace std;

int main()
{
    string s;
    stringstream stream;
    stream << "Country road, " << "take me home." << endl << "To the place I belong.";
    while (getline(stream, s)) {
        cout << s << endl;
    }
    return 0;
}

p75

  不过字符串流也不一定只能输出到string对象中,因为它是流,所以它满足对于各种数据的输入与输出,我们来看看下面这个例子:

#include <iostream>
#include <sstream>
#include <string>
using namespace std;

int main()
{
    string s{ "1 2 3 4 5 6 7" };
    stringstream stream{ s };
    int c{ 0 };
    while (stream >> c) {
        cout << c << '\t';
    }
    cout << endl;
    return 0;
}

p76

  int类型的数字也可以完全正常的读取呢!所以字符串流对于我们来说的确会是个很方便的东西,我们来看一道题:EOJ-899-赛博计算机2077
p77
p78
p79
p80
  这好像已经是我第二次提到这道题了,乐,这次我们来解决一下输入的问题,你会发现它输入的数据还是规则的,比如:DIV DX,CX,BX,对于这种输入,我们可以这么处理:

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

int main()
{
    string line;
    getline(cin, line);
    for (auto& c : line) {
        if (c == ',') c = ' ';
    }
    vector<string> words;
    string temp;
    istringstream sin{ line };
    while (sin >> temp) {
        words.push_back(temp);
    }
    for (auto& str : words) {
        cout << str << endl;
    }
    return 0;
}

p81

  这样就把一条语句中的内容全部分别提取出来了,对于我们后续完成这道题还是相当方便的,不过考虑到后面应该不会再提到这道题了,我在这里给出我的代码

#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;

map<string, int> registers
{
  {"AX", 0}, {"BX", 0}, {"CX", 0}, {"DX", 0}, {"EX", 0},
  {"FX", 0}, {"GX", 0}, {"HX", 0}, {"IX", 0}, {"JX", 0},
  {"KX", 0}, {"LX", 0}, {"MX", 0}, {"NX", 0}, {"OX", 0},
  {"PX", 0}, {"QX", 0}, {"RX", 0}, {"SX", 0}, {"TX", 0},
  {"UX", 0}, {"VX", 0}, {"WX", 0}, {"XX", 0}, {"YX", 0},
  {"ZX", 0}
};

void IN(const string& reg, int number)
{
    registers[reg] = number;
}

void OUT(const string& reg)
{
    cout << registers[reg] << '\n';
}

void MOV(const string& reg1, const string& reg2)
{
    registers[reg1] = registers[reg2];
}

void XCHG(const string& reg1, const string& reg2)
{
    int temp{ registers[reg1] };
    registers[reg1] = registers[reg2];
    registers[reg2] = temp;
}

void ADD(const string& reg1, const string& reg2, const string& reg3);
void SUB(const string& reg1, const string& reg2, const string& reg3);
void MUL(const string& reg1, const string& reg2, const string& reg3);
void DIV(const string& reg1, const string& reg2, const string& reg3);
void MOD(const string& reg1, const string& reg2, const string& reg3);
void AND(const string& reg1, const string& reg2, const string& reg3);
void OR( const string& reg1, const string& reg2, const string& reg3);
void XOR(const string& reg1, const string& reg2, const string& reg3);
void Interpreter(const string& line);

map
<string, void (*)(const string&, const string&, const string&)>
commands
{
    {"ADD", ADD}, {"SUB", SUB}, {"MUL", MUL}, {"DIV", DIV},
    {"MOD", MOD}, {"AND", AND}, {"OR" , OR }, {"XOR", XOR}
};

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    string cmd;
    while (getline(cin, cmd)) {
        Interpreter(cmd);
    }
    return 0;
}

void ADD(const string& reg1, const string& reg2, const string& reg3)
{
    if (reg3 != "") registers[reg1] = registers[reg2] + registers[reg3];
    else registers[reg1] += registers[reg2];
}

void SUB(const string& reg1, const string& reg2, const string& reg3)
{
    if (reg3 != "") registers[reg1] = registers[reg2] - registers[reg3];
    else registers[reg1] -= registers[reg2];
}

void MUL(const string& reg1, const string& reg2, const string& reg3)
{
   if (reg3 != "") registers[reg1] = registers[reg2] * registers[reg3];
    else registers[reg1] *= registers[reg2];
}

void DIV(const string& reg1, const string& reg2, const string& reg3)
{
    if (reg3 != "") registers[reg1] = registers[reg2] / registers[reg3];
    else registers[reg1] /= registers[reg2];
}

void MOD(const string& reg1, const string& reg2, const string& reg3)
{
    if (reg3 != "") registers[reg1] = registers[reg2] % registers[reg3];
    else registers[reg1] %= registers[reg2];
}

void AND(const string& reg1, const string& reg2, const string& reg3)
{
    if (reg3 != "") registers[reg1] = registers[reg2] & registers[reg3];
    else registers[reg1] &= registers[reg2];
}

void OR(const string& reg1, const string& reg2, const string& reg3)
{
    if (reg3 != "") registers[reg1] = registers[reg2] | registers[reg3];
    else registers[reg1] |= registers[reg2];
}

void XOR(const string& reg1, const string& reg2, const string& reg3)
{
    if (reg3 != "") registers[reg1] = registers[reg2] ^ registers[reg3];
    else registers[reg1] ^= registers[reg2];
}

void Interpreter(const string& line)
{
    int regs_beg = line.find(' ');
    string cmd{ line.substr(0, regs_beg) };
    if (cmd != "OUT") {
        if (cmd == "IN") {
            string reg1{ line.substr(regs_beg + 1, 2) };
            int num{ stoi(line.substr(regs_beg + 4, 100)) }; // Max-length of int is shorter than 100
            IN(reg1, num);
        }
        else if (cmd == "MOV") {
            string reg1{ line.substr(regs_beg + 1, 2) }, reg2{ line.substr(regs_beg + 4, 2) };
            MOV(reg1, reg2);
        }
        else if (cmd == "XCHG") {
            string reg1{ line.substr(regs_beg + 1, 2) }, reg2{ line.substr(regs_beg + 4, 2) };
            XCHG(reg1, reg2);
        }
        else {
            string temp;
            vector<string> regs;
            for (int i{ regs_beg+1 }; i < line.size(); i++) {
                if (line[i] != ',') temp.push_back(line[i]);
                else {
                    regs.push_back(temp);
                    temp.clear();
                }
                if (i == line.size() - 1) {
                    regs.push_back(temp);
                    temp.clear();
                }
            }
            if (regs.size() == 2) regs.push_back("");
            commands[cmd](regs[0], regs[1], regs[2]);
        }
    }
    else {
        cout << registers[line.substr(regs_beg + 1, 2)] << '\n';
    }
}

  我在这里一开始没有考虑使用stringstream,如果用了的话应该能够大大减少工作量才对,我的解答当然不是最优的,如果你有更好、更快的解决方案,记得在评论中留言哦。

(6). 输入输出流和多态

  前面提到说,后面的stringstream和fstream都是由iostream等派生来的,所以理论上讲,我们可以用istream&来保存istringstream和ifstream对象,ostream&保存ostringstream和ofstream对象,不过这有什么用呢,我们看个例子:

#include <iostream>
#include <filesystem>
#include <fstream>
#include <string>
namespace fs = std::filesystem;
using namespace std;

unsigned long long factorial(unsigned long long x)
{
    unsigned long long ans = 1;
    for (unsigned long long i = 1; i <= x; i++) {
        ans *= i;
    }
    return ans;
}

bool calculate(std::ostream& out)
{
    double e{ 0.0 };
    for (int i = 0; i < 20; i++) {
        e += 1 / static_cast<double>(factorial(i));
    }
    out << e << endl;
    return (bool)out;
}

int main()
{
    fs::path p{ "text.txt" };
    ofstream fout{ p, ios::app };
    bool ans{ calculate(fout)};
    cout << boolalpha << ans << endl;
    ans = calculate(cout);
    cout << ans << endl;
    return 0;
}

  这个calculate可以计算e的值,并传出到out中,在这里我们第一次传入了fout,输出到文件,第二次传入了cout,输出到标准输出设备,结果是:
p83

  这很不错啊,这样一来我们就可以对多种形式的流进行处理了,只要使用std::ostream&作为流参数的类型就好了!

  这里还有一个点要强调一下,C语言中我们会这么处理接收到EOF截止的需求:

while (scanf("%d", &a) != EOF) {
    ...
}

  scanf函数会返回成功输入的个数,如果接收到EOF就返回-1,对应的也就是EOF,而在C++中,我们经常可以看到这样的写法:

while (cin >> a) {
    ...
}

  很神奇,我们来解析一下这个流程,首先流提取运算符的定义中,返回类型是它本身,例如cin会返回一个std::istream&,就以运算符重载为例:

friend std::istream& operator>>(std::istream& in, const Complex& c1)
{
    ...
    return in;
}

  所以cin完成了流提取操作之后,会返回本身,从而使得我们能够连续输入,而std::istream类型重载了到bool类型的类型转换运算符,当流处于正常状态的时候,会返回true,否则返回false,例如接收到了EOF,就会返回false,因此在这里就使得cin返回false,从而停止输入的过程。

小结

  擦,这一篇也比我想的长好多,本来我估计会比前面的几篇短很多,但是实际上还是差不多。这一篇主要介绍了对于标准输入、文件和字符串的IO流的基本使用,实际上,C++中的IO操作并不只有我们这次介绍的这些以面向对象形式出现的流,因为兼容C语言,你可以使用FILE*完成操作,C++20中还引入了std::print和std::println,它们可以直接完成格式化输出的操作,你也可以去了解一下。
  下一篇让我们来了解了解STL(Standard Template Library)的一些基本容器、迭代器和算法,敬请期待。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值