《C++ Primer 中文版》第八 章读书笔记及习题解答

IO库

简介

  C++语言不直接处理输入输出,而是通过一族定义在标准库的类型来处理IO。这些类型支持从设备读取数据,向设备写入数据的IO操作,设备可以是文件、控制台窗等。还有一些类型运行内存IO,即从string读取数据,向string写入数据。
  IO库定义了读写内置类型值的操作。此外,一些类,如string,通常也会定义类似的IO操作,来读写自己的对象。
  本章介绍IO库的基本内容。后续章节会介绍更多IO库的功能:14章将会介绍如何编写自己的输入输出运算符,第17章将会介绍如何控制输出格式以及如何对文件进行随机访问。
  我们的程序已经使用了很多IO库设施了。我们在1.2节已经介绍了大部分IO库设施:
    istream(输入流)类型,提供输入操作。
    ostream(输出流)类型,提供输出操作。
    cin,一个istream对象,从标准输入读取数据。
    cout,一个ostream对象,从标准输出读取数据。
    cerr,一个ostream对象,通常用于输出程序错误消息,写入到标准错误。
    >>运算符,用来从istream对象中读取数据。
    <<运算符,用来从ostream对象中写入数据。
    getline函数,从一个给定的istream读取一行 数据,存入一个给定的string对象中。

8.1 IO类

  到目前为止,我们已经使用过的IO类型和对象都是操作char数据的。默认情况下,这些对象都是关联到用户的控制台窗口。当然,我们不能限制实际应用程序仅从控制台窗口进行IO操作,应用程序常常需要读写命令文件。而且,使用IO操作string中的字符会很方便。此外,应用程序还可能读写需要支持宽字符支持的语言。
  为了支持这些不同种类的IO处理操作,在istream和ostrem之外,标准库还定义了其他一些IO类型。在本书表8.1列出了这些类型,分别定义在三个独立的头文件中:iostream定义了用于读写流的基本类型,fstream定义了读写命名文件的类型,sstream定义了读写内存string对象的类型。
  为了支持使用宽字符的语言,标准库定义了一组类型和对象来操纵wchar_t类型的数据。宽字符版本的类型和函数的名字以一个w开头。宽字符版本的类型和对象与其对应的普通char版本的类型定义在同一个头文件中。例如,头文件fstream定义了ifstream和wifstream类型。

IO类型间的关系
  概念上,设备类型和字符大小都不会影响我们要执行的IO操作。例如我们可以用>>读取数据,而不用管是从一个控制台窗口,一个磁盘文件,还是一个string读取。类似的,我们也不用管读取的字符能存入一个char对象内,还是需要一个wchar_t对象来存储。
  我们不用管这些的原因是因为标准库能使我们忽略这些不同类型的流之间的差异,这是通过继承机制(inheritance)实现的。利用模板,我们可以使用具有继承关系的类,而不必了解继承机制如何工作的细节。我们将在第15章和18.3节介绍C++是如何支持继承机制的。
  简单来说,继承机制使我们可以声明一个特定的类继承自另一个类。我们通常可以将一个派送类(继承类)对象当做其基类(所继承的类)对象使用。
  类似ifstream和istringstream都继承自istream。因此,我们可以像使用istream对象一样来使用ifstream和istringstream对象。也就是说,我们是如何使用cin的,就可以同样地使用这些类型的对象。例如,可以对一个ifstream或istringstream对象调用getline,也可以使用>>从一个ifstream或istringstream对象中读取数据。类似的,类型ofstream和ostringstream都继承自ostream。因此,我们是如何使用cout的,就可以同样地使用这些类型的对象。
  本节剩下部分所介绍的标准库流特性都可以无差别地应用于普通流、文件流和string流,以及char或宽字符流版本。

8.1.1 IO对象无拷贝或赋值

  如我们在7.1.3 节所见,我们不能拷贝或对IO对象赋值:

