您的位置:首页 > 编程语言 > C语言/C++

《C++编程思想》第六章 输入输出流介绍(原书代码+习题+解答)

2015-07-30 11:16 633 查看
一.相关知识点

操纵算子

这里已经添加了一个新的元素:一个称作 endl的操纵算子。一个操纵算子作用于流上,这种情况下,插入一新行并清空流(消除所有存储在内部流缓冲区里的还没有输出的字符)。也可以只清空流:


cout<<flush;
另外有一个基本的操纵算子把基数变为 oct (八进制), dec (十进制)或hex (十六进制):


cout<<hex<<"0x"<<i<<endl;
有一个用于提取的操纵算子“跳过”空格:


cin>>ws;
还有一个叫 ends的操纵算子和 endl操纵算子一样,仅仅用于 strstreams。

面向行的输入

要获取一行输入,有两种选择:成员函数 get()或getline()。两个函数都有三个参数:指向存储结果字符的缓冲区指针、缓冲区大小(不能超过其限度)和知道什么时候停止读输入的终止符。终止符有一个经常用到的缺省值“\n”。两个函数遇到输入终止符时,都把零储存在结果缓冲区里。

其不同点是什么呢?差别虽小但极其重要:get()遇到输入流的分隔符时就停止,而不从输入流中提取分隔符。如果用同样的分隔符再调用一次 get()函数,它会立即返回而不带任何输入。(要么在下一个get()说明里用一个不同的分隔符,要么用一个不同的输入函数)。getline()与其相反,它从输入流中提取分隔符,但仍没有把它储存在结果缓冲区里。



出错处理

除没有参数表的 get()外。所有 get()和
getline()的版本都返回字符来源的输入流,没有参数表的get()返回下一个字符或 EOF。如果取回输入流对象,要询问一下它是否正确。事实上,我们可用成员函数 good()、 eof()、fail()和bad()询问任何输入输出流是否正确。这些返回状态信息基于 eofbit(指缓冲位于序列的末尾)、 failbit(指由于格式化问题或不影响缓冲区的其他问题而使操作失败)和 badbit(指缓冲区出错)。







这些标志可用一个“位或”(OR)运算来连接。



流定位( streampos)方法要求先调用“tell”函数:对一个输出流用 tellp()函数,对一个输入流用tellg()函数。( “ p”指“放指针”,“ g”指“取指针”)。要返回到流中的那个位置时,这个函数返回一个streampos,我们以后可以在用于输出流的
seekp()函数或用于输入流的seekg()函数的单参数版本里使用这个 streampos。

另一个方法是相对查找,使用 seekp()和seekg()的重载版本。第一个参数是要移动的字节数,它可以是正的或负的。第二个参数是查找方向:







为用户分配的存储

由用户负责分配存储空间,恰好是弄懂这个问题的最容易的途径。用istrstream,这是唯一允许的方法。下面是两个构造函数:






第一个构造函数取一个指向零终止符数组的指针;我们可以提取字节直到零为止。第二个构造函数另外还需要这个数组的大小,这个数组不必是零终止的。我们可以一直提取字节到buf[size],而不管是否遇到一个零。当移交数组地址给一个istrstream构造函数时,这个数组必须已经填充了我们要提取的并且假定格式化成某种其他数据类型的字符。


内部格式化数据


ios类(在头文件 IOSTREAM.H中可看到)包含数据成员以存储属于那个流的所有格式化数据。有些数据的值有一定范围并被储存在变量里:浮点精度、输出域宽度和用来填充输出(通常是一空格)的字符。格式化的其余部分是由标志所决定的,这些标志通常被连在一起以节省空间,并一起被指定为格式标志。可以用
ios::flags()成员函数发现格式化标志的值,这个成员函数没带参数并返回一个包含当前格式化标志的 long(typedefed to fmtflags)型值。函数的所有其余部分使格式化标志发生改变并返回格式化标志先前的值。






有时第一个函数迫使所有的标志改变。更多的是每次用剩下的三个函数来改变一个标志。setf()的用法看来更加令人混淆:要想知道用哪个重载版本,必须知道正要改变的是哪类标志。这里有两类标志:一类是简单的
on或off,一类是与其他标志在一个组里工作的标志。

on/off标志理解起来最简单,因为我们可用 setf(fmtflags)将它们变为on,用unsetf(fmtflags)将它们变为off。这些标志是:






例如,为cout显示加号,可写成cout.setf(ios::showpos);停止显示加号,可写成cout.unsetf(ios::showpos)。应该解释一下最后两个标志。当一个字符一旦被插进一个输出流,如果想确信它是一个输出时,可启用缓冲设备。也可以不用缓冲输出,但用缓冲设备会更好。有一个程序用了输入输出流和C标准I/O库(用C库不是不可能的),标志ios
::stdio就被采用。如果发现输入输出流的输出和printf()输出出现了错误的次序,就要设置这个标志。




格式域

第二类格式化标志在一个组里工作,一次只能用这些标志中的一种,就像旧式的汽车收音机按钮一样—按下一个按钮,其余的弹出。可惜的是,这是不能自动发生的,我们必须注意正在设置的是什么标志,这样就不会偶然调用错误的 setf()函数。例如,每一个数字基数有一个标志:十六进制,十进制和八进制。这些标志一起被指定为 ios::basefield。如果ios::dec标志被设置而调用setf(ios::hex),将设置ios::hex标志,但不会清除ios::dec位,结果出现未被定义的方式。适当的方法是像这样调用setf()的第二种形式:setf(ios::hex,ios::basefield)。这个

函数首先清除ios::basefield里的所有位,然后设置ios::hex。这样,setf()的这个形式保证无论什么时候设置一个标志,这个组里的其他标志都会“弹出”。当然,所有这些操作由hex()操纵算子自动完成。所以不必了解这个类实现的内部细节甚至不必关心它是一个二进制标志的设置。以后将会看到有一个与set()有提供同样的功能操纵算子。

下面是标志组和它们的作用:









域宽、填充字符和精度


一些内部变量,用于控制输出域的宽度,或当数据没有填入时,用作填充的字符,或控制打印浮点数的精度。它被与变量同名字的成员函数读和写。






