http://blog.csdn.net/liaoyoujinb/article/details/14639967
文件操作之不得不知的细节
说道文件操作,可能很多人都或多或少知道一些,但是你是否曾遇到一些莫名其妙的问题呢,你是否了解如何真正正确的判断是否读到文件末尾呢(!f.eof()----to young, to simple),你是否真正了解seekp()/seekg()/tellg()/tellp()呢?如果这些你都明白了,那么这篇文章你就不需要看了。但是如果你还不太明白,我们就一起探讨一下:
说道文件操作我们不得不先说说有哪些文件读取操作符或者文件读取函数。
一、文件读取方式
1.文件读取操作符“>>”:(以字节方式读)
fstream f("info.txt"); |
这个文件读取操作符的机制:
char ch; f>>ch; |
当读取一个字符串时,例如文件中有“BBBBB”,读取代码为:
while(!f.eof()) { f>>ch; cout<<c<<endl; } |
结果输出了:6个B,而实际只有5个B,这是为什么呢? |
经过多番查找资料以及验证,发现:文件读取时当刚好读取到文件最后一条记录时,文件的读指针会+1,即使+1到达了文件末尾,读函数或者操作符都不负责检查是否到达文件末尾。这就会导致当文件指针越界时iso_base::eofbit仍然没有置位依然是0,当再次进入循环读取记录时,发现无记录可读,读取失败,此时置位eofbit,(事实上此时failbit也会置位)。再次判断循环条件才会出错。所以循环会多执行一次。
那么为什么会多读出一个B呢,是不是指针回退了呢,事实并非如此,标准库不可能这么傻。看下面这段程序:
while(!f.eof()) { if (f>>ch) cout<<c<<endl; } |
结果输出了:5个B,如我们所愿。 |
这是为何呢?因为最后一次尝试读取文件末尾,读取越界,所以读取出错,而上一个例子其实其实并非读取6个B,最后一个B是上次保存在ch中的。
题外篇:">>"以字节方式读取,如int a ; f>>a;其实向a中读取一个字节,而"<<"也是以字节方式写,如int a = 98;f<<a;向文件写入字母;如果是int a = 98,f<<a; 然后cout<<a<<endl;发现结果居然对了。但是你不要以为这就万事大吉了,看下面例子:
int a = 98; f<<a; f<<a; f>>a; cout<<a<<endl; |
输出:9898 分析:说明读出整数时是先读出一个字符串,然后转换为整数。这样子才会出现上述情况。如果9898989898989898连的太多,读出就会出现不可预期的错误。 |
所以我们标准的读取必须是char或者char*或者string型方式,其他类型需要转换。
再看下面这个例子:
string str; f>>str; |
(忽略前面的空白字符)当读取一个字符串时,实际上是一个一个字符的尝试读取,当读取遇到空白或者到文件末尾(末尾时读取尝试失败,eofbit,failbit都置位1)。所以下面程序就不会出现多一次读取的情况:
while(!f.eof()) { //忽略前面的空白字符,遇到空白或尝试读取文件末尾失败 f>>str; cout<<str<<endl; } |
文件内容:Hello World! 输出: Hello World! |
这就是因为读取World!时当读到‘!’字符时文件读指针已达到文件末尾,但读取字符串工作并未完成,因为没有遇到结束符。继续尝试读取,结果读到文件尾部(读取错误,置eofbit,failbit为1)。返回已经读取的字符串(这是重载">>"做的事情);
继续看下一个例子:
while(!f.eof()) { //忽略前面的空白字符,遇到空白或尝试读取文件末尾失败 f>>str; cout<<str<<endl; } |
文件内容:Hello World!&b 说明:&b代表空白 输出: Hello World! World! |
这是为什么呢?为什么World!会输出两次呢?
原因很简单,当读完World!是并未到达文件末尾,继续尝试读取下一个字符串。然而前面的空白(字符串出现之前)会被忽略,结果读到文件末尾,尝试读取文件末尾出错。且读到0个字节。那么str不变。输出上一次结果。
所有上面这些错误都会置eofbit,failbit,以及读取返回非真。
所以最保险,最安全的代码形式应该是:
if(f>>get) cout<<get<<endl; |
对于写就没这么多讲究了,不过需要补充一句,在二进制文件的读取的read函数中也存在这个问题。另外还有一个例子:
fstream f("Info.txt"); f.seekg(0,ios_base::end); char ch; while(!f.eof()) { cout<<"in while."<<endl; if(f>>ch) cout<<ch<<endl; } |
输出: in while. |
这个也许会让我们有点小疑惑,ios_base::end不是文件末尾吗,是的,没错,但是我们需要说明的是文件指针到达文件结束并不置eofbit为1.只有尝试读取文件结束位置才置位eofbit为1(同时置位failbit为1);
小结:
1.文件读写指针到达文件末尾并不会引起ios_base::eofbit置位为1.自然f.eof()也不回返回true.只有当试图读取文件尾(结束位置)时才置ios_base::eofbit为1,同时置文件读写指针位置为-1;所以当我们判断是否读到文件末尾时,还应当判断读写是否出错。
2.以字节流读写文件时,需将数据视为char,char*,string类型,其他类型的读写必须经过必要的转换,否则会出现错误。
3.其他读写函数以此类推。
二、随机读取文件
在C++文件操作中,有四个函数是让我们自己来自由控制读写位置的,他们分别是:seekp()/seekg()/tellg()/tellp().
函数原型:
seekp:设置输出文件流的文件流指针位置 |
tellp():(p---put 写的意思)返回写指针的当前位置。
tellg():返回读指针的当前位置。
seekp():设置写指针位置。
seekg():设置读指针位置。
它们对于fstream对象来说,seekp()/seekg()等价,因为读写指针是同一个指针,而且tellg()/tellp()也等价,因为读写指针在同一个位置。而对于ifstream/ofstream就需要使用特定函数。
还有一点需要注意,seekp()/seekg()的越界问题,当函数的偏移量参数超出文件大小(如文件只有10字节大小,seekg(20))
1.首先,写在文件大小允许的情况下是没有越界这么一说的:
|
输出:pos for write:20 文件:youjin.youjin.NULNULNULNULNULNUL youjin. |
2.但是读是有越界这么一说的:
|
输出: pos for read:20 read fail. |
结论:当seekg/seekp越界时,读取失败。 注意:pos = 20; |
进一步测试:
|
输出: file status before read:0 pos for read:20 file status after read:6 |
分析:seekg(20)指针越界后,文件目前状态为0,即ios_base::goodbit.但试图读取时出错置eofbit = 1,failbit = 1; 110 = 6;注意:if(f>>get)强烈建议写上。若有循环并将多循环一次。 |
继续寻求解决方案:
|
输出: youjin. youjin. file status after read:0 |
分析:if(offset > size)break;(size文件大小) 可以有效防止seekg/seekp越界。 此时不会发生failbit,eofbit置位。 |
三、文件状态位
C++中负责的输入/输出的系统包括了关于每一个输入/输出操作的结果的记录信息。这些当前的状态信息被包含在io_state类型的对象中。io_state是一个枚举类型(就像open_mode一样),以下便是它包含的值。
ios_base::goodbit 无错误
ios_base::eofbit 已到达文件尾
ios_base::failbit 非致命的输入/输出错误,可挽回
ios_base::badbit 致命的输入/输出错误,无法挽回
这四个标记位均为bit位,标记值为0或者1,每个标记位0表示清除,1表示设置。
他们分别对应四个函数:
good():返回goodbit的值。
eof():返回eofbit的值.
fail():返回failbit的值。
bad():返回badbit的值。
cout<<ios_base::failbit<<endl; cout<<ios_base::eofbit<<endl; cout<<ios_base::badbit<<endl; cout<<ios_base::goodbit<<endl; |
输出: 4 2 1 0 |
而rdstate()返回的是这些值的叠加。
Setstate网上自己查吧。
四、同时读写
对于fstream,我们可以对文件进行同时读写操作:如下例
|
文件内容:1 输出: 1 2 Bug:没有发生写事件。 |
记过多番测试发现上一次文件读取完事后,文件状态为已标记为错误状态,即f.good()不为true,改正如下:
|
这样就对了,这说明文件并未发生致命错误。 |