ofstream out1,out2;
out1 = out2;//错误,不能对流对象赋值
ofstream print(ofstream);//错误,不能初始化ofstream参数
out2 = print(out);//错误,不能拷贝流对象

  由于不能拷贝IO对象,因此我们也不能将形参或返回类型设置为流类型。进行IO操作的函数通常以引用方式传递和返回流。读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的

8.1.2 条件状态

  IO操作一个与生俱来的问题是可能发生错误。一些错误是可恢复的,而其他错误则发生在系统深处,以及超出了应用程序可以修正的范围。表8.2列出了IO类所定义的一些函数和标志,可以帮助我们访问和操纵流的条件状态(condition state)。
  下面是一个IO错误的例子:

int ival;
cin >> ival;

  如果我们在标准输入上键入Boo,读操作就会失败。因为输入运算符期待读取一个int,但却得到了一个字符B。这样,cin会进入错误状态。类似,我们输入一个文件结束标志,cin也会进入错误状态。
  一个流一旦发生错误,其后续的IO错误都会失败。只有一个流处于无错状态,我们才可以从它读取数据,写入数据。由于流可能处于错误状态,因此代码通常应该在使用一个流之前检查它是否处于良好状态。确定一个流对象的状态最简单的方法就是将它作为一个条件使用:

while(cin >> word)//循环检查>>表达式返回的流的状态,如果输入操作成功,流保持有效状态,则条件为真。

查询流的状态
  将流作为条件使用,只能告诉我们流是否有效,而无法具体告诉我们发生了什么。有时我们也需要知道流为什么失败。例如,在键入文件结束标识符后我们的应对措施,可能与遇到一个IO设备错误的处理方式是不同的。
  IO库定义了一个与机器无关的iostate类型,它提供了表达流状态的完整功能。这个类型应作为一个位集合来使用,使用方式与我们在4.8节中使用quiz1的方式一样。IO库定义了4个iostate类型的constexpr值,表示特定的位模式。这些值用来表示特定类型的IO条件,可以与位运算符一起使用来一次性检测或设置多个标志位。
  badbit表示系统级错误,如不可恢复的读写错误。通常情况下,一旦badbit被置位,流就无法使用了。
  在发生可恢复错误后,failbit被置位,如期望读取数值却读出一个字符等错误。这种问题通常是可以修正的,流还可以继续使用。
  如果到达文件结束位置,eofbit和failbit都会被置位。
  goodbit的值为0,表示流未发生错误。
  如果badbit、failbit和eofbit任一个被置位,则检测流状态的条件会失败。

  标准库还定义了一组函数来查询这些标志位的状态。操作good在所有错误位均为置位的情况下返回true,而bad、fail和eof则在对应错误位被置位时返回true。此外,在badbit被置位时,fail也会返回true。这意味着,使用good和fail是确定流的总体状态的正确方法。 实际上,当我们将流作为条件使用的代码就等价于!fail()。而eof和bad只能表示特定的错误。

管理条件状态
  流对象的rdstate返回一个iostate值,对应流当前的状态。setstate操作将给定条件位置位,表示发生了对应错误。clear成员是一个重载的成员:它有两个版本,一个接受iostate参数一个不接受。
  不接受参数版本清除(复位)所有错误标志位。执行clear()后,调用good会返回true。我们可以这样使用这些成员:

//记住cin的当前状态
auto old_state = cin.rdstate();//记住cin的当前状态
cin.clear();//使cin有效
process_input(cin);//使用cin
cin.setstate(old_state);//将cin置为原有状态

  带参数版本接受一个iostate值,表示流的新状态。为了复位单一的条件状态位,我们首先使用rdstate读取当前条件状态,然后用位操作将所需位复位来生成新的状态。例如,下面的代码将failbit和badbit复位,但保持eofbit不变:

//复位failbit和badbit,保持其他位不变
cin.clear(cin.rdstate()& ~cin.failbit & ~cin.badbit);