填充和精度值是相当直观的,但宽度值需要一些解释。当宽度为0时,插入一个值将产生代表这个值所需字符的最小数。一个正值的宽度意味着插入一个值将产生至少与宽度一样多的字符。假如值小于字符宽度,填充字符用来填这个域。然而,这个值决不被截断。所以,如果打印
123而宽度为2,我们仍将得到123。域宽标识了字符的最小数目。没有标识字符最大数目的办法。宽度也是明显不同的,因为每个插入符或提取符可能受到它的值的影响,它被每个插入符或提取符重新设置为 0。它不是一个真正的静态变量,而是插入符和提取符的一个隐含参数。如我们想有一个恒定的宽度,得在每一个插入或提取它之后调用 width()。


格式化操纵算子

就像我们在前面的例子中看到的一样,调用成员函数有点乏味。为使读和写更容易, C++提供了一套操纵算子以起到与成员函数同样的作用。提供在IOSTREAM.H里的是不带参数的操纵算子。这些操纵算子包括 dec、oct和hex。它们各自更简明扼要地完成与 setf(ios::dec, ios::basefield)、setf(ios::oct, ios::basefield)和

setf(ios::hex, ios::basefield)同样的任务。 IOSTREAM.H [ 1 ] 还包括ws、endl、ends和flush以及如下所示的其他操纵算子:





带参数的操纵算子

如果正在使用带参数的操纵算子,必须也包含头文件 IOMANIP.H。这包含了解决建立带参数操纵算子所遇到的一般问题的代码。另外,它有六个预定义的操纵算子:





二.相关代码实现
1.
<span style="font-size:18px;"><strong><span style="font-size:18px;">#ifndef FILECLAS_H_
#define FILECLAS_H_
#include <stdio.h>
/*FILECLAS.h*/
class file
{
FILE* f;
public:
file(const char* fname, const char* mode = "r");
~file();
FILE* fp();
};
#endif

/*在C中执行文件I / O时,要用一个没有保护的指针指向文件结构。而这个类封装了这个指针,
并用构造函数和析构函数保证它能被正确地初始化和清除。第二个构造函数参数是文件模式,
其缺省值为“r”,代表“只读”*/</span></strong></span>
<span style="font-size:18px;"><strong><span style="font-size:18px;"><span style="font-size: 18px;">#include <stdlib.h>
#include "FILECLAS.h"
/*FILECLAS.cpp*/
file::file(const char* fname, const char* mode)
//如果结果是零意味着打开文件时出错。如果出错,这个文件名就被打印,而且函数 exit( )被调用
{
f = fopen(fname, mode);
if(f == NULL)
{
printf("%s: file not found\n",fname);
exit(1);
}
}

file::~file()
{
fclose(f);
}

</span><span style="font-size:18px;">FILE* file::fp()
{
return f;
}</span></span></strong></span>

<span style="font-size:18px;"><strong><span style="font-size:18px;"><span style="font-size:18px;">#include <assert.h>
#include "FILECLAS.h"
#define BSIZE 100
/*USEFILECLAS.cpp*/
int main(int argc, char* argv[])
{
assert(argc == 2);
file f(argv[1]);
char buf[BSIZE];
while(fgets(buf, BSIZE, f.fp()))
{
puts(buf);
}

return 0;
}</span></span></strong></span>

2.
<span style="font-size:18px;"><strong><span style="font-size:18px;">#ifndef FULLWRAP_H_
#define FULLWRAP_H_
#include <stdio.h>
/*FULLWRAP.h*/
class File
{
FILE* f;
FILE* F();
public:
File();
File(const char* path, const char* mode = "r");
~File();
int open(const char* path, const char* mode = "r");
int reopen(const char* path, const char* mode);
int Getc();
int Ungetc(int c);
int Putc(int c);
int puts(const char*s);
char* gets(char* s, int n);
int printf(const char* format,...);
size_t read(void* ptr,size_t size,size_t n);
size_t write(const void* ptr, size_t size, size_t n);
int eof();
int close();
int flush();
int seek(long offset, int whence);
int getpos(fpos_t* pos);
int setpos(const fpos_t* pos);
long tell();
void rewind();
void setbuf(char* buf);
int setvbuf(char* buf, int type, size_t sz);
int error();
void Clearerr();
};
#endif</span></strong></span>


3.
<span style="font-size:18px;"><strong><span style="font-size:18px;">/*IOSEXAMP.cpp*/
#include <iostream>
using namespace std;

int main()
{
int i;
cin>>i;

float f;
cin>>f;

char c;
cin>>c;

char buf[100];
gets(buf);

cout<<"i = "<<i<<endl;
cout<<"f = "<<f<<endl;
cout<<"c = "<<c<<endl;
cout<<"buf = "<<buf<<endl;

cout<<flush;
cout<<hex<<"0x"<<i<<endl;

return 0;
}
</span></strong></span>

4.

<span style="font-size:18px;"><strong><span style="font-size:18px;">/*STRFILE.cpp*/
#include <fstream.h>
#include <assert.h>
#define SZ 100

int main()
{
char buf[SZ];
{
/*在打开一个要读的文件之前,为确保文件是正当关闭的,有两种选择。可以用大括
号包住程序的第一部分以迫使out对象脱离范围,这样,调用析构函数并在这里关闭
这个文件。也可以为两个文件调用close(),如想这样做,可以调用open()成员函数
重用in对象*/
ifstream in("text.cpp");
assert(in);
ofstream out("text.out");
assert(out);
int i = 1;
/*第一个while循环表明get()函数的两种形式的用法。一是不论读到第 SZ-1个字符还是遇到
第三个参数(缺省值为“\ n”),get()函数把字符取进一个缓冲区内,并放入一个零终止符。
get()把终止符留在输入流内,这样,通过使用不带参数形式的get(),这个终止符必然通过
in.get()而被扔掉,这个 get()函数取回一个字节并使它作为一个 int类型返回。二是可以用
ignore()成员函数,它有两个缺省的参数,第一个参数是扔掉字符的数目,缺省值是 1,第二
参数表示ignore()函数退出处的那个字符(在提取后),缺省值是EOF。*/
while(in.get(buf, SZ))
{
in.get();
cout<<buf<<endl;          //输入回显行至标准输出
out<<i++<<": "<<buf<<endl;//写行至新文件并包括一个行数目
}
}
ifstream in("text.out");
assert(in);

while(in.getline(buf, SZ))
{
/*第二个while循环说明getline()如何从其遇到的输入流中移走终止符(它的第三个
参数缺省值是"\n")。然而getline()像get()一样,把零放进缓冲区,而且它同样不
能插入终止符。*/
char* cp = buf;
while(*cp != ':')
{
cp++;
}
cp += 2;
cout<<cp<<endl;
}

return 0;
}</span></strong></span>


