C++中常见的文件读取结尾问题

  • Post author:
  • Post category:其他


在使用C/C++读文件的时候,一定都使用过eof()这个函数来判断文件是否为空或者是否读到文件结尾了,也会在使用这个函数的过程中遇到一些问题,如不能准确的判断是否为空或者是否到了文件尾,以至于有些人可能还会怀疑这个函数是不是本身在设计上就有问题。

先来看看如下这段代码:

#include <iostream>
#include <fstream>
using namespace std;
int main()
{
  char ch = 'x';
  ifstream fin("test.txt" /*, ios::binary*/);
  if (fin.eof())
  {
    cout << "file is empty."<<endl;
    return 0;
  }

  while (!fin.eof())
  {
    fin.get(ch);
    cout << ch <<endl;
  }    
  system("pause");
  return 0;
}

编译并运行以上代码,

如果test.txt不存在,程序会形成死循环,fin.eof()永远返回false,满屏幕‘x’;

如果test.txt为空,程序打印出一个x字符,

当test.txt中存在一字符串“abcd”且没有换行时,程序打印出“abcdd”,

当存在以上字符串并且有一新的空行时,程序打印出“abcd”加上一空行。

这种现象可能让很多人很迷惑,程序运行的结果似乎很不稳定,时对时错。使用binary模式读时结果一样。在这里,大家可能有一个误区,认为eof()返回true时是读到文件的最后一个字符,其实不然,

eof()返回true时是读到文件结束符0xFF,而文件结束符是最后一个字符的下一个字符。

