C++流格式控制(2006-9-25 15:25:00)
当输入/输出的数据没有指定格式,它们都按缺省的格式输入/输出。然而,有时需要对数据格式进行控制。这时需利用ios类中定义的格式控制成员函数,通过调用它们来完成格式的设置。ios类的格式控制函数如下所示:
long flags( ) const | 返回当前的格式标志。 |
long flays(long newflag) | 设置格式标志为newflag,返回旧的格式标志。 |
long setf(long bits) | 设置指定的格式标志位,返回旧的格式标志。 |
long setf(long bits,long field) | 将field指定的格式标志位置为bits,返回旧的格式标志。 |
long unsetf(long bits) | 清除bits指定的格式标志位,返回旧的格式标志。 |
long fill(char c) | 设置填充字符,缺省条件下是空格。 |
char fill( ) | 返回当前填充字符。 |
int precision(int val) | 设置精确度为val,控制输出浮点数的有效位,返回旧值。 |
int precision( ) | 返回旧的精确度值。 |
int width(int val) | 设置显示数据的宽度(域宽),返回旧的域宽。 |
int width( ) | 只返回当前域宽,缺省宽度为0。这时插入操作能按表示数据的最小宽度显示数据。 |
预定义的操纵算子
使用成员函数控制格式化输入输出时,每个函数调用需要写一条语句,尤其是它不能用在插入或提取运算符的表达式中,而使用操纵算子,则可以在插入和提取运算符的表达式中控制格式化输入和输出。在程序中使用操纵算字必须嵌入头文件iomanip.h
dec | 十进制的输入输出 |
hex | 十六进制的输入输出 |
oct | 八进制的输入输出 |
ws | 提取空白字符 |
ends | 输出一个nul字符 |
endl | 输出一个换行字符,同时刷新流 |
flush | 刷新流 |
resetiosflags(long) | 请除特定的格式标志位 |
setiosflags(long) | 设置特定的格式标志位 |
setfill(char) | 设置填充字符 |
setprecision(int) | 设置输出浮点数的精确度 |
setw(int) | 设置域宽格式变量 |
错误处理
在对一个流对象进行I/O操作时,可能会产生错误。当错误发生时,错误的性质被记录在ios类的一个数据成员中。
ios类中定义的描述错误状态的常量:
goodbit | 没有错误,正常状态 eofbit 到达流的结尾 |
failbit | I/O操作失败,清除状态字后,可以对流继续进行操作。 |
badbit | 试图进行非法操作,清除状态字后,流可能还可以使用。 |
hardfail | 致命错误,不可恢复的错误。 |
ostream类的成员函数
流的其它成员函数可以从流中读取字符或字符串,对流进行无格式化的输入 输出操作,以及直接控制对流的I/O操作。
返回类型 | ios类的成员 | 描 述 |
ostream* | tie(ostream*) | 将当前流与指定的输出流连接起来。每当需要 读取当前流时,连接的流会自动刷新。C++流库已用cin.tie(cout)将输入流与输出流连接起来。要取消与输出流的连接可采用is.tie(0) |
ostream* | tie( ) | 返回指向连接流的指针 |
返回类型 | ostream类的成员 | 描 述 |
ostream& | put(char ch) | 向流中输出一个字符ch,不进行任何转换 |
ostream& | write(char*,int) | 向流中输出指定长度的字符串,不进行转换 |
ostream& | flush( ) | 刷新流,输出所有缓冲的但还未输出的数据 |
ostream& | seekp(streampos) | 移动流的当前指针到给定的绝对位置 |
ostream& | seekp(sereamoff,seek_dir) | 流的当前指针类似与文件的当前指针 |
streampos | teelp( ) | 返回流的当前指针的绝对位置 |
istream类的成员函数
返回类型 | istream类的成员 | 描 述 |
int | get( ) | 读取并返回一个字符 |
istream& | get(char&c) | 读取字符并存入c中 |
istream& | get(char*ptr,int len,char delim='') | 读取指定的字符到缓冲区中,直到遇到指定的分界符为止,分界符不填入缓冲区。 |
istream& | getline(char*ptr,int len,char delim='') | 与get(char*ptr,int len,chardelim ='') 类似,但将分界符填入缓冲区。 |
istream& | putback( ) | 将最近读取的字符放回流中 |
istream& | read(char*,int) | 读取规定长度的字符串到缓冲区中 |
int | peek( ) | 返回流中下一个字符,但不移动文件指针 |
istream& | seekg(streampos) | 移动当前指针到一绝对地址 |
istream& | seekg(streampos,seek_dir) | 移动当前指针到一相对地址 |
streampos | tellg( ) | 返回当前指针 |
istream& | ignore(int n=1,delim=EOF) | 跳过流中几个字符,或直到遇到指定的分界符为止 |
附:以16进制形式打印内存数据:
#include <iostream>
#include <iomanip>
using namespace std;
void foo( const void* buf, size_t len )
{
const unsigned char* p = (const unsigned char*)buf;
for( size_t i=0; i<len; ++i )
{
cout << setfill('0') << setw(2) << uppercase << hex << (unsigned)p[i] << ' ';
}
cout << endl;
}
int main( void )
{
char *test1 = "/x00/x01/x10/xFF";
foo( test1, 4 );
double test2 = 123.456;
foo( &test2, 8 );
return 0 ;
};
输出:
00 01 10 FF
77 BE 9F 1A 2F DD 5E 40
补充(2008-11-06):
每一个iostream库对象都维护了一个格式状态(format state),它控制格式化操作的细节,比如整型值的进制基数或浮点数值的精度。C++为程序员提供了一组预定义的操纵符,可用来修改一个对象的格式状态。
操纵符被应用在流对象上的方式,就好像它们是数据一样。但是,操纵符不导致读写数据,而是修改流对象的内部状态。例如,缺省情况下,true值的bool对象被写成整数值1:
#include <iostream>
int main()
{
bool illustrate = true;;
cout << "bool object illustrate set to true: "
<< illustrate << '/n';
}
为了修改cout,使它能够把illustrate显示为true,我们应用boolalpha操纵符:
#include <iostream>
int main()
{
bool illustrate = true;;
cout << "bool object illustrate set to true: ";
// 改变cout的状态
// 用字符串true和false输出bool值
cout << boolalpha;
cout << illustrate << '/n';
}
因为操纵符被应用之后,仍然返回原来被应用的流对象,所以我们可以把它的应用与数据的应用连接起来(或者与其他操纵符的应用连接起来)。下面是重写之后的小程序,它混合了数据和操纵符:
#include <iostream>
int main()
{
bool illustrate = true;
cout << "bool object illustrate: "
<< illustrate
<< "/nwith boolalpha applied: "
<< boolalpha << illustrate << '/n';
// ...
}
像这样,把操纵符和数据混合起来容易产生误导作用。应用操纵符之后,不只改变了后面输出值的表示形式,而且修改了ostream的内部格式状态。在我们的例子中,整个程序的余下部分都将把bool值显示为true或false。
为了消除对cout的修改,我们必须应用noboolalpha操纵符:
cout << boolalpha // 设置cout的内部状态
<< illustrate
<< noboolalpha // 解除cout内部状态
我们将会看到,许多操纵符都有类似的“设置/消除(set/unset)”对 。
缺省情况下,算术值以十进制形式被读写。程序员可以通过使用hex、oct和dec操纵符,把整数值的进制基数改为八进制或十六进制,或改回十进制(浮点值的表示不受影响)。例如:
#include <iostream>
int main()
{
int ival = 16;
double dval = 16.0;
cout << "ival: " << ival
<< " oct set: " << oct << ival << "/n";
cout << "dval: " << dval
<< " hex set: " << hex << dval << "/n";
cout << "ival: " << ival
<< " dec set: " << dec << ival << "/n";
}
编译并执行程序,产生下列输出:
ival: 16 oct set: 20
dval: 16 hex set: 16
ival: 10 dec set: 16
我们这个程序的一个问题是,我们在看到一个值的时候无法知道它的进制基数。例如,20是真正的20,还是16的八进制表示?操纵符showbase可以让一个整数值在输出时指明它的基数,形式如下:
1.0x开头表明是十六进制数(如果希望显示为大写字母,则可以应用uppercase操纵符;为了转回小写的x,我们可以应用nouppercase操纵符)。
2.以0开头表示八进制数。
3.没有任何前导字符,表示十进制数。
下面是用showbase改写过的程序:
#include <iostream>
int main()
{
int ival = 16;
double dval = 16.0;
cout << showbase;
cout << "ival: " << ival
<< " oct set: " << oct << ival << "/n";
cout << "dval: " << dval
<< " hex set: " << hex << dval << "/n";
cout << "ival: " << ival << " dec set: "
<< dec << ival << "/n";
cout << noshowbase;
}
下面是修改后的输出:
ival: 16 oct set: 020
dval: 16 hex set: 16
ival: 0x10 dec set: 16
noshowcase操纵符重新设置cout,使它不再显示整数值的进制基数。
缺省情况下,浮点值有6位的精度。这个值可以用成员函数precision(int)或流操纵符setprecision()来修改(若使用后者,则必须包含iomanip头文件)。precision()返回当前的精度值。例如:
#include <iostream>
#include <iomanip>
#include <math.h>
int main()
{
cout << "Precision: "
<< cout.precision() << endl
<< sqrt(2.0) << endl;
cout.precision(12);
cout << "/nPrecision: "
<< cout.precision() << endl
<< sqrt(2.0) << endl;
cout << "/nPrecision: " << setprecision(3)
<< cout.precision() << endl
<< sqrt(2.0) << endl;
return 0;
}
编译并执行程序,产生以下输出:
Precision: 6
1.41421
Precision: 12
1.41421356237
Precision: 3
1.41
带有一个实参的操纵符,比如前面见到的setprecision()和setw(),要求包含iomanip头文件:
#include <iomanip>
我们的例子没有说明setprecision()的两个更深入的方面:1)整数值不受影响,2)浮点值被四舍五入而不是被截取。因此当精度为4时,3.14159变成3.142,精度为3时变成3.14。
缺省情况下,当小数部分为0时,不显示小数点。例如:
cout << 10.00
输出为
10
为了强制显示小数点,我们使用showpoint操纵符:
cout << showpoint
<< 10.0
<< noshowpoint << '/n';
noshowpoint操纵符重新设置缺省行为。
缺省情况下,浮点值以定点小数法显示。为了改变为科学计数法,我们使用scientific操纵符。为了改回到定点小数法,我们使用fixed操纵符:
cout << "scientific: " << scientific
<< 10.0
<< "fixed decimal: " << fixed
<< 10.0 << '/n';
这产生
scientific: 1.0e+01
fixed decimal: 10
如果希望把‘e’输出为‘E’,我们可以使用uppercase操纵符。要转回小写字母,我们使用nouppercase操纵符。(uppercase操纵符不会使所有字母字符都显示为大写!)。
缺省情况下,重载的输入操作符跳过空白字符(空格、制表符、换行符、走纸、回车)。已知序列
a b c
d
循环
char ch;
while ( cin >> ch )
// ...
执行四次,以读入从a到d的四个字符,跳过中间的空格、可能的制表符和换行符。操纵符noskipws使输入操作符不跳过空白字符:
char ch;
cin >> noskipws;
while ( cin >> ch )
// ...
cin >> skipws;
现在while循环需要迭代七次,才能读入字符a到d。为了转回到缺省行为,我们在cin上应用操纵符skipws。
当我们写
cout << "please enter a value: ";
文字字符串被存储在与cout相关联的缓冲区中。有许多种情况可以引起缓冲区被刷新——即,清空——在我们的例子中,也就是将缓冲区写到标准输出上:
1.缓冲区可能会满,在这种情况下,它必须被刷新,以便读取后面的值。
2.我们可通过显式地使用flush、ends或endl操纵符来刷新缓冲区。
// 清空缓冲区
cout << "hi!" << flush;
// 插入一个空字符然后刷新缓冲区
char ch[2]; ch[0] = 'a'; ch[1] = 'b';
cout << ch << ends;
// 插入一个换行符然后刷新缓冲区
cout << "hi!" << endl;
3.unitbuf,一个内部的流状态变量,若它被设置,则每次输出操作后都会清空缓冲区。
4.一个ostream对象可以被捆绑到一个istream上,在这种情况下,当istream从输入流读取数据时,ostream的缓冲区就会被刷新。cout被预定义为“捆绑”在cin上:
cin.tie( &cout );
语句
cin >> ival;
使得与cout相关联的缓冲区被刷新。
一个ostream对象一次只能被捆绑到一个istream对象上,为了打破现有的捆绑,我们可以传递一个实参0。例如:
istream is;
ostream new_os;
// ...
// tie() 返回现有的捆绑
ostream *old_tie = is.tie();
is.tie( 0 ); // 打破现有的捆绑
is.tie( &new_os ); // 设置新的捆绑
// ...
is.tie( 0 ); // 打破现有的捆绑
is.tie( old_tie ); // 重新建立原来的捆绑
我们可以用setw()操纵符来控制数字或字符串值的宽度。例如程序
#include <iostream>
#include <iomanip>
int main()
{
int ival = 16;
double dval = 3.14159;
cout << "ival: " << setw(12) << ival << '/n'
<< "dval: " << setw(12) << dval << '/n';
}
产生以下输出:
ival: 16
dval: 3.14159
第二个setw()是必需的,因为不像其他操纵符,setw()不修改ostream对象的格式状态。
要想使输出的值左对齐,我们可以应用left操纵符(通过right操纵符可以重新设回到缺省状态)。如果我们希望产生
16
- 3
我们可以应用internal操纵符,它使得正负符号左对齐,而值右对齐,中间添加空格。如果希望用其他字符填充中间的空白,则可以应用setfill()操纵符。
cout << setw(6) << setfill('%') << 100 << endl;
产生
%%%100
预定义的所有操纵符都被列在表20.1中。
操 纵 符 含 义
boolalpha 把true 和 false 表示为字符串
*noboolalpha 把true 和 false 表示为0、1
showbase 产生前缀,指示数值的进制基数
*noshowbase 不产生进制基数前缀
showpoint 总是显示小数点
*noshowpoint 只有当小数部分存在时才显示小数点
Showpos 在非负数值中显示 +
*noshowpos 在非负数值中不显示 +
*skipws 输入操作符跳过空白字符
noskipws 输入操作符不跳过空白字符
uppercase 在十六进制下显示 0X , 科学计数法中显示E
*nouppercase 在十六进制下显示 0x, 科学计数法中显示e
*dec 以十进制显示
hex 以十六进制显示
oct 以八进制显示
left 将填充字符加到数值的右边
right 将填充字符加到数值的左边
续表
操 纵 符 含 义
Internal 将填充字符加到符号和数值的中间
*fixed 以小数形式显示浮点数
scientific 以科学计数法形式显示浮点数
flush 刷新ostream缓冲区
ends 插入空字符,然后刷新ostream缓冲区
endl 插入换行符,然后刷新ostream缓冲区
ws “吃掉” 空白字符
// 以下这些要求 #include <iomanip>
setfill( ch ) 用ch填充空白字符
setprecision( n ) 将浮点精度设置为 n
setw( w ) 按照w个字符来读或者写数值
setbase( b ) 以进制基数 b 输出整数值
* 表示缺省的流状态
iostream库是强类型的。例如,试图从一个ostream读数据,或者写数据到一个istream,都会在编译时刻被捕获到,并标记为类型违例。例如,已知下列声明
#include <iostream>
#include <fstream>
class Screen;
extern istream& operator>>( istream&, const Screen& );
extern void print( ostream& );
ifstream inFile;
下面的两条语句都将导致编译时刻类型违例:
int main()
{
Screen myScreen;
// 错误:期望一个 ostream&
print( cin >> myScreen );
// 错误:期望 >> operator
inFile << "error: output operator";
}
stringstream通常是用来做数据转换的。
相比c库的转换,它更加安全,自动和直接。
例子一:基本数据类型转换例子 int转string
#include <string>
#include <sstream>
#include <iostream>
int main()
{
std::stringstream stream;
std::string result;
int i = 1000;
stream << i; //将int输入流
stream >> result; //从stream中抽取前面插入的int值
std::cout << result << std::endl; // print the string "1000"
}
运行结果:
例子二:除了基本类型的转换,也支持char *的转换。
#include <sstream>
#include <iostream>
int main()
{
std::stringstream stream;
char result[8] ;
stream << 8888; //向stream中插入8888
stream >> result; //抽取stream中的值到result
std::cout << result << std::endl; // 屏幕显示 "8888"
}
例子三:再进行多次转换的时候,必须调用stringstream的成员函数clear().
#include <sstream>
#include <iostream>
int main()
{
std::stringstream stream;
int first, second;
stream<< "456"; //插入字符串
stream >> first; //转换成int
std::cout << first << std::endl;
stream.clear(); //在进行多次转换前,必须清除stream
stream << true; //插入bool值
stream >> second; //提取出int
std::cout << second << std::endl;
}
运行clear的结果
没有运行clear的结果
在C++中,有一个stream这个类,所有的I/O都以这个“流”类为基础的,包括我们要认识的文件I/O,stream这个类有两个重要的运算符:
1、插入器(<<)
向流输出数据。比如说系统有一个默认的标准输出流(cout),一般情况下就是指的显示器,所以,cout<<"Write Stdout"<<'/n';就表示把字符串"Write Stdout"和换行字符('/n')输出到标准输出流。
2、析取器(>>)
从流中输入数据。比如说系统有一个默认的标准输入流(cin),一般情况下就是指的键盘,所以,cin>>x;就表示从标准输入流中读取一个指定类型(即变量x的类型)的数据。
在C++中,对文件的操作是通过stream的子类fstream(file stream)来实现的,所以,要用这种方式操作文件,就必须加入头文件fstream.h。下面就把此类的文件操作过程一一道来。
一、打开文件
在fstream类中,有一个成员函数open(),就是用来打开文件的,其原型是:
void open(const char* filename,int mode,int access);
参数:
filename: 要打开的文件名
mode: 要打开文件的方式
access: 打开文件的属性
打开文件的方式在类ios(是所有流式I/O类的基类)中定义,常用的值如下:
ios::app: 以追加的方式打开文件
ios::ate: 文件打开后定位到文件尾,ios:app就包含有此属性
ios::binary: 以二进制方式打开文件,缺省的方式是文本方式。两种方式的区别见前文
ios::in: 文件以输入方式打开
ios::out: 文件以输出方式打开
ios::nocreate: 不建立文件,所以文件不存在时打开失败
ios::noreplace:不覆盖文件,所以打开文件时如果文件存在失败
ios::trunc: 如果文件存在,把文件长度设为0
可以用“或”把以上属性连接起来,如ios::out|ios::binary
打开文件的属性取值是:
0:普通文件,打开访问
1:只读文件
2:隐含文件
4:系统文件
可以用“或”或者“+”把以上属性连接起来 ,如3或1|2就是以只读和隐含属性打开文件。
例如:以二进制输入方式打开文件c:/config.sys
fstream file1;
file1.open("c://config.sys",ios::binary|ios::in,0);
如果open函数只有文件名一个参数,则是以读/写普通文件打开,即:
file1.open("c://config.sys");<=>file1.open("c://config.sys",ios::in|ios::out,0);
另外,fstream还有和open()一样的构造函数,对于上例,在定义的时侯就可以打开文件了:
fstream file1("c://config.sys");
特别提出的是,fstream有两个子类:ifstream(input file stream)和ofstream(outpu file stream),ifstream默认以输入方式打开文件,而ofstream默认以输出方式打开文件。
ifstream file2("c://pdos.def");//以输入方式打开文件
ofstream file3("c://x.123");//以输出方式打开文件
所以,在实际应用中,根据需要的不同,选择不同的类来定义:如果想以输入方式打开,就用ifstream来定义;如果想以输出方式打开,就用ofstream来定义;如果想以输入/输出方式来打开,就用fstream来定义。
二、关闭文件
打开的文件使用完成后一定要关闭,fstream提供了成员函数close()来完成此操作,如:file1.close();就把file1相连的文件关闭。
三、读写文件
读写文件分为文本文件和二进制文件的读取,对于文本文件的读取比较简单,用插入器和析取器就可以了;而对于二进制的读取就要复杂些,下要就详细的介绍这两种方式
1、文本文件的读写
文本文件的读写很简单:用插入器(<<)向文件输出;用析取器(>>)从文件输入。假设file1是以输入方式打开,file2以输出打开。示例如下:
file2<<"I Love You";//向文件写入字符串"I Love You"
int i;
file1>>i;//从文件输入一个整数值。
这种方式还有一种简单的格式化能力,比如可以指定输出为16进制等等,具体的格式有以下一些
操纵符 功能 输入/输出
dec 格式化为十进制数值数据 输入和输出
endl 输出一个换行符并刷新此流 输出
ends 输出一个空字符 输出
hex 格式化为十六进制数值数据 输入和输出
oct 格式化为八进制数值数据 输入和输出
setpxecision(int p) 设置浮点数的精度位数 输出
比如要把123当作十六进制输出:file1<<hex<<123;要把3.1415926以5位精度输出:file1<<setpxecision(5)<<3.1415926。
2、二进制文件的读写
①put()
put()函数向流写入一个字符,其原型是ofstream &put(char ch),使用也比较简单,如file1.put('c');就是向流写一个字符'c'。
②get()
get()函数比较灵活,有3种常用的重载形式:
一种就是和put()对应的形式:ifstream &get(char &ch);功能是从流中读取一个字符,结果保存在引用ch中,如果到文件尾,返回空字符。如file2.get(x);表示从文件中读取一个字符,并把读取的字符保存在x中。
另一种重载形式的原型是: int get();这种形式是从流中返回一个字符,如果到达文件尾,返回EOF,如x=file2.get();和上例功能是一样的。
还有一种形式的原型是:ifstream &get(char *buf,int num,char delim='/n');这种形式把字符读入由 buf 指向的数组,直到读入了 num 个字符或遇到了由 delim 指定的字符,如果没使用 delim 这个参数,将使用缺省值换行符'/n'。例如:
file2.get(str1,127,'A');//从文件中读取字符到字符串str1,当遇到字符'A'或读取了127个字符时终止。
③读写数据块
要读写二进制数据块,使用成员函数read()和write()成员函数,它们原型如下:
read(unsigned char *buf,int num);
write(const unsigned char *buf,int num);
read()从文件中读取 num 个字符到 buf 指向的缓存中,如果在还未读入 num 个字符时就到了文件尾,可以用成员函数 int gcount();来取得实际读取的字符数;而 write() 从buf 指向的缓存写 num 个字符到文件中,值得注意的是缓存的类型是 unsigned char *,有时可能需要类型转换。
例:
unsigned char str1[]="I Love You";
int n[5];
ifstream in("xxx.xxx");
ofstream out("yyy.yyy");
out.write(str1,strlen(str1));//把字符串str1全部写到yyy.yyy中
in.read((unsigned char*)n,sizeof(n));//从xxx.xxx中读取指定个整数,注意类型转换
in.close();out.close();
四、检测EOF
成员函数eof()用来检测是否到达文件尾,如果到达文件尾返回非0值,否则返回0。原型是int eof();
例: if(in.eof())ShowMessage("已经到达文件尾!");
五、文件定位
和C的文件操作方式不同的是,C++ I/O系统管理两个与一个文件相联系的指针。一个是读指针,它说明输入操作在文件中的位置;另一个是写指针,它下次写操作的位置。每次执行输入或输出时, 相应的指针自动变化。所以,C++的文件定位分为读位置和写位置的定位,对应的成员函数是 seekg()和 seekp(),seekg()是设置读位置,seekp是设置写位置。它们最通用的形式如下:
istream &seekg(streamoff offset,seek_dir origin);
ostream &seekp(streamoff offset,seek_dir origin);
streamoff定义于 iostream.h 中,定义有偏移量 offset 所能取得的最大值,seek_dir 表示移动的基准位置,是一个有以下值的枚举:
ios::beg: 文件开头
ios::cur: 文件当前位置
ios::end: 文件结尾
这两个函数一般用于二进制文件,因为文本文件会因为系统对字符的解释而可能与预想的值不同。
例:
file1.seekg(1234,ios::cur);//把文件的读指针从当前位置向后移1234个字节
file2.seekp(1234,ios::beg);//把文件的写指针从文件开头向后移1234个字节
=================================
流
在数据从一个对象流动到另一个对象的过程中,无论是否对数据进行缓冲或对数据进行格式变换,这种流动都被抽象为流.
=================================================
cout 标准输出
cerr 标准错误输出,没有缓冲,发给它的内容立即输出
clog 类似于cerr,但有缓冲,缓冲区满时被输出.
====================================================
根据数据的组织形式,文件可以分为文本文件和二进制文件。
文本文件也称ASCII文件,每个字节存放一个ASCII字符。
二进制文件是将数据按在内存中的存储形式存放到磁盘上。
+++++++++++++++++++++++++++++++
再注意一下main函数中最激动人心的那一行:
myout << myStr << a << "/n";
我 们知道,最后出现的"/n"可以实现一个换行,不过我们在用C++时教程中总是有意无意地让我们使用endl,两者看上去似乎一样——究竟其中有什么玄 妙?查书,书上说endl是一个操纵符(manipulator),它不但实现了换行操作,而且还对输出缓冲区进行刷新。什么意思呢?原来在执行输出操作 之后,数据并非立刻传到输出设备,而是先进入一个缓冲区,当适宜的时机(如设备空闲)后再由缓冲区传入,也可以通过操纵符flush进行强制刷新:
cout << "Hello, World! " << "Flush the screen now!!!" << flush;
这样当程序执行到operator<<(flash)之前,有可能前面的字符串数据还在缓冲区中而不是显示在屏幕上,但执行 operator<<(flash)之后,程序会强制把缓冲区的数据全部搬运到输出设备并将其清空。而操纵符endl相当于<< "/n" << flush的简写版本,它先输出一个换行符,再实现缓冲区的刷新。大概这是因为一般的输出都是以换行结尾,而结尾处又是习惯进行刷新的时期,方便起见就把两者结合成了endl。
使用C++读写二进制文件
要读取文件必须包含<fstream>头文件,这里包含了C++读写文件的方法。
可以使用fstream类,这个类可以对文件进行读写操作。
1、打开文件。
打开文件可以有两种方式,第一种可以使用fstream类的构造函数。
fstream file("test.dat",ios_base::in|ios_base::out|ios_base::app);
另外一种方法就是使用open函数。
fstream file;
file.open("test.dat",ios_base::in|ios_base::out|ios_base::app);
这样就可以打开一个可读写的文件了。如果文件不存在的话,就会创建一个新文件并且以读写方式打开。
这里需要说明一点,如果文件不存在的话,open函数中第二个参数必须包含ios_base::out|ios_base::app,
否则就不能正确创建文件。
2、写文件。
先进性写文件的操作否则读一个空文件是没有意义的。
既然是写二进制文件可以向文件中写入一个整形值。写二进制字符只能使用write函数。
但是write函数的原形是write(const char * ch, int size)。第一个参数是char *类型,所以需要把将要写入
文件的int类型转换成char *类型。这里的转换困扰了我好几天,不过终于弄明白了。代码如下。
int temp;
file.write((char *)(&temp),sizeof(temp));
3、读文件。
可以写文件了,读文件就好办多了。读文件需要用到read函数。其参数和write大致相同,read(const char * ch, int size)。
要把内容读到int类型变量中同样涉及到一个类型转换的问题。和写文件一样。
int readInt;
file.read((char *)(&readInt),sizeof(readInt));
这样文件中的int值就读入到int型变量readInt中了。
4、文件指针。
在文件的读写过程中往往需要对文件进行选择性读取。所以需要进行文件指针的移动。这是需要用到seekg和seekp函数。
在fstream类中有两个文件指针,一个是读取文件的指针,一个是写文件的指针分别用tellg和tellp文件来取得指针的位置。
同样seekg和seekp两个函数分别是对这两个指针进行移动的函数。这两个函数的参数都是一样的。
先对几个枚举类型进行一下说明:
ios_base::beg ——文件开始位置
ios_base::cur ——文件当前位置
ios_base::end ——文件末尾位置
下面以seekg为例说明一下指针移动的方法:
file.seekg(3) ——指针移动到第三个字符的位置
file.seekg(ios_base::beg) ——指针移动到文件开头
file.seekg(ios_base::end) ——指针移动到文件末尾
file.seekg(-3,ios_base::cur) ——指针当前位置向前移动三个字符
file.seekg(3,ios_base::cur) ——指针当前位置向后移动三个字符
file.seekg(3,file.tellg()) ——指针当前位置向后移动三个字符
file.seekg(file.tellg()+3) ——指针当前位置向后移动三个字符
5、对文件操作完毕后别忘了关闭文件。
file.close();
以上5个步骤就完成了对文件的读写操作。文本文件的操作是相同的,比二进制文件还要简单
作者:管宁
o 1 iostream: istream 和 ostream
o 2 fstream: ifstream 和 ofstream
o 3 strstream: ostrstream 和 istrstream
我们从一开始就一直在利用C++的输入输出在做着各种练习,输入输出是由iostream库提供的,所以讨论此标准库是有必要的,它与C语言的 stdio库不同,它从一开始就是用多重继承与虚拟继承实现的面向对象的层次结构,作为一个c++的标准库组件提供给程序员使用。
iostream为内置类型类型对象提供了输入输出支持,同时也支持文件的输入输出,类的设计者可以通过对iostream库的扩展,来支持自定义类型的输入输出操作。
为什么说要扩展才能提供支持呢?我们来一个示例。
#include <stdio.h>
#include <iostream>
using namespace std;
class Test
{
public:
Test(int a=0,int b=0)
{
Test::a=a;
Test::b=b;
}
int a;
int b;
};
int main()
{
Test t(100,50);
printf("%???",t);//不明确的输出格式
scanf("%???",t);//不明确的输入格式
cout<<t<<endl;//同样不够明确
cin>>t;//同样不够明确
system("pause");
}
由于自定义类的特殊性,在上面的代码中,无论你使用c风格的输入输出,或者是c++的输入输出都不是不明确的一个表示,由于c语言没有运算符重载机制,导致stdio库的不可扩充性,让我们无法让printf()和scanf()支持对自定义类对象的扩充识别,而c++是可以通过运算符重载机制扩充 iostream库的,使系统能能够识别自定义类型,从而让输入输出明确的知道他们该干什么,格式是什么。
在上例中我们之所以用printf与cout进行对比目的是为了告诉大家,C与C++处理输入输出的根本不同,我们从c远的输入输出可以很明显看出是函数调用方式,而c++的则是对象模式,cout和cin是ostream类和istream类的对象。
C++中的iostream库主要包含下图所示的几个头文件:
IOSstream 库 | |
fstream | iomainip |
ios | iosfwd |
iostream | istream |
ostream | sstream |
streambuf | strstream |
我们所熟悉的输入输出操作分别是由istream(输入流)和ostream(输出流)这两个类提供的,为了允许双向的输入/输出,由istream和ostream派生出了iostream类。
类的继承关系见下图:
iostream库定义了以下三个标准流对象:
1. cin,表示标准输入(standard input)的istream类对象。cin使我们可以从设备读如数据。
2. cout,表示标准输出(standard output)的ostream类对象。cout使我们可以向设备输出或者写数据。
3. cerr,表示标准错误(standard error)的osttream类对象。cerr是导出程序错误消息的地方,它只能允许向屏幕设备写数据。
输出主要由重载的左移操作符(<<)来完成,输入主要由重载的右移操作符(>>)完成:
1. >>a表示将数据放入a对象中。
2. <<a表示将a对象中存储的数据拿出。
这些标准的流对象都有默认的所对应的设备,见下表:
cin | 键盘 | stdin | 标准输入 |
cout | 显示器屏幕 | stdout | 标准输出 |
cerr | 显示器屏幕 | stderr | 标准错误输出 |
上表中的意思表明cin对象的默认输入设备是键盘,cout对象的默认输出设备是显示器屏幕。
那么原理上C++有是如何利用cin/cout对象与左移和右移运算符重载来实现输入输出的呢?
下面我们以输出为例,说明其实现原理:
1. cout是ostream类的对象,因为它所指向的是标准设备(显示器屏幕),所以它在iostream头文件中作为全局对象进行定义。
2. ostream cout(stdout);//其默认指向的C中的标准设备名,作为其构造函数的参数使用。
3. 在iostream.h头文件中,ostream类对应每个基本数据类型都有其友元函数对左移操作符进行了友元函数的重载。
o ostream& operator<<(ostream &temp,int source);
o ostream& operator<<(ostream &temp,char *ps);
o ... 等等
一句输出语句:cout<<"www.cndev-lab.com";,事实上调用的就是ostream& operator<<(ostream &temp,char *ps);这个运算符重载函数,由于返回的是流对象的引用,引用可以作为左值使用,所以当程序中有类似cout<<"www.cndev- lab.com"<<"中国软件开发实验室";这样的语句出现的时候,就能够构成连续输出。
由于iostream库不光支持对象的输入输出,同时也支持文件流的输入输出,所以在详细讲解左移与右移运算符重载只前,我们有必要先对文件的输入输出以及输入输出的控制符有所了解。
和文件有关系的输入输出类主要在fstream.h这个头文件中被定义,在这个头文件中主要被定义了三个类,由这三个类控制对文件的各种输入输出操作,他们分别是ifstream、ofstream、fstream,其中fstream类是由iostream类派生而来,他们之间的继承关系见下图所示。
由于文件设备并不像显示器屏幕与键盘那样是标准默认设备,所以它在fstream.h头文件中是没有像cout那样预先定义的全局对象,所以我们必须自己定义一个该类的对象,我们要以文件作为设备向文件输出信息(也就是向文件写数据),那么就应该使用ofstream类。
ofstream类的默认构造函数原形为:
ofstream::ofstream(const char *filename,int mode = ios::out,int openprot = filebuf::openprot);
· filename: 要打开的文件名
· mode: 要打开文件的方式
· prot: 打开文件的属性
其中mode和openprot这两个参数的可选项表见下表:
mode属性表 | |
ios::app | 以追加的方式打开文件 |
ios::ate | 文件打开后定位到文件尾,ios:app就包含有此属性 |
ios::binary | 以二进制方式打开文件,缺省的方式是文本方式。两种方式的区别见前文 |
ios::in | 文件以输入方式打开 |
ios::out | 文件以输出方式打开 |
ios::trunc | 如果文件存在,把文件长度设为0 |
可以用“或”把以上属性连接起来,如ios::out|ios::binary。
openprot属性表 | |
属性 | 含义 |
0 | 普通文件,打开访问 |
1 | 只读文件 |
2 | 隐含文件 |
4 | 系统文件 |
可以用“或”或者“+”把以上属性连接起来 ,如3或1|2就是以只读和隐含属性打开文件。
实例代码如下:
#include <fstream>
using namespace std;
int main()
{
ofstream myfile("c://1.txt",ios::out|ios::trunc,0);
myfile<<"中国软件开发实验室"<<endl<<"网址:"<<"www.cndev-lab.com";
myfile.close()
system("pause");
}
文件使用完后可以使用close成员函数关闭文件。
ios::app为追加模式,在使用追加模式的时候同时进行文件状态的判断是一个比较好的习惯。
示例如下:
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
ofstream myfile("c://1.txt",ios::app,0);
if(!myfile)//或者写成myfile.fail()
{
cout<<"文件打开失败,目标文件状态可能为只读!";
system("pause");
exit(1);
}
myfile<<"中国软件开发实验室"<<endl<<"网址:"<<"www.cndev-lab.com"<<endl;
myfile.close();
}
在定义ifstream和ofstream类对象的时候,我们也可以不指定文件。以后可以通过成员函数open()显式的把一个文件连接到一个类对象上。
例如:
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
ofstream myfile;
myfile.open("c://1.txt",ios::out|ios::app,0);
if(!myfile)//或者写成myfile.fail()
{
cout<<"文件创建失败,磁盘不可写或者文件为只读!";
system("pause");
exit(1);
}
myfile<<"中国软件开发实验室"<<endl<<"网址:"<<"www.cndev-lab.com"<<endl;
myfile.close();
}
下面我们来看一下是如何利用ifstream类对象,将文件中的数据读取出来,然后再输出到标准设备中的例子。
代码如下:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main()
{
ifstream myfile;
myfile.open("c://1.txt",ios::in,0);
if(!myfile)
{
cout<<"文件读错误";
system("pause");
exit(1);
}
char ch;
string content;
while(myfile.get(ch))
{
content+=ch;
cout.put(ch);//cout<<ch;这么写也是可以的
}
myfile.close();
cout<<content;
system("pause");
}
上例中,我们利用成员函数get(),逐一的读取文件中的有效字符,再利用put()成员函数,将文件中的数据通过循环逐一输出到标准设备(屏幕)上, get()成员函数会在文件读到默尾的时候返回假值,所以我们可以利用它的这个特性作为while循环的终止条件,我们同时也在上例中引入了C++风格的字符串类型string,在循环读取的时候逐一保存到content中,要使用string类型,必须包含string.h的头文件。
我们在简单介绍过ofstream类和ifstream类后,我们再来看一下fstream类,fstream类是由iostream派生而来,fstream类对象可以同对文件进行读写操作。
示例代码如下:
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
fstream myfile;
myfile.open("c://1.txt",ios::out|ios::app,0);
if(!myfile)
{
cout<<"文件写错误,文件属性可能为只读!"<<endl;
system("pause");
exit(1);
}
myfile<<"中国软件开发实验室"<<endl<<"网址:"<<"www.cndev-lab.com"<<endl;
myfile.close();
myfile.open("c://1.txt",ios::in,0);
if(!myfile)
{
cout<<"文件读错误,文件可能丢失!"<<endl;
system("pause");
exit(1);
}
char ch;
while(myfile.get(ch))
{
cout.put(ch);
}
myfile.close();
system("pause");
}
由于fstream类可以对文件同时进行读写操作,所以对它的对象进行初始话的时候一定要显式的指定mode和openprot参数。
接下来我们来学习一下串流类的基础知识,什么叫串流类?
简单的理解就是能够控制字符串类型对象进行输入输出的类,C++不光可以支持C++风格的字符串流控制,还可以支持C风格的字符串流控制。
我们先看看看C++是如何对C风格的字符串流进行控制的,C中的字符串其实也就是字符数组,字符数组内的数据在内存中的位置的排列是连续的,我们通常用 char str[size]或者char *str的方式声明创建C风格字符数组,为了能让字符数组作为设备并提供输入输出操作,C++引入了ostrstream、istrstream、 strstream这三个类,要使用他们创建对象就必须包含strstream.h头文件。
· istrstream类用于执行C风格的串流的输入操作,也就是以字符串数组作为输入设备。
· ostrstream类用于执行C风格的串流的输出操作,也就是一字符串数组作为输出设备。
· strstream类同时可以支持C风格的串流的输入输出操作。
istrstream类是从istream(输入流类)和strstreambase(字符串流基类)派生而来,ostrstream是从 ostream(输出流类)和strstreambase(字符串流基类)派生而来,strstream则是从iostream(输入输出流类)和和 strstreambase(字符串流基类)派生而来。
他们的继承关系如下图所示:
串流同样不是标准设备,不会有预先定义好的全局对象,所以不能直接操作,需要通过构造函数创建对象。
类istrstream的构造函数原形如下:
istrstream::istrstream(const char *str,int size);
参数1表示字符串数组,而参数2表示数组大小,当size为0时,表示istrstream类对象直接连接到由str所指向的内存空间并以/0结尾的字符串。
下面的示例代码就是利用istrstream类创建类对象,制定流输入设备为字符串数组,通过它向一个字符型对象输入数据。代码如下:
#include <iostream>
#include <strstream>
using namespace std;
int main()
{
char *name = "www.cndev-lab.com";
int arraysize = strlen(name)+1;
istrstream is(name,arraysize);
char temp;
is>>temp;
cout<<temp;
system("pause");
}
类ostrstream用于执行串流的输出,它的构造函数如下所示:
ostrstream::ostrstream(char *_Ptr,int streamsize,int Mode = ios::out);
第一个参数是字符数组,第二个是说明数组的大小,第三个参数是指打开方式。
我们来一个示例代码:
#include <iostream>
#include <strstream>
using namespace std;
int main()
{
int arraysize=1;
char *pbuffer=new char[arraysize];
ostrstream ostr(pbuffer,arraysize,ios::out);
ostr<<arraysize<<ends;//使用ostrstream输出到流对象的时候,要用ends结束字符串
cout<<pbuffer;
delete[] pbuffer;
system("pause");
}
上面的代码中,我们创建一个c风格的串流输出对象ostr,我们将arraysize内的数据成功的以字符串的形式输出到了ostr对象所指向的pbuffer指针的堆空间中,pbuffer也正是我们要输出的字符串数组,在结尾要使用ends结束字符串,如果不这么做就有溢出的危险。
对于stringstream了来说,不用我多说,大家也已经知道它是用于C++风格的字符串的输入输出的。 stringstream的构造函数原形如下:
stringstream::stringstream(string str);
示例代码如下:
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main()
{
stringstream ostr("ccc");
ostr.put('d');
ostr.put('e');
ostr<<"fg";
string gstr = ostr.str();
cout<<gstr<<endl;
char a;
ostr>>a;
cout<<a
system("pause");
}
除此而外,stringstream类的对象我们还常用它进行string与各种内置类型数据之间的转换。示例代码如下:
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main()
{
stringstream sstr;
//--------int转string-----------
int a=100;
string str;
sstr<<a;
sstr>>str;
cout<<str<<endl;
//--------string转char[]--------
sstr.clear();//如果你想通过使用同一stringstream对象实现多种类型的转换,请注意在每一次转换之后都必须调用clear()成员函数。
string name = "colinguan";
char cname[200];
sstr<<name;
sstr>>cname;
cout<<cname;
system("pause");
}
接下来我们来学习一下输入/输出的状态标志的相关知识.
C++中负责的输入/输出的系统包括了关于每一个输入/输出操作的结果的记录信息。这些当前的状态信息被包含在io_state类型的对象中。io_state是一个枚举类型(就像open_mode一样),以下便是它包含的值。
· goodbit 无错误
· Eofbit 已到达文件尾
· failbit 非致命的输入/输出错误,可挽回
· badbit 致命的输入/输出错误,无法挽回
有两种方法可以获得输入/输出的状态信息。一种方法是通过调用rdstate()函数,它将返回当前状态的错误标记。例如,假如没有任何错误,则rdstate()会返回goodbit.下例示例,表示出了rdstate()的用法:
#include <iostream>
using namespace std;
int main()
{
int a;
cin>>a;
cout<<cin.rdstate()<<endl;
if(cin.rdstate() == ios::goodbit)
{
cout<<"输入数据的类型正确,无错误!"<<endl;
}
if(cin.rdstate() == ios_base::failbit)
{
cout<<"输入数据类型错误,非致命错误,可清除输入缓冲区挽回!"<<endl;
}
system("pause");
}
另一种方法则是使用下面任何一个函数来检测相应的输入/输出状态:
bool bad();
bool eof();
bool fail();
bool good();
下例示例,表示出了上面各成员函数的用法:
#include <iostream>
using namespace std;
int main()
{
int a;
cin>>a;
cout<<cin.rdstate()<<endl;
if(cin.good())
{
cout<<"输入数据的类型正确,无错误!"<<endl;
}
if(cin.fail())
{
cout<<"输入数据类型错误,非致命错误,可清除输入缓冲区挽回!"<<endl;
}
system("pause");
}
如果错误发生,那么流状态既被标记为错误,你必须清除这些错误状态,以使你的程序能正确适当地继续运行。要清除错误状态,需使用clear()函数。此函数带一个参数,它是你将要设为当前状态的标志值。,只要将ios::goodbit作为实参。
示例代码如下:
#include <iostream>
using namespace std;
int main()
{
int a;
cin>>a;
cout<<cin.rdstate()<<endl;
cin.clear(ios::goodbit);
cout<<cin.rdstate()<<endl;
system("pause");
}
通常当我们发现输入有错又需要改正的时候,使用clear()更改标记为正确后,同时也需要使用get()成员函数清除输入缓冲区,以达到重复输入的目的。
示例代码如下:
#include <iostream>
using namespace std;
int main()
{
int a;
while(1)
{
cin>>a;
if(!cin)//条件可改写为cin.fail()
{
cout<<"输入有错!请重新输入"<<endl;
cin.clear();
cin.get();
}
else
{
cout<<a;
break;
}
}
system("pause");
}
最后再给出一个对文件流错误标记处理的例子,巩固学习,代码如下:
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
ifstream myfile("c://1.txt",ios_base::in,0);
if(myfile.fail())
{
cout<<"文件读取失败或指定文件不存在!"<<endl;
}
else
{
char ch;
while(myfile.get(ch))
{
cout<<ch;
}
if(myfile.eof())
{
cout<<"文件内容已经全部读完"<<endl;
}
while(myfile.get(ch))
{
cout<<ch;
}
}
system("pause");
}