5.

<span style="font-size:18px;"><strong><span style="font-size:18px;">/*STYPE.cpp*/
#include <fstream.h>
#include <assert.h>*/

/*在确信命令行有一个参数后,通过使用这个变数建立一个文件输入流 ifstream。如
果这个文件不存在,打开它时将会失败,这个失败被 assert(in)捕获。所有的工作实
际上在这个说明里完成:
cout<<in.rdbuf();
它把文件的整个内容送到 cout。这不仅比代码更简明扼要,也比在每次移动字节更加
有效。*/

int main(int argc, char* argv[])
{
assert(argc == 2);
ifstream in(argv[1]);
assert(in);
cout<<in.rdbuf();*///为了允许我们访问streambuf,每个流对象有一个叫做rdbuf
//()的成员函数,这个函数返回指向对象的streambuf的指针。
//为了提供通用接口给这些流并且仍然隐藏其基本的实现,它
//被抽像成自己的类,叫 streambuf。

return 0;
}</span></strong></span>


6.

<span style="font-size:18px;"><strong><span style="font-size:18px;">/*SBUFGET.cpp*/
/*使用带streambuf的get()函数
有一种get()形式允许直接向另一对象的streambuf写入。第一个参数是 streambuf的
目的地址(它的地址神秘地由一个引用携带)。第二个参数是终止符,它终止get()函
数。所以,打印一个文件到标准输出的另一方法是:*/

#include <fstream.h>

int main()
{
ifstream in("sbufget.cpp");
while(in.get(*cout.rdbuf()))
{
in.ignore();
}
/*rdbuf()返回一个指针,它必须逆向引用,以满足这个函数看到对象的需要。get()
函数不从输入流中拖出终止符,必须通过调用ignore()移走终止符。所以,get()永远
不会跳到新行上去。*/
return 0;
}</span></strong></span>


7.

<span style="font-size:18px;"><strong><span style="font-size:18px;">/*SEEKING.cpp*/
/*在文件中移动的例子,记住,不仅限于在文件里查找*/
#include <fstream.h>
#include <assert.h>

int main(int argc, char* argv[])
{
assert(argc == 2);
ifstream in(argv[1]);
assert(in);
in.seekg(0, ios::end);//end of file
streampos sp = in.tellg();//size of file
/*由于这是一种输入流,因此用seekg()来定位“取指针”,第一次调用从文件末
查找零字节,即到末端。由于streampos是一个long的typedef,那里调用tellg()
,返回被打印文件的大小。*/
cout<<"file size = "<<sp<<endl;
in.seekg(-sp/10, ios::end);
streampos sp2 = in.tellg();
in.seekg(0, ios::beg);//start of file
cout<<in.rdbuf();*///print of file
/*然后执行查找,移取指针至文件大小的 1/10处—注意那是文件尾的反向查找,
所以指针从尾部退回。如我们想进行从文件尾的正向查找,取指针刚好停在文件
尾。那里的streampos被读进sp2,然后,seekg()会到文件开始处执行,整个过程
可通过由rdbuf()产生的streampos指针打印出来。*/
in.seekg(sp2);//move to streampos
prints the last 1/10th of file
cout<<endl<<endl<<in.rdbuf()<<endl;
/*最后,seekg()的重载版与streampos sp2一起使用,移到先前的位置,文件的
最后部分被打印出来。*/

return 0;
}</span></strong></span>


8.

<span style="font-size:18px;"><strong><span style="font-size:18px;">/*IOFILE.cpp*/
/*建立读/写文件*/
/*建立具有基本流缓冲区的输出流(ostream):
ifsrream in("filename",ios::in|ios::out);
ostream out(in.rdbuf());
*/
#include <fstream.h>

int main()
{
ifstream in("text.cpp");
ofstream out("text.out");
out<<in.rdbuf();
in.close();
out.close();

ifstream in2("text.out", ios::in|ios::out);
ostream out2(in2.rdbuf());
cout<<in2.rdbuf();
out2<<"Where does this end up?";//结尾
out2.seekp(0, ios::beg);
out2<<"And what about this?";//开头
in2.seekg(0, ios::beg);
cout<<in2.rdbuf();

return 0;
}
/*前五行把这个程序的源代码拷贝进一个名叫text.out的文件,然后关闭这个文件。
这给了我们一个可在其周围操作的安全的文本文件。那么前面提及的技术被用来建立
两个对象,这两个对象向同一个文件读和写。在cout<<in2.rdbuf()里,可看到“取”
指针在文件的开始被初始化。“放”指针被放到文件的末尾,这是由于“Where does
this end up?”追加到这个文件里。然而,如果“放”指针移到 seekp()的开始处,
所有插入的文本覆盖现成的文本。当“取”指针用seekg()移回到开始处时,两次写结
果均可见到,而且文件被打印出来。当然,当 out2脱离范围时,析构函数被调用,这
个文件被自动保存和关闭。*/
</span></strong></span>


9.

<span style="font-size:18px;"><strong><span style="font-size:18px;">/*ISTRING.cpp*/
#include <strstrea.h>

int main()
{
istrstream s("1.414 47 This is a test");
int i;
float f;
s >> i >> f;
char buf2[100];
s >> buf2;
cout<<"i = "<<i<<",f = "<<f<<endl;
cout<<"buf2 = "<<buf2<<endl;
cout<<s.rdbuf();

return 0;
}</span></strong></span>


10.

输出strstream也允许我们提供自己的存储空间。在这种情况下,字节在内存中被格式化。相应的构造函数是:



ostrstream::ostrstream(char*, int, int=ios::out);
第一个参数是预分配的缓冲区,在那里字符将结束,第二个参数是缓冲区的大小,第三个参数是模式。如果模式是缺省值,字符从缓冲区的开始地址格式化。如果模式是 ios::ate或ios::app(效果一样),字符缓冲区被假定已经包含了一个零终止字符串,而任何新的字符只能从零终止符开始添加。

第二个构造函数参数表示数组大小,且被对象用来保证它不覆盖数组尾。如我们已填满数组而又想添加更多的字节,这些字节是加不进去的。