8.1.3 管理输出缓冲

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

os << "please enter a value: ";

  文本串可能立刻打印出来,但也有可能被操作系统暂存在缓冲区中,随后再打印。有了缓冲机制,操作系统就可以将程序的多个输出操作合成单一的系统级写操作。因为设备的写操作可能很耗时,允许操作系统将多个输出操作组合成单一的设备写操作可以带来很大的性能提升。
  导致缓冲刷新(即,数据真正写到设备或文件)的原因有很多:
    1.程序正常结束,作为main函数的return操作的一部分,缓冲刷新被执行。
    2.缓冲区满,需要刷新缓冲,之后的数据才能继续写入缓冲区。
    3.我们可以使用操纵符如endl,来显式刷新缓冲区。
    4.在每个输出操作后,我们都可以通过用操纵符unitbuf来设置流的内部状态,来清空缓冲区。在默认情况下,对cerr是设置unitbuf的,因此写到cerr的内容都是立即刷新的
    5.一个输出流可能被关联到另外一个流。在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。例如,在默认情况下,cin和cerr都关联到cout。因此,读cin或写cerr都会导致cout的缓冲区被刷新。

刷新输出缓冲区
  我们已经使用过 操纵符endl,它完成换行并刷新缓冲区的工作。IO库里还有两个类似的操纵符:flush和ends。flush刷新缓冲区,但不输出任何额外的字符;ends向缓冲区插入一个空字符,然后刷新缓冲区。

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

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

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

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

关联输入输出流
  当一个输入流被关联到一个输出流,任何试图从输入流读取数据的操作都会刷新关联的输出流。标准库将cout和cin关联在一起,因此下面的语句,将导致cout的缓冲区刷新:

cin >> ival;

  交互式系统通常应该关联输入流和输出流。这意味着所有输出,包括用户提升信息,都会在读操作之前被打印出来
  tie有两个重载版本,一个版本不带参,返回指向输出流的指针。如果本对象当前关联到一个输出流,则返回的就是指向这个流的指针,如果未关联到输出流,则返回空指针。tie的第二个版本接受一个指向ostream的指针,将自己关联到此ostream,并返回前任关联的ostream指针。即,x.tie(&o) 将流x关联到输出流o,返回x之前关联的ostream指针。
  我们既可以将一个istream对象关联到另一个ostream,也可以将一个ostream关联到一个ostream:

cin.tie(&cout);//仅仅用来展示,标准库已经将cin和cout关联
//old_tie指向当前关联到的cin流(如果有)
ostream *old_tie = cin.tie(nullptr);//cin关联到nullptr(不再与其他流关联)
cin.ite(&cerr);//关联到cerr,读取cin会刷新cerr而不是cout了
cin.tie(old_tie);//重建cin和cout的正常关联

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

8.2 文件输入输出

  头文件fstream定义了三个类型来支持文件IO:
    ifstream从一个给定文件读取数据。
    ofstream向一个给定文件写入数据。
    fstream 可以读写给定文件。在17.5.3节中将介绍如何对同一个流既读又写。
  这些类型提供的操作与我们之前已经使用过的对象cin和cout的操作一样。特别是,我们可以用IO运算符(<<和>>)来读写文件,可以用getline(3.2.2)从一个ifstream读取数据,包括8.1节介绍的内容也都适用于这些类型。
  除了继承自iostream类型的行为外,fstream中的定义类型还增加了一些新的成员来管理与流关联的文件。在表8.3中列出了这些操作,我们可以对fstream、ifstream和ofstream对象调用这些操作,但不能对其他IO类型调用这些操作

8.2.1 使用文件流对象

  当我们想要读写一个文件时,可以定义一个文件流对象,并将对象与文件关联起来。每个文件流类都定义了一个名为open的成员函数,它完成了一些系统相关的操作,来定位给定的文件,并视情况打开为读或写模式。
  创建文件流对象时,我们可以提供文件名(可选)。如果提供了 一个文件名,则open会自动被调用:

ifstream in(ifile)://构造一个ifstream并打开给定文件
ofstream out;//输出文件流未关联到任何文件

  这段代码定义了一个输入流in,它被初始化从文件读取数据,文件名由string类型的参数ifile指定。第二条语句定义了一个输出流out,未与任何文件关联。在新的C++标准中,文件名既可以时库类型string对象,也可以是C风格字符数组。旧版本的标准库只允许 C风格字符数组。

用fstream代替iostream&
  在8.1节已经提到过,在要求使用基类对象的地方,我们可以用继承类型的对象来代替。这意味着,接受一个iostream类型引用(或指针)参数的函数,可以用一个对应的fstream(或sstream)类型来调用。也就是说,如果一个函数接受ostream&参数,我们在调用这个函数时,可以传递给它一个ofstream对象,对istream&和ifstream也是类似的
  例如,我们可以使用7.1.3节中的read和print函数来读写命名文件。在本例中,我们假定输入和输出文件的命名时通过传递给main函数的参数来指定的:

ifstream input(argv[1]);//打开销售记录
ofstream output(argv[2]);//打开输出文件
Sales_data total;//保存销售总额的变量
if(read(input,total)){//读取第一条记录 
	Sales_data trans;//保存下一条记录的变量 
	while(read(input,total)){//读取剩余记录 
		if(total.isbn() == trans.isbn())//检查isbn 
			total.combine(trans);//更新销售记录 
		else{
			print(output,total) << endl;//打印处理结果 
			total = trans;//处理下一本书 
		}
	}
	print(output,total) << endl;//打印最后一本书的销售额 
} 
else
	cerr << "No data?!" <<endl;//文件中无输入数据 

  除了读写的是命名文件外,这段程序与229页的加法程序几乎相同。重要的部分是 对read和print的调用。虽然这两个函数定义时指定的形参分别是istream&和ostream&,但我们可以向它们传递fstream对象。

成员函数open和close
  如果我们定义一个空文件流对象,可以随后调用open来将它与文件夹关联起来

ifstream in(ifile);//构筑一个ifstream并打开给定文件
ofstream out;//输出文件流未与任何文件关联
out.open(ifile+".copy");//打开指定文件

  如果调用open失败,failbit会被置位(8.1.2)。因为调用open可能失败,进行open是否成功的检验通常是一个好习惯:

if(out)//检查open是否成功,成功我们就可以使用文件了

  如果open成功,则open会设置流的状态,使得good()为true。

自动构造和析构
  考虑这样一个程序,它的main函数接受一个要处理的文件列表。这种程序可能会有以下循环:

//对于每个传递给程序的文件执行循环操作
for(auto p = argv + 1;p != argv + argc;++p){
	ifstream input(*P);//创建输出流并打开文件
	if(input){//如果文件打开成功,处理此文件
		process(input);
	}else
		cerr << "couldnt open :"+string(*p);
}//每个循环步input都会离开作用域因此会被销毁

  每个循环步构造一个新的名为input的ifstream对象,并打开它来读取给定文件。像之前一样,我们检查open是否成功。如果成功,将文件传递给一个函数,该函数赋值读取并处理输入数据。如果open失败,打印一条错误信息并继续处理下一个文件。
  因为input是while循环的局部变量,它在每个循环步中都要创建销毁一次。当一个fstream对象离开其作用域时,与之关联的文件会自动关闭。在下一步循环中,input会再次被创建。

8.2.2 文件模式

  每个流都有关联的文件模式,用来指出如何使用文件。表8.4列出了文件模式和它们的含义。
在这里插入图片描述
  无论用什么方式打开文件,我们都可以指定文件模式。指定文件模式有如下限制:
在这里插入图片描述
  每个文件流类型都定义了一个默认的文件模式,当我们没指定文件模式时,就使用此默认模式。与ifstream关联的文件默认以in模式打开;与ofstream关联的模式以out模式打开;与fstream关联的文件模式以in和out模式打开

以out模式打开文件会丢弃已有数据
  默认情况下,当我们打开一个ofstream时,文件的内容会被丢弃。阻止一个ofstream清空文件内容的方法是同时指定app模式
在这里插入图片描述
  保留被ofstream打开的文件中已有数据的唯一方法是显式指定app或in模式。

每次调用open时都会确定文件模式
  对于一个给定流,每当打开文件时,都可以改变其文件模式:
在这里插入图片描述
  第一个open调用时,未显式指定输出模式,文件隐式的以out模式打开。通常情况下,out模式意味着同时使用trunc模式。 因此,当前目录下名为scratchpad的文件的内容将被清空。当打开名为precious的文件时,我们指定了append模式,文件中已有的数据都得到保留,所有写操作都在文件末尾进行。
  每次打开文件时,都要设置文件模式,可能显式的设置,也可能是隐式地设置。当程序未指定模式时,就使用默认值

8.3 string流

  sstream头文件定义了三个类型来支持内存IO,这些类型可以向string写入数据,从string读取数据,就像string是一个IO流一样。
  isstringstream从string读取数据,ostringstream向string写入数据,而头文件stringstream二者都可。与fstream类型类似,头文件sstream中定义的类型都继承自我们已经使用过的iostream头文件中 定义过的类型。除了继承得来的操作,sstream中定义的类型还增加了一些成员来管理与流相关的string。表8.5列出了这些操作,可以对stringstream对象调用这些操作,但不能对其他IO类型调用。
在这里插入图片描述

8.3.1 使用istringstream

  当我们 的某些工作是对整行文本进行处理,而其他一些工作是处理行内的单个单词时,可以使用isstringstream。
  考虑这样一个例子:假定有一个文件,列出了一些人的电话号码。某些人只有一个号码,而有些人则有多个,所以我们的输入文件可能是这样的:
在这里插入图片描述
  我们首先定义一个简单类来描述输入数据:

//成员默认为publc
struct PersonInof {
	string name;
	vector<string> phones;
};

  我们的程序会读取数据文件,并创建一个PersonInfo的vector。这个vector中每个元素对应文件中的一条记录。我们在一个循环中处理输入数据,每个循环读取一条记录,提取出一个人名和若干电话号码:

string line, word;//分别保存来自输入的一行和单词
vector<PersonInfo> people;//保存来自输入的所有记录
//逐行从输入读取数据,纸质cin遇到文件尾(或其他错误)
while (getline(cin, line)) {
	PersonInfo info;//创建一个保存此记录数据的对象
	isstringstream record(line);//将记录绑定到刚读入的行
	record >> info.name;//读名字 
	while (record >> word)	info.phones.push_back(word);//读入号码,保持他们
	people.push_back(info);//将此纪录追加到people末尾 
} 

  这里我们用getline从标准输入读取整条记录。如果getline调用成功,那么line中将保存输入文件而来的一条记录。在while中,我们定义了一个局部PersonInfo,来保存当前记录中的数据。
  接下来我们将一个isstringstream与刚读取的文本进行绑定,这样我们就可以在此isstringstream上使用输入运算符来读取当前记录中的每个元素。先读人名,后用while循环读取此人的所有号码。
  当读取完line中所有数据时,内层while循环就结束了。此循环的工作方式与前面章节中读取cin的循环很相似,不同的是,此循环从一个string而不是标准输入读取数据。当string中的数据全部读出后,同样会触发“文件结束”信号,在record上的下一个输入操作会失败。退出内层while循环。
  我们将刚处理好的PersonInfo追加到vector中,外层while循环的一个循环步就随之结束了。外层while循环会持续执行,直至遇到文件结束标识。