(这里我更愿意相信是到了文件末尾时,eof()并没有返回EOF值,只有

再一次读取

才能返回EOF值,然后通过这个EOF判断是否到了末尾,那么

这再一次读取

就是上一次读取的值(就是还没有到末尾时读取的那一次的值),也就是出现两个d.(如果是结构体的话,就是上一次读取的最末尾的成员数据,然后判断返回)(也就是认为是两次都在文件末尾,第一次在文件末尾还要在读一次,第二次才能正确的返回退出,)

因此,当读到最后一个字符时,程序会多读一次(编译器会让指针停留在最后一个字符那里,然后重复读取一次,这也就是就上面最后一个字符会输出两次的原因。至于是不是所有的编译器都这样处理我就不太清楚了,我使用的VC6,vs2010似乎都是这样的((,而且如果你是一个一个的结构体存入的话,那么最后一次读取的是最后一个结构体的最后一项成员数据,而且这项数据被赋值给一个新结构体也不会报错。也就是多读一次的问题。))

问题出来了,就要找出对应的解决之道,要解决以上的问题,只需要调整一下条件语句即可:



fin.peek() == EOF



fin.get(ch)



1



while(fin.get(ch)){ //替换!fin.eof()



cout<<ch;



}



2.



while(fin.peek()!= EOF){




fin.get(ch);



cout<<ch;



}


这两种情况最后都可以输出abcd 而不是abcde  。。

再来看一下另外一种情况:

#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main()
{
 string str;
 ifstream fin("test.txt"/*, ios::binary*/);
 if (fin.peek() == EOF)
 {
  cout << "file is empty."<<endl;
  return 0;
 }
 while (!fin.eof())
 {
  fin >> str;
  cout<< str<<endl;
 }    
 system("pause");
 return 0;
}

上述代码在VS2010下编译运行,发现,当文件结尾没有空行时,结果正确,当结尾有空行时,最后一个字符串将被重复输出一次, 而VC6的情况则有所不同,没有空行时,正确输出。有空行时,没有重复输出,但输出了一个空行。

因此,为了保证在不同的编译器下得到一致的我们期望的结果,将条件语句做一下修改:



fin >> str



综上所述,我们可以得到以下结论:

1. 判断文件是否为空时使用peek函数,若peek返回EOF则文件为空;

2. 读取文件过程中,读取非char型时,使用peek判断文件尾将不再适用,循环判断条件应改用>>操作符进行读取,若读入char型缓冲区,peek函数会表现得很好。






也即是只做一次读取就做判断,不进行二次读取然后判断(peek)

那么

如果我是读取结构体类型的数据呢?(用容器存储流中的数据)

#include <iostream>
#include <string>
#include <fstream>
#include <assert.h>
#include <vector>
using namespace std;
struct person{
	char Icn[10];
	char Name[20];
	char Adr[20];
	int age;
	char* getName() {return Name;}
	friend istream& operator>>(istream& in,person& p);
	friend ostream& operator<<(ostream& out,person& p);
};
istream& operator>>(istream& in,person& p) {
	in>>p.Icn>>p.Name>>p.Adr>>p.age;//人为的空格。in输入时以空格为界。
	return in;
}
ostream& operator<<(ostream& out,person& p) {
	out<<p.Icn<<" "<<p.Name<<" "<<p.Adr<<" "<<p.age;
	return out;
}

class Database {
public:
	size_t query(size_t ssn);
	//person retrieve(size_t rn) {return retrieve(rn,file);};
	void add(const person& p);
private:
	ifstream file;
	//person retrieve(size_t,ifstream&);
};

/*person Database::retrieve(size_t rn,ifstream& file){
	person per;
	for(size_t i=0;i<rn;i++){
		file.read(reinterpret_cast<char*>(&per),sizeof(per));
	}
	return per;
}*/

person retrieve(size_t size,ifstream& f){// 如果不加 & ,会出现error2048 之类的错误。
		person per;
		for(size_t i=0;i<size;i++){
			f>>per;
			f.ignore(1,'\n');
		}
		return per;
	}
int personNum(ifstream& f){
	int k=0;
	person per;
	while(!f.eof()){
		f>>per;
		f.ignore(1,'\n');
		k++;
	}
	return k-1;//这里需要减去 1 
}
void personNum(ifstream& f,vector<person>& v){
	person per;
	while(!f.eof()){
		f>>per;
		v.push_back(per);//如果到了文件末尾还要再次发生读取才能将设置为EOF,那么最后已从到vector中的是什么数据
	}
}

int main() {
	Database d;
	ofstream out("data.txt",ios::app);
	assert(out);
	int num ,i,number;
	person per;
	cout<<"请输入要存入的人数:";
	cin>>num;cout<<endl;
	if(num>0){
		cout<<"请输入他们的信息(编号,姓名,地址,年龄):"<<endl;
		for(i=0;i<num;i++){
			cin>>per;
	    	out<<per<<endl;
		}
	}
	cout<<endl;
	//out.close();//这一步关闭输出文件流。

	ifstream in("data.txt");
	assert(in);
	    //(1...)cout<<in.rdbuf();//如果我此时一次全部输出,那么personnum应该也不存在?是这样吗?
	                 //答案是肯定的,从而我就确定,文件流中的数据无论以任何一种方式被读取
	                 //赋值,那么就不存在了。至少被读取的部分不存在了。最后personnum=0;
	int personnum=personNum(in);//这里文件流已经到了文件末尾。
	cout<<"文件中的人数:"<<personnum<<endl;

	/*
	in.seekg(0,ios::beg);//1重新定义文件流 和 2用原来的文件流 进行操作,结果会怎样?
	                     //问题是本文件流中的数据在计算个数的时候已经被读取了。
	                     //同时流缓冲区不会被复制,也就是没有拷贝的复制构造函数,那么
	                     //说把缓冲区中的数据暂时存放起来就不能用流对象实现,可以用容器
	                     //实现,但是用容器暂时存储数据的时候,只是简单地push_back()
	                     //那么你读取的时候会不会只读一个person对象,其他的数据留作下一次
	                     //下一次读取(还是说一次整个的读取,出错。虽然vector的存储类型是person)
	   // 也就是说用原来的文件流进行操作是不行的。需要重新定义文件流,以从中取得数据
	//in.close();

	//ifstream in1("data.txt");
	//assert(in1);
	cout<<"文件中的全部信息如下:"<<endl;
	for(i=0;i<personnum;i++){
		in>>per;
		in.ignore(1,'\n');
		cout<<i+1<<" : "<<per<<endl;
	}
	cout<<endl;
	   //(3....)cout<<"通过rdbuf()得到结果如下:\n"<<in1.rdbuf()<<endl;//如果已经进行了上面的操作,
	                //那么这一步会出现什么结果?答案是什么也没有,程序没有出错,但是
	                //已经不能继续往下运行。
	in.close();
	*/

	vector<person> ver;// (2...)通过容器暂时存储文件流中读取到的数据
	ifstream fin("data.txt");
	assert(fin);
	personNum(fin,ver);
	int counter =1;
	for(vector<person>::iterator it=ver.begin();it!=ver.end();it++,counter++)
		cout<<counter<<" : "<<*it<<endl;
	cout<<endl;
	fin.close();

	ifstream f("data.txt");
	int flags=1;
	while(flags){
	f.seekg(0,ios::beg);
	cout<<"请输入你要找第几个人:";
	cin>>number;cout<<endl;
	per=retrieve(number,f);
	cout<<per<<endl;
	cout<<"Name:"<<per.getName()<<endl;
	cout<<"是否继续查找:(是--1,不是--0)";
	cin>>flags;cout<<endl;
	}
	return 0;
}

我data.txt中的数据如下:

2011 李小薇 武汉 21

2012 何志强 武汉 21

2011 杨文俊 武汉 20

2011 卢伟 武汉 21

2011 张军 武汉 20

2011 黄金定 武汉 21

2011 秦宇飞 武汉 20

显然这里是7个人,而且最后一次(反复说的到真正到文件末尾EOF之前读取)多读取的就是 正确读取的最后一次(秦宇飞 那一次)的最后一项数据20 。容器中的体现就是第八次 显示  20  (但是没有其他的任何数据)。。

2.。如果是重新创建流对象来实现的话

#include <iostream>
#include <string>
#include <fstream>
#include <assert.h>
using namespace std;
struct person{
	char Icn[10];
	char Name[20];
	char Adr[20];
	int age;
	char* getName() {return Name;}
	friend istream& operator>>(istream& in,person& p);
	friend ostream& operator<<(ostream& out,person& p);
};
istream& operator>>(istream& in,person& p) {
	in>>p.Icn>>p.Name>>p.Adr>>p.age;//人为的空格。in输入时以空格为界。
	return in;
}
ostream& operator<<(ostream& out,person& p) {
	out<<p.Icn<<" "<<p.Name<<" "<<p.Adr<<" "<<p.age;
	return out;
}

class Database {
public:
	size_t query(size_t ssn);
	//person retrieve(size_t rn) {return retrieve(rn,file);};
	void add(const person& p);
private:
	ifstream file;
	//person retrieve(size_t,ifstream&);
};

/*person Database::retrieve(size_t rn,ifstream& file){
	person per;
	for(size_t i=0;i<rn;i++){
		file.read(reinterpret_cast<char*>(&per),sizeof(per));
	}
	return per;
}*/

person retrieve(size_t size,ifstream& f){
		person per;
		for(size_t i=0;i<size;i++){
			f>>per;
			f.ignore(1,'\n');
		}
		return per;
	}
int personNum(ifstream& f){
	int k=0;
	person per;
	while(!f.eof()){
		f>>per;
		f.ignore(1,'\n');
		k++;
	}
	return k-1;
}
int main() {
	Database d;
	ofstream out("data.txt",ios::app);
	assert(out);
	int num ,i,number;
	person per;
	cout<<"请输入要存入的人数:";
	cin>>num;cout<<endl;
	cout<<"请输入他们的信息(编号,姓名,地址,年龄):"<<endl;
	for(i=0;i<num;i++){
		cin>>per;
		out<<per<<endl;
	}
	cout<<endl;
	out.close();

	ifstream in("data.txt");
	assert(in);
	int personnum=personNum(in);//这里文件流已经到了文件末尾。
	//in.seekg(0,ios::beg);
	in.close();

	ifstream in1("data.txt");
	assert(in1);
	cout<<"文件中的全部信息如下:"<<endl;
	for(i=0;i<personnum;i++){
		in1>>per;
		in1.ignore(1,'\n');
		cout<<i+1<<" : "<<per<<endl;
	}
	cout<<endl;
	//cout<<"通过rdbuf()得到结果如下:\n"<<in1.rdbuf()<<endl;//如果已经进行了上面的操作,
	                //那么这一步会出现什么结果?答案是什么也没有,程序没有出错,但是
	                //已经不能继续往下运行。
	in1.close();

	ifstream f("data.txt");
	int flags=1;
	while(flags){
	f.seekg(0,ios::beg);
	cout<<"请输入你要找第几个人:";
	cin>>number;cout<<endl;
	per=retrieve(number,f);
	cout<<per<<endl;
	cout<<"Name:"<<per.getName()<<endl;
	cout<<"是否继续查找:(是--1,不是--0)";
	cin>>flags;cout<<endl;
	}
	return 0;
}

如果我没有将最后一次 多读取的减去 就是确定文件中人数的时候

int personNum(ifstream& f){
	int k=0;
	person per;
	while(!f.eof()){
		f>>per;
		f.ignore(1,'\n');
		k++;
	}
	return k-1;
}

没有 -1,而是直接return k .那么结果也和上面容器实现的一样。