关于ostrstream,记住重要的是:没有为我们插入一般在字符数组末尾所需要的零终止符.当我们准备好零终止符时,用特别操纵算子ends.一旦已建立一个ostrstream,就可以插入我们需要插入的任何东西,而且它将在内存缓冲区里完成格式化。



<span style="font-size:18px;"><strong><span style="font-size:18px;">/*OSTRING.cpp*/
#include <strstrea.h>
#define SZ 100

int main()
{
cout<<"type an int, a float and a string:";
int i;
float f;
cin>>i>>f;
cin>>ws;//throw away white space,用于提取的操纵算子“跳过”空格
char buf[SZ];
cin.getline(buf, SZ);//get rest of the line
ostrstream os(buf, SZ, ios::app);
os<<endl;
os<<"integer = "<<i<<endl;
os<<"float = "<<f<<endl;
os<<ends;
cout<<buf;
cout<<os.rdbuf();//same effect
cout<<os.rdbuf();//not same effect

return 0;
}
/*使用getline()所表明的方法,一直取输入直到用户按下回车才停下来。这个输入被
取进 buf, buf用来构造ostrstream os 。如果未提供第三个参数ios::app,构造函
数缺省地写在buf的开头,覆盖刚被收集的行。然而,“追加”标志使它把被格式化后
的信息放在这个串的末尾。像其他的输出流一样,可以用平常的格式化工具发送字节
到 ostrstream。区别是仅用ends在末尾插入零。注意,endl是在流中插入一个新行,
而不是插入零。现在信息在buf里格式化,可用cout<<buf直接发送它。然而,也有可
能用os.rdbuf()发送它。当我们这样做的时候,在streambuf里的“取”指针随这字符
被输出而向前移动。正因如此,第二次用到cout<<os.rdbuf()时,什么也没有发生—
“取”指针已经在末端。*/
</span></strong></span>


11.

<span style="font-size:18px;"><strong><span style="font-size:18px;">/*  为了不让内存泄漏,必须清理存储器。有两种清理办法。较普通的办法是直接释放
要处理的内存。为搞懂这个问题,我们得预习一下 C++中两个新的关键字: new和
delete。目前可认为它们是用来替代 C中malloc()和free()的。
操作符new返回一个存储块,而delete释放它。重要的是必须知道它们,因为实际
上 C++中所有内存分配是由new完成的。 ostrstream也是这样的。如果内存是由new分
配的,它必须由delete释放。所以,如果有一个ostrstream A,用str()取得char*,
清理存储器的办法是:
delete A.str();
这能满足大部分需要,但还有另一个不是很普通的释放存储器的办法:解冻
ostrstream,可通过调用freeze()来做。freeze()是 ostrstream的streambuf成员函数
。freeze有一个缺省参数,这个缺省参数冻结这个流。用零参数对它解冻:
A.rdbuf()- >freeze(0);
当A脱离作用域时,存储被重新分配,而且它的析构函数被调用。另外,可添加更
多的字节给A。但是这可能引起存储移动,所以最好不要用以前通过调用str()得到的
指针—在添加更多的字符后,这个指针将不可靠。*/
/*WALRUS.cpp*/
/*流被解冻后追加字符的能力*/

#include <strstrea.h>
int main()
{
ostrstream s;
s << "'The time has com', the walrus said,";
s << ends;
//在放第一个串到s后,添加一个ends,所以这个串能用由str()产生的char*打印出来。
cout << s.str() << endl;//string is frozen
//s is frozen;destructor won't delete
//the streambuf storage on the heap
s.seekp(-1, ios::cur);//back up before NULL
//为了添更多的字节给 s,"放"指针必须后移一步,这样下一个字符被
//放到由ends插入的零的上面。
s.rdbuf()->freeze(0);//unfreeze it
//Now destructor releases memory,and
//you can add more characters(but you
//better not use the previous str()value)
//然后通过使用rdbuf()和调用freeze(0)取回基本streambuf指针,s被解冻
s << "'To speak of many things'" <<ends;
//我们可以添加更多的字符,清理由析构函数自动完成
cout << s.rdbuf();
//将整个内容发送到cout
cout << endl;

return 0;
}</span></strong></span>


12.

<span style="font-size:18px;"><strong><span style="font-size:18px;">/*检验移动
如果我们仍不相信调用str()就得对ostrstream的存储空间负责,下面的例子说
明存储定位被移动了,因而由str()返回的旧指针是无效的:*/
/*STRMOVE.cpp*/

/*在插入一个串到s中并用str()捕获char*后,这个串被解冻而且有足够的新字节被插
入,真正确保了内存被重新分配且大多数被移动。在打印出旧的和新的char*值后,存
储明确地由delete释放,因为第二次调用str()又冻结了这个串。
为了打印出它们指向的串的地址而不是这个串,必须把char*指派为void*。char*的操
作符“<<”打印出它正指向的串,而对应于void*的操作符“<<”打印出指针的十六进
制表示值。有趣的是应注意到:在调用str()前,如不插一个串到s中,结果则为0。这
意味着直到第一次插入字节到ostrstream时,存储才被重新分配。*/

#include <strstrea.h>

int main()
{
ostrstream s;
s << "hi";
char* old = s.str();//freezes s
s.rdbuf()->freeze(0);//unfreeze
for(int i = 0; i < 100; ++i)
{
s << "howdy";//should force reallocation
}
cout << "old = " << (void*)old << endl;
cout << "new = " << (void*)s.str();//freeze
cout << endl;
delete s.str();//release storage

return 0;
}</span></strong></span>


13.

<span style="font-size:18px;"><strong><span style="font-size:18px;">/*FORMAT.cpp*/
#include <fstream.h>
#define D(a) T << #a <<endl; a
ofstream T("format.out");

