格式化输出
输出流提供了很多方法格式化内置类型的输出。对于用户自定义类型,则需要由程序员定义适合的<<操作。
输出整数
整型值可以输出为八进制、十进制和十六进制(十六进制多用于输出与硬件相关的信息,原因在于一个十六进制数字精确地表示了4位二进制值)。
例如:
cout<<1234<<'\t'<
这段代码会输出:
1234 4d2 2322
2322 //整数将以八进制的形式输出,直到输出格式被改变
也就是说,oct、hex和dec是持久的——后面的整数一直按照这种数制输出,直至我们指定新的数制。hex和oct这种用来改变流的行为的关键字被称为操纵符。
我们可以要求ostream显示每个整数的基数。例如:
cout<<1234<<'\t'<
会输出:
1234 4d2 2322
1234 0x4d2 02322
这样,十进制数将没有前缀,八进制数将带前缀0,而十六进制将带前缀0x(或0X)。这与C++源程序中的整数文字常量的表示方法是完全一致的。例如:
cout<<1234<<'\t'<<0x4d2<<'\t'<<02322<
如果是十进制输出格式,这段代码会输出:
1234 1234 1234
与oct和hex一样,showbase也是持久的。想去掉其效果的话,可以用noshowbase操纵符,它会恢复默认效果——输出整数时不显示基数。
对整数输出操纵符总结如下:
oct //使用8为基数的(八进制)表示
dec //使用10为基数的(十进制)表示
hex //使用16为基数的(十六进制)表示
showbase //为八进制加前缀0,为十六进制加前缀0x
noshowbase //取消前缀
输入整数
默认情况下,>>假定数值使用十进制表示,但你可以指定读入十六进制或八进制数:
int a;
int b;
int c;
int d;
cin>>a>>hex>>b>>oct>>c>>d;
cout<
如果你键入:
1234 4d2 2322 2322
上面程序会输出:
1234 1234 1234 1234
注意,这意味着oct、dec和hex对输入也是持久的,如同在输出操作中一样。
你可以让>>接受前缀0和0x并正确解释。为了实现这一效果,你需要“复位”所有默认设置,例如:
cin.unsetf(ios::dec);
cin.unsetf(ios::oct);
cin.unsetf(ios::hex);
流的成员函数unsetf()将参数中给出的一个或多个标识位复位。这时如果我们键入:
如果你键入:
1234 0x4d2 02322 2322
上面程序会输出:
1234 1234 1234 1234
输出浮点数
操纵符fixed、scientific和defaultfloat用来选择浮点数格式。现在,我们可以这么写:
cout<<1234.56789<<'\t'
<
会输出:
1234.57 1234.567890 1.234568e+003
1.234568e+003 //scientific操作符的效果是持久的
1234.57 1234.567890 1.234568e+003
对基本的浮点数格式化输出操纵符总结如下:
fixed //使用定点表示
scientific //使用尾数和指数表示方式。尾数总在[1:10)之间,也就是说,在小数点之前有单个非0数字
defaultfloat //在defaultfloat的精度范围内自动选择fixed或者scientific中更为精确的一种表示
精度
默认情况下,defaultfloat格式用总共6位数字来输出一个浮点值。流会选择最适合的格式,浮点值按6位数字(defaultfloat格式的默认精度)所能表示的最佳近似方式进行舍入。例如:
1234.567输出为1234.57
1.2345678输出为1.23457
1234567输出为1234567(因为这是一个整数)
1234567.0输出为1.23457e+006
基本上,defaultfloat格式在scientific和fixed两种格式间进行选择,期望将浮点数以最精确的表示形式呈现给用户,所采用的精度限定为general格式的精度——默认为6位数字长度。
程序员可以使用操纵符setprecision()来设置精度,例如:
cout<<1234.56789<<'\t'
<
会输出(注意舍入):
1234.57 1234.567890 1.234568e+003
1234.6 1234.56789 1.23457e+003
1234.5679 1234.56789000 1.23456789e+003
1234.56789 1234.567890000000 1.234567890000e+003 //defaultfloat下大于精度也不补0
几种格式的精度分别定义为:
defaultfloat //精度就是数字的个数
scientific //精度为小数点之后数字的个数
fixed //精度为小数点之后数字的个数
域
使用scientific和fixed格式,程序员可以精确控制一个值输出所占用的宽度。整数输出也有类似的机制,称为域。你可以使用“设置域宽度”操纵符setw()精确指定一个整数或一个字符串输出占用多少个位置。例如:
cout<<12345 //不使用域
<<'|'<
会输出:
12345|123456| 123456|123456|
首先注意第三个123456之前的两个空格,这就是我们所期望的效果——一个6位数字的数占用一个8个字符的域。但是,当你指定一个4个字符的域时,123456不会被截取来适应域宽,因为坏的格式总比“坏的输出数据”更好些。
域也可作用于浮点数和字符串,例如:
cout<<12345<<'|'<
会输出:
12345|12345| 12345|12345|
1234.5|1234.5| 1234.5|1234.5|
asdfg|asdfg| asdfg|asdfg|
注意,域的宽度不是持久的。除非你在语句中直接在输出操作之前设置域宽,否则不会有域的限制。
打开和定位文件
从C++程序的角度看,文件是操作系统提供的一个抽象。一个文件就是一个从0开始编号的简单的字节序列。问题是我们如何访问这些字节。如果使用iostream,访问方式很大程度上在我们打开文件将其与一个流相关联时就确定了。流的属性决定了文件打开后我们可以对它执行哪些操作,以及这些操作的意义。
文件打开模式
ios_base::app //追加模式(即添加在文件末尾)
ios_base::ate //“末端”模式(打开文件并定位到文件尾)
ios_base::binary //二进制模式———注意系统特有的行为
ios_base::in //读模式
ios_base::out //写模式
ios_base::trunk //将文件截为长度0
可以在文件名之后指定文件模式,例如:
ofstream of1{name1}; //默认设置为ios_base::out
ifstream if1{name2}; //默认设置为ios_base::in
ofstream ofs{name3,ios_base::app}; //带ios_base::out模式的默认设置的输出流
fstream fs{name4,ios_base::in|ios_base::out}; //同时带in和out模式的流
后一个例子中的“|”是“位或”运算符,可用于组合多个模式。app模式常用于写日志文件,因为你总是将新的日志追加到文件末尾。在每个例子中,打开文件的确切效果依赖于操作系统,而且如果操作系统不能使用某种特定的模式打开文件的话,流可能会进入非good()状态。
注意,如果以写模式打开一个文件,而文件不存在的话,通常操作系统会创建一个新文件,但如果以读模式打开一个不存在的文件,就不会创建新文件。
ofstream ofs{"no-such-file"}; //创建名为“no-such-file"的新文件
ifstream ifs{"no-file-of-this-name"}; //错误:ifs将处于非good()状态
只要条件允许,就应坚持只读取由istream打开的文件,只写入到由ostream打开的文件中。
二进制文件
默认情况下,iostream使用字符表示方式,也就是说,istream从文件读取字符序列,并将其转换为所需类型的对象。而ostream将指定类型的对象转换为字符序列,然后写入文件。但是,我们可以令istream和ostream将对象在内存中对应的字节序列简单地复制到文件。这称为二进制I/O,通过在打开文件时指定ios_base::binary模式来实现。例如:
int main()
{
//以二进制文件读取模式打开一个istream
cout<<"please enter input file name\n";
string iname;
cin>>iname;
ifstream ifs{iname,ios_base::binary}; //注意:流模式
//brinary告知流不要自作聪明地处理这些字节
if(!ifs) error("can't open input file",iname);
//以二进制文件写入模式打开一个ostream
cout<<"please enter output file name\n";
string oname;
cin>>oname;
ofstream ofs{oname,ios::binary}; //注意:流模式
//brinary告知流不要自作聪明地处理这些字节
if(!ofs) error("can't open output file",oname);
vectorv;
//从二进制文件中读取
for(int x;ifs.read(as_bytes(x),sizeof(int));) //注意:读入字节
v.push_back(x);
//对v进行处理
//写入到二进制文件
for(int x:v)
ofs.write(as_bytes(x),sizeof(int)); //注意:写入字节
return 0;
}
在上述例子中,我们使用相对复杂但也更为紧凑的二进制表示方式。当我们从面向字符的I/O转向二进制I/O时,要放弃常用的>>和<<操作符。这两个操作符按默认约定将值转换为字符序列(如,字符串”asdf“转换为字符a、s、d、f,整数123转换为字符1、2、3)。只有在默认模式不能满足需求时,我们才需要使用二进制文件。我们使用二进制模式,就是告知流不要自作聪明地处理字节序列。
我们如何处理int型值才是”聪明的“?显然是用4歌字节存储4字节宽的int型值,也就是说,我们可以查看int型值在内存中的表示方式(4个字节的序列),并直接将这些字节传输到文件。随后,我们就可以用同样的方式读回这些字节重组出int值:
ifs.read(as_bytes(x),sizeof(int)) //注意:读字节
ofs.write(as_bytes(x),sizeof(int)) //注意:写字节
ostream的write()函数和istream()的read()函数都接受两个参数:地址(这里用函数as_bytes()获取)和字节(字符)数量(这里我们用运算符sizeof获得)。对于我们要读/写的值,地址参数指向保存它的内存区域的第一个字节。
函数as_bytes()可以用来获取对象存储区域的第一个字节。它可以定义如下:
template
char *as_bytes(T& i) //将T视为一个字节序列
{
void* addr=&i; //得到保存对象的内存区域的第一个字节的地址
return static_cast(addr); //将内存视为多个字节
在文件中定位
只要有可能,尽量使用从头至尾的文件读写方式。很多时候,当你觉得必须对文件进行修改时,最好的方法是创建一个新的文件。
C++也支持在文件中定位到指定位置以进行读写。基本上,每个以读方式打开的文件,都有一个“读/获取位置”,而每个以写方式打开的文件,都有一个“写/放置位置”。
使用方法如下:
fstream fs{name}; //打开文件进行输入输出
if(!fs) error("can't open",name);
fs.seekg(5); //移动读位置(g表示“获取”)到5(从0开始,因此是第6个字符处)
char ch;
fs>>ch; //进行读操作,并增加读位置
cout<<"character[5] is "<
注意,seekg()和seekp()增加了它们的相对位置。请小心:这段代码中在文件定位之前进行了运行时错误检测,这是必要的。另外特别要注意的是,如果你试图定位(用seekg()或者seekp())到文件尾之后,结果如何是未定义的,不同操作系统会表现出不同行为。
字符串流
你可以将一个string对象作为istream的源,或者ostream的目标。从一个字符串读取内容的istream对象称为istringstream,保存字符并将其写入字符串的ostream对象称为ostringstream。例如,从字符串提取数值时,istringstream就很有用:
double str_to_double(string s)
//如果可能,将字符转换为浮点数
{
istringstream is{s}; //定义一个流来从s中读出
double d;
is>>d;
if(!is) error("double format error:",s);
return d;
}
相反,对于要求一个简单字符串参数的系统,如GUI系统,ostringstream可用于格式化输出来生成单一字符串。例如:
void my_code(string label,Temperature temp)
{
//...
ostringstream os; //生成一条消息的流
os<
ostringstream的成员函数str()返回由输出操作到ostringstream对象的内容构成的字符串。c.str()是string的成员函数,它返回很多系统接口所需要的C风格字符串。
stringstream通常用于将真实I/O和数据处理分离,可以看作一种裁剪I/O以适应特殊需求和偏好的机制。
ostringstream的一个简单应用是连接字符串,例如:
int seq_no=get_next_number(); //获取日志文件的编号
ostringstream name;
name<<"myfile"<
通常情况下,我们用一个string来初始化istringstream,然后用输入操作从该字符串中读取字符。相反,我们通常用一个空字符串初始化ostringstream,然后用输出操作向其中填入字符。有一种更为直接的方法来访问stringstream中的字符:ss.str()返回ss的字符串的一个拷贝,而ss.str(s)则将ss的字符串设置为s的一个拷贝。
面向行的输入
如果希望一次读取整行的内容,随后再决定如何从中格式化输入数据,可以使用函数getline(),例如:
string name;
getline(cin,name);
cout<
使用整行输入的一个常见原因是,默认的空白符不符合我们的要求。例如,我们可能将一行作为一句话,我们可以先读入一行,然后从中提取单个单词。
string command;
getline(cin,command);
stringstream ss{command};
vectorwords;
for(string s;ss>>s;)
words.push_back(s); //提取单个单词
字符分类
我们可以写出如下代码实现正确的单词分解:
for(char ch;cin.get(ch);){
if(isspace(ch)){ //如果ch是一个空白符
//什么也不做(也就是说,跳过空白符)
}
if(isdigit(ch)){
//读入一个数值
}
else if(isalpha(ch)){
//读入一个标识符
}
else{
//处理操作符号
}
}
}
函数istream::get()读入单个字符,赋予它的参数。它不跳过空白符。与>>类似,get()返回其istream对象的引用,便于我们检测其状态。
当我们采取逐个字符读取方式时,通常需要对字符进行分类,下面是实现字符分类的标准库函数:
isspace(c) //c是空白符吗(' '、'\t'、'\n',等等)?
isalpha(c) //c是字母吗('a'..'z'、'A'..'Z')(注意,不包括'_')?
isdigit(c) //c是十进制数字吗('0'..'9')?
isxdigit(c) //c是十六进制数字吗(十进制数字或者'a'..'f'、'A'..'F')?
isupper(c) //c是大写字母吗?
islower(c) //c是小写字母吗?
isalnum(c) //c是字母或十进制数字吗?
iscntrl(c) //c是控制字符吗(ASCII码0..31和127)?
ispunct(c) //c是标点(除字母、数字、空白符或不可见控制字符之外的字符)吗?
isprint(c) //c是可打印字符吗(ASCII字符' '..'~')?
isgraph(c) //c是字母、十进制数字或者标点吗(注意:不包括空白符)?
注意,多个字符分类可以用“或”运算符(||)进行组合。例如,isalnum(c)意味着isalpha(c)||isdigit(c)。
另外,标准库还提供了另外两个有用的函数,用来转换大小写:
toupper(c) //C或者c对应的大写字母
tolower(c) //C或者c对应的小写字母
如果你想忽略大小写的话,这两个函数很有用。我们可以为任何字符串定义tolower函数:
void tolower(string& s) //将s置为小写格式
{
for(char& x:s) x=tolower(x);
}
在处理这类问题时,使用tolower()比使用toupper()更好些。因为对于某些自然语言如德语,并不是所有小写字母都有对应的大写字母,因此前者能获得更好的效果。
使用非标准分隔符
例如,我们要分割开每个单词,可是,如果我们输入
As planned,the guests arrived;then,
我们会得到这些“单词”:
As
planned,
the
guests
arrived;
then,
如何处理达到我们的目的呢?我们可以逐个处理字符,将标点字符删除或者转换为空白符,然后再从“清理干净的”输入中读取数据:
string line;
getline(cin,line);
for(char& ch:line) //将每个标点字符替换为一个空格
{
switch(ch){
case ';':case '.':case '?':case '!':
ch=' ';
}
}
stringstream ss{line}; //使用istream ss读取整行
vectorvs;
for(string word;ss>>word;) //读取不带标点字符的单词
vs.push_back(word);
同样是前面给出的输入,以下代码会得到我们想要的单词:
As
planned
the
guests
arrived
then
接下来我们提出一种更为通用、有效的输入流中删除不需要字符的方法。基本思想是先从一个普通输入流读入单词,然后使用用户指定的“空白符”来处理输入内容,也就是说,我们并不将“空白符”交给用户,我们只是用它们来分割单词。我们用一个类来实现:
class Punct_stream{
public:
Punct_stream(istream& is)
:source{is},sensitive{true} {}
void whitespace(const string& s) //定义s为空白符集
{ white=s; }
void add_white(char c) { white+=c; } //加入到空白符集
bool is_whitespace(char c); //c在空白符集中?
void case_sensitive(bool b) { sensitive=b; }
bool is_case_sensitive() { return sensitive; }
Punct_stream& operator>>(string& s);
operator bool();
private:
istream& source; //符号源
istringstream buffer; //使用buffer处理格式
string white; //被视为“空白符”的符号
bool sensitive; //该stream是否大小写敏感?
};
输入操作符>>的基本策略是从istream读取一整行,存入一个名为line的字符串,然后将所有自定义空白符转换为空格符,我们将line放入名为buffer的istringstream中。注意,从buffer中读取数据直接就可以进行,但只有它为空的情况下,才能向其写入内容。
Punct_stream& Punct_stream::operator>>(string& s)
{
while(!(buffer>>s)){ //尝试从buffer中读取
if(buffer.bad()||!source.good()) return *this;
buffer.clear();
string line;
getline(source,line); //从source中读入一行
//按需进行字符替换
for(char& ch:line)
if(is_whitespace(ch))
ch=' '; //替换为空格
else if(!sensitive)
ch=tolower(ch); //替换为小写符号
buffer.str(line); //将符号串放入到流中
}
return *this;
}
如果名为buffer的istringstream中存有字符,读操作buffer>>s就可以进行,s会收到“空白符”分隔的单词,随后就没有什么可做的了。只要buffer中有我们可以读取的字符,这个过程就会发生。但是,当buffer>>s失败时,也就是!(buffer>>s)为真时,我们必须利用source中的内容将buffer重新填满。注意读操作buffer>>s是在一个循环中,当我们尝试重新填充buffer后,会再次尝试这个读操作,因此有如下代码:
while(!(buffer>>s)){ //尝试从buffer中读取
if(buffer.bad()||!source.good()) return *this;
buffer.clear();
//重新填充buffer
如果buffer处于bad()状态,或者source有问题的话,我们将放弃读取操作。否则,我们清空buffer,再次尝试。我们清空buffer的原因是,只有在读失败的情况下,通常是buffer遇到eof()时,我们才进入“重新填充循环”,而这意味着buffer中没有供我们读取的字符。
一旦我们正确处理完line中内容,就需要将其存入istringstream。buffer.str(line)完成这一工作,这条语句可以读成“将istringstream对象buffer的字符串设置为line的内容”。
istream的一种习惯用法是测试>>的结果,例如:
while(ps>>s) {/*...*/}
这意味着我们需要一种方法将ps>>s的结果当作布尔值来进行检查。但ps>>s结果是一个Punct_stream,因此我们需要一种方法将一个Punct_stream隐式转换为一个bool值。这就是Punct_stream的运算符bool()所做的事情。
Punct_stream::operator bool()
{
return !(source.fail()||source.bad())&&source.good();
}
现在我们就可以写程序了。
#include "std_lib_facilities.h"
using namespace std;
class Punct_stream{
public:
Punct_stream(istream& is)
:source{is},sensitive{true} {}
void whitespace(const string& s) //定义s为空白符集
{ white=s; }
void add_white(char c) { white+=c; } //加入到空白符集
bool is_whitespace(char c); //c在空白符集中?
void case_sensitive(bool b) { sensitive=b; }
bool is_case_sensitive() { return sensitive; }
Punct_stream& operator>>(string& s);
operator bool();
private:
istream& source; //符号源
istringstream buffer; //使用buffer处理格式
string white; //被视为“空白符”的符号
bool sensitive; //该stream是否大小写敏感?
};
Punct_stream& Punct_stream::operator>>(string& s)
{
while(!(buffer>>s)){ //尝试从buffer中读取
if(buffer.bad()||!source.good()) return *this;
buffer.clear();
string line;
getline(source,line); //从source中读入一行
//按需进行字符替换
for(char& ch:line)
if(is_whitespace(ch))
ch=' '; //替换为空格
else if(!sensitive)
ch=tolower(ch); //替换为小写符号
buffer.str(line); //将符号串放入到流中
}
return *this;
}
bool Punct_stream::is_whitespace(char c)
{
for(char w:white)
if(c==w) return true;
return false;
}
Punct_stream::operator bool()
{
return !(source.fail()||source.bad())&&source.good();
}
int main()
//给定文本输入,产生一个该文本中所有单词的升序列表
//忽略标点符号和大小写的区别
//去掉输入中的重复结果
{
Punct_stream ps{cin};
ps.whitespace(":;,.?!()\"{}<>/&$@#%^*|~"); //注意,\"表示在字符串中的"
ps.case_sensitive(false);
cout<<"please enter words\n";
vectorvs;
for(string word;ps>>word;)
vs.push_back(word); //读入单词
sort(vs.begin(),vs.end()); //按字典序进行排序
for(int i=0;i