8.3.2 使用ostringstream

  当我们逐步构造输出,希望最后一起打印时,ostringstream是很有用的。例如,对于上一小节的例子,我们可能逐个验证电话号码并改变其格式。如果所有号码都是有效的,我们希望输出一个新的文件,内含改变格式后号码。对于那些无效的号码,我们不会将它们输出到新文件,而是打印一条包含人名和无效号码的错误信息。
  由于我们不想输出无效电话号码的人,因此对每个人,直到验证完所有电话号码后才可以进行输出操作。但是我们可以先将输出内容”写入到“一个内存ostringstream中

for (const auto &entry : people) {//对people中的每一项
	ostringstream formatted, badNums;//每个循环步创建的对象
	for (const auto &nums : entry.phones) {//对每个数
		if (!valid(nums)) {
			badNums << " " << nums;//将数的字符串形式存入badNums
		}
		else {
			//将格式化的字符串“写入”formatted
			formatted << " " << format(nums);
		}
	}
		if (badNums.str().empty()) {//没有错误的数
			os << entry.name<< " "<<formatted.str() << endl;//打印名字和格式化的数
		}
		else {//否则,打印名字和错误的数
			cerr << "input error:" <<entry.name << " invalid number(s)" << badNums.str() <<endl;
		}
}
			

  在此函数中,我们假定已有两个函数,valid和format,分别完成了电话号码验证和改变格式的功能。最有趣的部分是对字符串流formatted和badNums的使用。我们使用标准的输出运算符(<<) 向这些对象写入数据,但这些“写入”操作实际上转换为string操作,分别向formatted和badNums中的string对象添加字符。

小结

在这里插入图片描述

习题解答

8.1.2节练习

  练习8.1 编写函数,接受一个istream&参数,返回类型也是istream&。此函数须从给定流中读取数据,直至遇到文件结束标识时停止。它将读取的数据打印在标准输出上。完成这些操作后,在返回流之前,对流进行复位,使其处于有效状态。
解答:

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

istream& f(istream& in){
	int v;
	while(in >> v, !in.eof()){
		//直到遇到文件结束符才停止读取
		if(in.bad())
			throw runtime_error("IO流错误");
		if(in.fail()){
			cerr << "数据错误,请重试:" << endl;
			in.clear();
			in.ignore(100,'\n');//刷新缓冲区(将输入最后的回车忽略掉) 
			continue; 
		}
		cout << v <<endl;	
	}
	in.clear();
	return in;
} 

int main(){
	cout << "请输入一些整数,按Ctrl+Z结束" << endl;
	f(cin);
	return 0; 
}

  练习8.2 测试函数,调用参数为cin。
解答:
在这里插入图片描述

  练习8.3 什么情况下,下面的while循环会终止?

while(cin  >> i)

解答:
  遇到文件结束符或者遇到IO流错误,或者读入了无效数据。

8.2.1节练习

  练习8.4 编写函数,以读模式打开一个文件,将其内容读入到一个string的vector中,将每一行作为一个独立的元素存于vector中。
解答:

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

int main(){
	ifstream in("data.txt");//打开文件
	if(!in){
		cerr << "无法打开输入文件" << endl;
		return -1; 
	} 
	string line;
	vector<string> words;
	while(getline(in,line)){//从文件中读取一行 
		words.push_back(line);//添加到vector中 
	}
	in.close();//输入完毕,关闭文件
	vector<string>::const_iterator it = words.begin();//迭代器,用auto也可
	while(it != words.end()){//遍历vector
		cout << *it << endl;
		++it; 
	} 
	return 0;
}

  练习8.5 重写上面的程序,将每个单词作为一个独立的元素进行存储。
解答:
  改为while(getline(in,line))改为while(in>>line)即可。

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