int main()
{
D(int i = 47;)
D(float f = 2300114.414159;)
char* s = "Is there any more";

D(T.setf(ios::unitbuf);)
D(T.setf(ios::stdio);)

D(T.setf(ios::showbase);)
D(T.setf(ios::uppercase);)
D(T.setf(ios::showpos);)
D(T << i << endl;)
D(T.setf(ios::hex, ios::basefield);)
D(T << i << endl;)
D(T.unsetf(ios::uppercase);)
D(T.setf(ios::oct, ios::basefield);)
D(T << i << endl;)
D(T.unsetf(ios::showbase);)
D(T.setf(ios::dec, ios::basefield );)
D(T.setf(ios::left, ios::adjustfield);)
D(T.fill('0');)
D(T << "fill char: " << T.fill() <<endl;)
D(T.width(10);)
T << i << endl;
D(T.setf(ios::right, ios::adjustfield);)
D(T.width(10);)
T << i << endl;
D(T.setf(ios::internal, ios::adjustfield);)
D(T.width(10);)
T << i << endl;
D(T << i << endl;)

D(T.unsetf(ios::showpos);)
D(T.setf(ios::showpoint);)
D(T << "prec = "<< T.precision() << endl;)
D(T.setf(ios::scientific, ios::floatfield);)
D(T << f << endl;)
D(T.setf(0, ios::floatfield);)
D(T << f << endl;)
D(T.precision(20);)
D(T << "prec = " << T.precision() << endl;)
D(T << endl << f << endl;)
D(T.setf(ios::scientific, ios::floatfield);)
D(T << f <<endl;)
D(T.setf(0, ios::floatfield);)
D(T << f << endl;)

D(T.width(10);)
T << s << endl;
D(T.width(10);)
T << s <<endl;
D(T.setf(ios::left, ios::adjustfield);)
D(T.width(40);)
T << s << endl;

D(T.unsetf(ios::showpoint);)
D(T.unsetf(ios::unitbuf);)
D(T.unsetf(ios::stdio);)
}</span></strong></span>


14.

<span style="font-size:18px;"><strong><span style="font-size:18px;">/*MANIFS.cpp*/
#include <fstream.h>
#include <iomanip.h>

//许多的多重语句已被精简成单个的链插入.注意调用setiosflags()和resetiosflags(),
//在这两个函数里,标志被按“位OR”运算成一个。在前面的例子里,这个工作是由
//setf()和unsetf()完成的。

int main()
{
ofstream T("trace.out");
int i = 47;
float f = 2300114.414159;
char* s = "Is there any more?";

T << setiosflags(
ios::unitbuf | ios::stdio
| ios::showbase | ios::uppercase
| ios::showpos
);
T << i << endl;
T << hex << i << endl;
T << resetiosflags(ios::uppercase)
<< oct << i << endl;
T.setf(ios::left, ios::adjustfield);
T << resetiosflags(ios::showbase)
<< dec << setfill('0');
T << "fill char: "<< T.fill() << endl;
T << setw(10) << i << endl;
T.setf(ios::right, ios::adjustfield);
T << setw(10) << i << endl;
T.setf(ios::internal, ios::adjustfield);
T << setw(10) << i << endl;
T << i << endl;

T << resetiosflags(ios::showpos)
<< setiosflags(ios::showpoint)
<< "prec = " << T.precision() << endl;
T.setf(ios::scientific, ios::floatfield);
T << f << endl;
T.setf(ios::fixed, ios::floatfield);
T << f << endl;
T.setf(0, ios::floatfield);
T << f << endl;
T << setprecision(20);
T << "prec = " << T.precision() << endl;
T << f << endl;
T.setf(ios::scientific, ios::floatfield);
T << f << endl;
T.setf(ios::fixed, ios::floatfield);
T << f << endl;
T.setf(0, ios::floatfield);
T << f << endl;

T << setw(10) << s << endl;
T << setw(40) << s << endl;
T.setf(ios::left, ios::adjustfield);
T << setw(40) << s << endl;

T << resetiosflags(
ios::showpoint | ios::unitbuf
| ios::stdio
);

return 0;
}</span></strong></span>


15.
<span style="font-size:18px;"><strong><span style="font-size:18px;">/*NL.cpp*/
/*下面是建立一个操纵算子的例子,这个操纵算子叫 nl,它产生一个换行而不刷新这
个流:*/
#include <iostream.h>

ostream& nl(ostream& os)
{
return os << '\n';
}

int main()
{
cout << "newlines" << nl << "between" << nl
<< "each" << nl << "word" << nl;
}</span></strong></span>

16.
<span style="font-size:18px;"><strong><span style="font-size:18px;">/*EFFECTOR.cpp*/
/*一个效用算子是一个简单的类,这个类的构造函数与工作在这个类里的一个重载操
作符“<<”一起执行想要的操作。下面是一个有两个效用算子的例子。第一个输出是
一个被截断的字符串,第二个打印出一个二进制数*/
#include <iostream.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <limits.h>//ULONG_MAX

class fixw
//fixw的构造函数产生char*参数的一个缩短的副本,析构函数释放产生这个副本的内
//存。重载操作符“<<”取第二个参数的内容,即 fixw对象,并把它插入第一个参数
//ostream,然后返回ostream,所以它可以用在一个链接表达式里。下面的表达式里
//用 fixw时:
//              cout <<fixw(string,i)<<endl;
//一个临时对象通过调用 fixw构造函数被建立了,那个临时对象被传送给操作符
//“<<”。带参数的操纵算子产生作用了。
{
char* s;
public:
fixw(const char* S, int width);
~fixw();
friend ostream& operator<<(ostream&, fixw&);
};

fixw::fixw(const char* S, int width)
{
s = (char*)malloc(width + 1);
assert(s);
strncpy(s, S, width);
s[width] = 0;
}

fixw::~fixw()
{
free(s);
}

ostream& operator<<(ostream& os, fixw& fw)
{
return os << fw.s;
}

typedef unsigned long ulong;

class bin//bin效用算子依赖于这样的事实,对一个无符号数向右移位把零移成高位。
{
ulong n;
public:
bin(ulong N);
friend ostream& operator<<(ostream&, bin&);
};

bin::bin(ulong N)
{
n = N;
}

ostream& operator<<(ostream& os, bin& b)
{
ulong bit = ~(ULONG_MAX >> 1);//ULONG_MAX(最大的长型值unsigned long,
//来自标准包含文件 LIMITS.H)用来产生一个带高位设置的值,这个值移过正被
//讨论的数(通过移位),屏蔽每一位。
while(bit)
{
os << (b.n & bit ? '1':'0');
bit >>= 1;
}
return os;
}

int main()
{
char* string =
"Things that make us happy, make us wise";
for(int i = 0; i <= strlen(string); ++i)
{
cout << fixw(string, i) << endl;
}
ulong x = 0xFEDCBA98UL;
ulong y = 0x76543210UL;
cout << "x in binary: "<< bin(x) << endl;
cout << "y in binary: "<< bin(y) << endl;

return 0;
}</span></strong></span>


17.

<span style="font-size:18px;"><strong><span style="font-size:18px;">/*MAKEMAIN.cpp*/
#include <fstream.h>
#include <strstrea.h>
#include <assert.h>
#include <string.h>
#include <ctype.h>

int main(int argc, char* argv[])
{
assert(argc == 2);
ofstream mainfile(argv[1], ios::noreplace);
//这个文件被打开,使用 ios::noreplace,以保证不会偶然地覆盖一个现成文件。
assert(mainfile);
istrstream name(argv[1]);
ostrstream CAPname;
char c;
while(name. get(c))
//有了标准 C库宏toupper(),这些字符可被转变成大写字母。这个变量返
//回一个 int,这样,它必须很明确地转换给char。
{
CAPname << char(toupper(c));
}
CAPname << ends;
mainfile<< "//:" << ' ' << CAPname.rdbuf()
<< " -- " << endl
<< "#include <iostream.h>" << endl
<< endl
<< "main() {" << endl << endl
<< "}" << endl;

return 0;
}</span></strong></span>


18.

<span style="font-size:18px;"><strong><span style="font-size:18px;">/*维护类库资源*/
/*CPPCHECK.cpp*/
/*这个例子需要不同缓冲区中的串格式化。不是建立单个命名的缓冲区和 ostrstream对象,
而是在enum bufs缓冲区中建立 一组名字。因而要建立两个数组:一个字符缓冲区数组和一
个从字符缓冲区里建立的ostrstream对象数组。注意在char缓冲区b的二维数组定义里,
char数组的数目是由bufnum决定的,bufnum是bufs中的最后一个枚举常量。当建立一个枚举
变量时,编译器赋一个整数值给所有那些标明从零开始的 enum,所以bufnum的唯一目的是成
为统计 buf中枚举常量数目的计数器。 b中每一个串的长度是SZ。
枚举变量里的名字是:base,即大写字母的不带扩展名的基文件名; header,即头文件
名; implement,即实现文件名(CPP); Hline1,即头文件第一行框架; guard1 、guard2和
guard3,即头文件里的“保护”行(阻止多重包含); CPPlinel,即C P P文件的第一行框架;
include,即包含头文件的CPP文件的行。
osarray是一个通过集合初始化和自动计数建立起来的 ostrstream对象数组。当然,这是带
两个参数(缓冲区地址和大小)形式的 ostrstream构造函数,所以构造函数调用必须相应地建
立在集合初始化表里。利用 bufs枚举常量,b的适当数组元素结合到相应的 osarray对象。一旦
数组被建立,利用枚举常量就可选择数组里的对象,作用是填充相应的 b元素。我们可看到,
每个串是怎样建立在ostrstream数组定义后面的行里的。
一旦串被建立,程序试图打开头文件和 CPP文件的现行版本作为 ifstreams。如果用操作符
“!”测试对象而这个文件不存在,测试将失败。如果头文件或实现文件不存在,利用以前建
立的文本的适当的行来建立它。
如果文件确实存在,那么这些文件后面跟着适当的格式,这是有保证的。在这两种情况下,
一个strstream被建立而且整个文件被读进;然后第一行被读出并被检查,看看这一行是否包含
一个“//:”和文件名以确信它跟在格式的后面。这是由标准 C库函数strstr()完成的。如果
第一行不符合,较早时建立的内容被插进一个已建好的 ostrstream中,目的是保存被编辑的那
个文件。
在头文件里,整个文件被搜索(再次使用 strstr()函数)以确保它包含三个“保护”行;如
果没有包含这三行,要插入它们。检查实现文件,看看包含头文件那一行是否存在(虽然编译
器有效地保证它的存在)。
在两种情况下,比较原始文件(在它的strstream里)和被编辑的文件(在 ostrstream里),
看看它们是否有变化。如果有变化,现存文件被关闭,一个新的 ofstream对象被建立,目的是
覆盖这个现存文件。一个特别的变化标志被添加到开始处之后, ostrstream被输出到这个文件,
所以可使用一文本搜索程序,通过检查快速发现产生另外变化的文件。*/

#include <fstream.h>
#include <strstrea.h>
#include <string.h>
#include <ctype.h>
#include <assert.h>
#define SZ 40
#define BSZ 100

int main(int argc, char* argv[])
{
assert(argc == 2);
enum bufs{base, header, implement,
Hline1, guard1, guard2, guard3,
CPPline1, include, bufnum};
char b[bufnum][SZ];
ostrstream osarray[] =
{
ostrstream(b[base], SZ),
ostrstream(b[header], SZ),
ostrstream(b[implement], SZ),
ostrstream(b[Hline1], SZ),
ostrstream(b[guard1], SZ),
ostrstream(b[guard2], SZ),
ostrstream(b[guard3], SZ),
ostrstream(b[CPPline1], SZ),
ostrstream(b[include], SZ),
};
osarray[base] << argv[1] << ends;
char* period = strchr(b[base], '.');
if(period)
{
*period = 0;
}
for(int i = 0; b[base][i]; ++i)
{
b[base][i] = toupper(b[base][i]);
}
osarray[header] << b[base] << ".H" << ends;
osarray[implement] << b[base] << ".CPP" << ends;
osarray[Hline1] << "//:" << ' ' << b[header]
<< " -- " << ends;
osarray[guard1] << "#ifndef " << b[base]
<< "_H_" << ends;
osarray[guard2] << "#ifndef " << b[base]
<< "_H_" << ends;
osarray[guard3] << "#ifndef " << b[base]
<< "_H_" << ends;
osarray[CPPline1] << "//:" << ' '
<< b[implement]
<< " -- " << ends;
osarray[include] << "#include \""
<< b[header] << "\"" << ends;
ifstream existh(b[header]),
existcpp(b[implement]);
if(!existh)
{
ofstream newheader(b[header]);
newheader << b[Hline1] << endl
<< b[guard1] << endl
<< b[guard2] << endl << endl
<< b[guard3] << endl;
}
if(!existcpp)
{
ofstream newcpp(implement);
newcpp << b[CPPline1] << endl
<< b[include] << endl;
}
if(existh)
{
strstream hfile;
ostrstream newheader;
hfile << existh.rdbuf() << ends;
char buf[BSZ];
if(hfile.getline(buf, BSZ))
{
if(!strstr(buf, "//:") ||
!strstr(buf, b[header]))
{
newheader << b[Hline1] << endl;
}
}
if(!strstr(hfile.str(), b[guard1]) ||
!strstr(hfile.str(), b[guard2]) ||
!strstr(hfile.str(), b[guard3]))
{
newheader << b[guard1] << endl
<< b[guard2] << endl
<< buf
<< hfile.rdbuf() << endl
<< b[guard3] << endl << ends;
}
else
{
newheader << buf
<< hfile.rdbuf() <<ends;
}
if(strcmp(hfile.str(), newheader.str()) != 0)
{
existh.close();
ofstream newH(b[header]);
newH << "//@//" << endl
<< newheader.rdbuf();
}
delete hfile.str();
delete newheader.str();
}
if(existcpp)
{
strstream cppfile;
ostrstream newcpp;
cppfile << existcpp.rdbuf() << ends;
char buf[BSZ];
if(cppfile.getline(buf, BSZ))
{
if(!strstr(buf, "//:") ||
!strstr(buf, b[implement]))
{
newcpp << b[CPPline1] << endl;
}
newcpp << buf << endl;
newcpp << cppfile.rdbuf() << ends;
if(strcmp(cppfile.str(), newcpp.str()) != 0)
{
existcpp.close();
ofstream newCPP(b[implement]);
newCPP << "//@//" << endl
<< newcpp.rdbuf();
}
delete cppfile.str();
delete newcpp.str();
}
}

return 0;
}</span></strong></span>