int main(){
	ifstream in("data.txt");//打开文件
	if(!in){
		cerr << "无法打开输入文件" << endl;
		return -1; 
	} 
	string line;
	vector<string> words;
	while(in>>line){//从文件中读取一行 
		words.push_back(line);//添加到vector中 
	}
	in.close();//输入完毕,关闭文件
	vector<string>::const_iterator it = words.begin();//迭代器,用auto也可
	while(it != words.end()){//遍历vector
		cout << *it << endl;
		++it; 
	} 
	return 0;
}

  练习8.6 重新7.1.1节程序,从一个文件中读取交易记录。将文件名作为一个参数传递给main。
解答:

#include <iostream>
#include <fstream>
#include "Sales_data.h"

using namespace std;

int main(int argc,char *argv[]){
	if(argc != 2){
		cerr << "请给出文件名" << endl;
		return -1;
	}
	ifstream in(argv[1]);
	if(!in){
		cerr << "无法打开输入文件" << endl;
		return -1; 
	}
	
	
	Sales_data total;//保存销售总额的变量
	if(read(input,total)){//读取第一条记录 
		Sales_data trans;//保存下一条记录的变量 
		while(read(input,total)){//读取剩余记录 
			if(total.isbn() == trans.isbn())//检查isbn 
				total.combine(trans);//更新销售记录 
			else{
				print(output,total) << endl;//打印处理结果 
				total = trans;//处理下一本书 
			}
		}
		print(output,total) << endl;//打印最后一本书的销售额 
	} 
	else
		cerr << "No data?!" <<endl;//文件中无输入数据 
	return 0;
}

8.2.2节练习

emsp; 练习8.7 修改上一节的书店程序,将结果保存到一个文件中。将输出文件名作为第二个参数传递给main函数。
解答:

#include <iostream>
#include <fstream>
#include <Sales_data.h>
using namespace std;

int main(int argc, char *argv[]) {
	if (argc != 3) {
		cerr << "请给出输入、输出文件名" << endl;
		return -1; 
	}
	ifstream in(argv[1]);
	if (!in) {
		cerr << "无法打开输入文件" << endl;
		return -1;
	}
	
	ofstream out[argv[2]];
	if (!out) {
		cerr << "无法打开输出文件" <<endl;
		return -1;
	}
	
	Sales_data total;
	if (read(in, total)) {
		Sales_data trans;
		while (read(in, trans)) {
			if (total.isbn() == trans.isbn())	total.combine(trans);
			else {
				print(out, total) << endl;
				total = trans;
			}
		}
		//输出最后一条交易
		print(out, total) << endl;
	}
	else {
		cerr << "没有数据" <<endl; 
	}
	return 0;
} 

emsp; 练习8.8 修改上题的程序,将结果追加到给定文件的末尾。对同一个文件,运行程序至少 两次,检验数据是否得以保存。
解答:
emsp; 只需将ofstream out (argv[2]);改为ofstream out(argv[2], ofstream::app);

8.3.1节练习

在这里插入图片描述
  练习8.9
解答:

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

istream& f(istream& in) {
	string v;
	while (in >> v, !in.eof()) {//直到遇到文件结束符才停止  
		if (in.bad()) throw runtime_error("IO流错误");
		if (in.fail()) {
			cerr << "数据错误,请重试:" << endl;
			in.clear();
			in.ignore(100, '\n');
			continue;
		} 
		cout << v << endl;
	}
	in.clear();
	return in;
}

int main() {
	ostringstream msg;
	msg << "C++ Primer  5th" << endl;
	istringstream in(msg.str());//istringstream对象 
	f(in);//打印对象的每个元素 
	return 0;
}

  练习8.10
解答:

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

int main() {
	ifstream in("data.txt");//打开文件
	if (!in) {
		cerr << "无法打开输入文件" << endl; 
		return -1;
	} 
	
	string line;
	vector<string> words;
	while (getline(in ,line)) {//向vector读入数据 
		words.push_back(line);
	}
	
	in.close() ;//输入完毕,关闭文件 
	
	vector<string>::const_iterator it = words.begin();//迭代器,遍历words的每一行 
	while (it != words.end()) {
		istringstream line_str(*it);//使用istringstream从vector读取数据元素,每次读取一个单词 
		string word;
		while (line_str >> word) {
			cout << word << " ";
		}
		cout << endl;
		++it; 
	}
	return 0;
}

  练习8.11
解答:

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

struct PersonInfo {
	string name;
	vector<string> phones;
};

int main(){
	string line, word;//分别保存来自输入的一行和单词 
	vector<PersonInfo> people;//保存来自输入的所有记录 
	istringstream record;//定义到while循环之外 
	
	while (getline(cin, line)) {
		PersonInfo info;//创建一个保存此记录数据的对象 
		record.clear();//重复使用字符串流时,每次都要调用clear 
		record.str(line); //将记录绑定到刚输入的行
		record >> info.name;//读取名字
		while (record >> word) {
			info.phones.push_back(word);//保持它们 
		} 
		people.push_back(info);//将此记录追加到people末尾 
	}
	//验证是否保持数据 
	for (vector<PersonInfo>::iterator it = people.begin(); it != people.end(); it++) {
		cout << (*it).name<< " ";
		for (vector<string>::iterator it1 = (*it).phones.begin(); it1 != (*it).phones.end(); it1++) {
			cout << (*it1) << " ";
		}
		cout << endl;
	}
	return 0; 
}

  练习8.12
解答:因为每个人的电话号码数量不固定,因此更好的方式不是通过类内初始人名和所有号码,而是在缺省初始化之后,在程序中设置人名并逐个添加电话号码

8.3.2节练习

  练习8.13 重写本节的电话号码程序,从一个命名文件而非cin读取数据。

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

struct PersonInfo {
	string name;
	vector<string> phones;
};

string format(const string &s) {
	return s;
}

bool valid(const string &s) {
	//如何验证电话号码将在第17章介绍,这里先简单返回true 
	return true;
}
int main(int argc, char *argv[]){
	string line, word;//分别保存来自输入的一行和单词 
	vector<PersonInfo> people;//保存来自输入的所有记录 
	istringstream record;//定义到while循环之外 
	
	if (argc != 2) {
		cerr << "请给出文件名" << endl;
		return -1; 
	}
	
	ifstream in(argv[1]);
	if (!in) {
		cerr << "无法打开输入文件" << endl;
		return -1; 
	}
	
	while (getline(in, line)) {
		PersonInfo info;//创建一个保存此记录数据的对象 
		record.clear();//重复使用字符串流时,每次都要调用clear 
		record.str(line); //将记录绑定到刚输入的行
		record >> info.name;//读取名字
		while (record >> word) {
			info.phones.push_back(word);//保持它们 
		} 
		people.push_back(info);//将此记录追加到people末尾 
	}

	ostringstream os;
	for (const auto &entry : people) {
		//对people中的每一项
		ostringstream formatted,badNums;//每个循环步创建的对象
		for (const auto &nums : entry.phones) {
			//对于每个数
			if (!valid(nums)) {
				badNums << " " << nums;//将数的字符串形式存入badNums 
			} 
			else {
				formatted << " " << format(nums);
			} 
		}
		if (badNums.str().empty()) {
			os << entry.name << " " << formatted.str() << endl;
		} 
		else {
			cerr << "input error:" << entry.name << " invalid number(s) " << badNums.str() << endl;
		}
	}
	cout << os.str() << endl;
	return 0;
}

  练习8.14 我们为什么将entry和nums定义为const auto&?
解答:这两条语句分别使用范围for语句枚举people中所有项(人)和每项的phones中的所有项(电话号码)。
  使用const表面在循环中不会改变这些项的值;
  auto是请求编译器依据vector元素类型来推断entry和nums的类型,即简化代码又避免出错
  使用引用是因为,people和phones的元素分别是结构对象和字符串对象,使用引用可以避免对象拷贝。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值