19.

<span style="font-size:18px;"><strong><span style="font-size:18px;">/*SHOWERR.cpp*/
#include <fstream.h>
#include <strstrea.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <assert.h>
//这个maker指针可被我们的一种选择所代替。
//每个文件每次读出一行,在每一行中搜索出现在行开头的 maker。这个行被修改并放进错
//误行表、放进strstream edited里。当整个文件被处理完时,它被关闭(通过到达范围的末端),
//重新作为一个输出文件打开, edited被灌进这个文件。注意计数器被保存在内部文件里。所以
//下一次调用这个程序时,继续由计数器按顺序计数。

char* marker = "//!";//任何产生编译错误的代码行都由特
//别注释顺序符“//!”注解出来。

char* usage =
"usage: showerr filename chapnum\n"
"where filename is a C++ source file\n"
"and chapnum is the chapter name it's in.\n"
"Finds lines commented with //! and removes\n"
"comment, appending //(#) where # is unique\n"
"across all files, so you an determine\n"
"if your compiler finds the error.\n"
"showerr /r\n"
"resets the unique counter.";

char* errnum = "..\\errnum.txt";
char* errfile = "..\\errlines.txt";
ofstream errlines(errfile, ios::app);

int main(int argc, char* argv[])
{
if(argc < 2)
{
cerr << usage << endl;
return 1;
}
if(argv[1][0] == '/' || argv[1][0] == '-')
{
switch(argv[1][1])
{
case 'r':
case 'R':
cout << "reset counter" << endl;
remove(errnum);
remove(errfile);
return 0;
default:
cerr << usage << endl;
return 1;
}
}
char* chapter = argv[2];
strstream edited;
int counter = 0;
{
ifstream infile(argv[1]);
assert(infile);
ifstream count(errnum);
if(count)
{
count >> counter;
}
int linecount = 0;
#define sz 255
char buf[sz];
while(infile.getline(buf, sz))
{
linecount++;
int i = 0;
while(isspace(buf[i]))
{
i++;
}
if(strstr(&buf[i], marker) == &buf[i])
{
memset(&buf[i], ' ', strlen(marker));
ostrstream out(buf, sz, ios::ate);
out << "//(" << ++counter << ") "
<< "Chapter " << chapter
<< "File: " << argv[1]
<< "Line " << linecount << endl
<< ends;
edited << buf;
errlines << buf;
}
else
{
edited << buf << '\n';
}
}
}
ofstream outfile(argv[1]);
outfile << edited.rdbuf();
ofstream count(errnum);
count << counter;

return 0;
}</span></strong></span>


20.

<span style="font-size:18px;"><strong><span style="font-size:18px;">/*显示了记录数据到磁盘上而后检索数据并作处理的一种途径。这个例子的意思是
产生一个海洋各处温度—深度的曲线图。为保存数据,要用到一个类:*/
/*DATALOG.h*/

#ifndef DATALOG_H_
#define DATALOG_H_
#include <time.h>
#include <iostream.h>
/*这个存取函数为每个数据成员提供受控制的读和写。 printf()函数用一个可读的形式格式化
datapoint到一个ostream对象中(printf()的参数),*/
#define BSZ 10

class datapoint
{
tm Tm;//Time & day
//ASCII degrees(*) minutes(') seconds("):
char Latitude[BSZ], Longitude[BSZ];
double Depth, Temperature;
public:
tm Time();//read the time
void Time(tm T);//set the time
const char* latitude();//read
void latitude(const char* l);//set
const char* longitude();//read
void longitude(const char* l);//set
double depth();//read
void depth(double d);//set
double temperature();//read
void temperature(double t);//set
void print(ostream& os);
};
#endif</span></strong></span>
<span style="font-size:18px;"><strong><span style="font-size:18px;">/*DATALOG.cpp*/
#include "DATALOG.h"
#include <iomanip.h>
#include <string.h>

tm datapoint::Time()
{
return Tm;
}

void datapoint::Time(tm T)
{
Tm = T;
}

const char* datapoint::latitude()
{
return Latitude;
}

void datapoint::latitude(const char* l)
{
Latitude[BSZ - 1] = 0;
strncpy(Latitude, l, BSZ - 1);
}

const char* datapoint::longitude()
{
return Longitude;
}

void datapoint::longitude(const char* l)
{
Longitude[BSZ - 1] = 0;
strncpy(Longitude, l, BSZ - 1);
}

double datapoint::depth()
{
return Depth;
}

void datapoint::depth(double d)
{
Depth = d;
}

double datapoint::temperature()
{
return Temperature;
}

void datapoint::temperature(double t)
{
Temperature = t;
}

void datapoint::print(ostream& os)
{
os.setf(ios::fixed, ios::floatfield);
os.precision(4);
os.fill('0');//pad on left with '0'
os << setw(2) << Time().tm_mon << '\\'
<< setw(2) << Time().tm_mday << '\\'
<< setw(2) << Time().tm_year << ' '
<< setw(2) << Time().tm_hour << ':'
<< setw(2) << Time().tm_min << ':'
<< setw(2) << Time().tm_sec;
os.fill(' ');
os << "Lat:" << setw(9) << latitude()
<< ",Long:" << setw(9) << longitude()
<< ",depth:" << setw(9) << depth()
<< ",temp:" << setw(9) << temperature()
<< endl;
}</span></strong></span>
<span style="font-size:18px;"><strong><span style="font-size:18px;">/*下面是一个程序,这个程序(用write())建立一个二进制形式的测试数据文件 ,并且用
datapoint::print()建立另一个ASCⅡ形式的文件。我们也可以把它打印到屏幕上,但是在文件形
式里更容易检测:*/
/*USEDATALOG.cpp*/

#include <fstream.h>
#include <stdlib.h>
#include <string.h>
#include "DATALOG.h"

int main()
{
ofstream data("data.txt");
ofstream bindata("data.bin", ios::binary);
time_t timer = time(NULL);//get time
//seed random generator
srand((unsigned)timer);
for(int i = 0; i < 100; ++i)
{
datapoint d;
d.Time(*localtime(&timer));
timer += 55;
d.latitude("45*20'31\"");
d.longitude("22*34'18\"");
//0 to 199 meters
double newdepth = rand() % 200;
double fraction = rand() % 100 + 1;
newdepth += double(1) / fraction;
d.depth(newdepth);
double newtemp = 150 + rand() % 200;
fraction = rand() % 100 + 1;
newtemp += (double)1 /fraction;
d.temperature(newtemp);
d.print(data);
bindata.write((unsigned char*)&d,
sizeof(d));
}

return 0;
} </span></strong></span>
<span style="font-size:18px;"><strong><span style="font-size:18px;">/*DATASCAN.cpp*/
/*为检查以二进制格式存储的数据的有效性,数据从盘中读出并被放进文本文件
DATA2.TXT中,所以这个文件可与 DATA.TXT比较以供检验。在下面的程序里,我们会看到
这个数据恢复是多么简单。在测试文件被建立后,记录在用户命令上被读出。*/
#include <fstream.h>
#include <strstrea.h>
#include <iomanip.h>
#include <assert.h>
#include "DATALOG.h"

int main()
{
ifstream bindata("data.bin", ios::binary);
assert(bindata);
ofstream verify("data2.txt");
datapoint d;
while(bindata.read((unsigned char*)&d,
sizeof d))
{
d.print(verify);
}
bindata.clear();//reset state to "good"
int recnum = 0;
cout.setf(ios::left, ios::adjustfield);
cout.setf(ios::fixed, ios::floatfield);
cout.precision(4);
for(;;)
{
bindata.seekg(recnum* sizeof d, ios::beg);
cout << "record " << recnum << endl;
if(bindata.read((unsigned char*)&d,
sizeof d))
{
cout << asctime(&(d.Time()));
cout << setw(11) << "Latitude"
<< setw(11) << "Longitude"
<< setw(11) << "Depth"
<< setw(11) << "Temperature"
<< endl;
cout << setfill('-') << setw(43) << '-'
<< setfill(' ') << endl;
cout << setw(11) << d.latitude()
<< setw(11) << d.longitude()
<< setw(10) << d.depth()
<< setw(12) << d.temperature()
<< endl;
}
else
{
cout << "invalid record number" << endl;
bindata.clear();
}
cout << endl
<< "enter record number, x to quit:";
char buf[10];
cin.getline(buf, 10);
if(buf[0] == 'x')
{
break;
}
istrstream input(buf, 10);
input >> recnum;
}

return 0;
}</span></strong></span>

三.习题+解答

1) 通过创建一个叫in 的ifstream对象来打开一个文件。创建一个叫 os的ostrstream对象,并通过rdbuf()成员函数把整个内容读进 ostrstream。用str()函数取出os的char*地址,并利用标准Ctoupper() 宏使文件里每个字符大写。把结果写到一新的文件中,并删除由 os分配的内存。

<span style="font-size:18px;"><strong>#include <strstrea.h>
#include <fstream.h>
#include <ctype.h>

int main()
{
ifstream in("Pricase.cpp");
ostrstream os;
os << in.rdbuf();
char* str = os.str();
while(*str++ != NULL)
{
*str = toupper(*str);
}
ofstream out("new.cpp");
out << os.rdbuf();
delete os.str();

return 0;
}</strong></span>


2) 创建一个能打开文件(命令行中的第一个参数)的程序,并从中搜索一组字中的任何一个(命令行中其余的参数)。每次,读入一行输入并打印出与之匹配的行(带行数)。


<span style="font-size:18px;"><strong><span style="font-size:18px;">#include <fstream>
#include <iostream>
#include <assert.h>
#include <string>
using namespace std;

int main(int argc, char* argv[])
{
ifstream in("Pricase.cpp");
//ifstream in(argv[1]);
assert(in);
for(int i = 0; i < argc; ++i)
{
cout<<i<<" = "<<argv[i]<<endl;
}
string line;
int k = 0;
while(getline(in, line))
{
cout << ++k <<" = "<< line << endl;
}

return 0;
}</span></strong></span>




3) 写一个在所有源代码文件的开始处添加版权注意事项的程序。只需对练习 1)稍作修改。


<span style="font-size:18px;"><strong>/********************************************************************
Copyright(c) 2015-2020 Companyname.
All rights reserved.

filename: 	E:\VC++\7_30\Pricase.cpp
file path:	E:\VC++\7_30
file base:	Pricase
file ext:	cpp

current version:     1.0
author:		ZY
created:	2015/07/30
created:	30:7:2015   11:13

Replace version:
author:
created:

purpose:	exercise
*********************************************************************/

#include <strstrea.h>
#include <fstream.h>
#include <ctype.h>

int main()
{
ifstream in("Pricase.cpp");
ostrstream os;
os << in.rdbuf();
char* str = os.str();
while(*str++ != NULL)
{
*str = toupper(*str);
}
ofstream out("new.cpp");
out << os.rdbuf();
delete os.str();

return 0;
}</strong></span>


4) 用你最喜爱的文本搜索程序(如grep) 输出包含一特殊模式的所有文件名字(仅是名字)。重定向输出到一个文件中。写一个用那个文件里的内容产生批处理文件的程序,这个批处理文件对每个由这个搜索程序找到的文件调用你的编辑器。


(不会,后期补充)

以上代码仅供参考,如有错误之处,希望大家指出,谢谢大家~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: