C++文件操作
变量中的数据保存在内存中,是临时的;文件数据保存在外存储器上,用于永久的保存大量的数据。根据数据的组织形式,文件分为文本文件和二进制文件。文本文件的每个字节存放一个ASCII代码,代表一个字符;二进制文件把内存中的数据,按照其在内存中的存储形式原样写到磁盘上存放。
C++把每个文件看成一个有序的字节流(字符流或二进制流)。每个文件不是以文件结束符结束,就是以在系统维护和管理的数据结构中特定的字符结束。文件打开时,就会创建一个对象,将这个对象和某个流关联起来。C++中,要进行文件的输入/输出,必须首先创建一个流,与文件关联,才能进行读写操作。
C++通过以下几个类都支持文件的输入输出:
以下几个流类都是头文件fstream需要#include<fstream>
ofstream:写操作(输出)的文件类(由ostream引申出来),将文件从内存中写入存储的设备(相当于本地磁盘)
ifstream:读操作(输入)的文件类(由istream引申出来),将文件从存储设备加载到内存中
fstream:同时读写操作的文件类(由iostream引申出来)
使用这三个类时,程序中需要包含fstream头文件。C++中类库流如图:
在这个头文件中主要被定义了三个类,由这三个类控制对文件的各种输入输出操 作,他们分别是ifstream、ofstream、fstream,其中fstream类是由iostream类派生而来,他们之间的继承关系见下图所示。
ifstream 类和 fstream 类是从 istream 类派生而来的,因此 ifstream 类拥有 istream 类的全部成员函数。同样地,ofstream 和 fstream 类也拥有 ostream 类的全部成员函数。这三个类中有一些十分熟悉的成员函数可以使用,如operator <<、 operator >>、peek、ignore、getline、get 等。
在程序中,要使用一个文件,先要打开文件后才能读写,读写完后要关闭。创建一个新文件也要先执行打开(open)操作,然后才能往文件中写入数据。C++ 文件流类有相应的成员函数来实现打开、读、写、关闭等文件操作。指明文件的使用方式。使用方式有只读、只写、既读又写、在文件末尾添加数据、以文本方式使用、以二进制方式使用等多种。
一、打开文件
无论是哪种方式操作文件,都要先创建文件流类的一个对象(这些类的一个实例),然后将这个对象与文件联系起来,也就是打开一个文件,被打开的文件在程序中由一个流对象(stream object)来表示,而对这个流对象所做的任何输入输出操作实际就是对该文件的操作。
使用fstream类的成员函数open()实现文件的打开,其函数原型为:
void open (const char * filename, openmode mode);
void open (const char * filename, openmode mode,int access);
filename是指向文件名的指针,代表要打开的文件名;mode是打开文件的方式;access打开文件的属性(基本上很少用到,使用第一种函数原型就可以)。
文件的打开模式标记代表了文件的使用方式,这些标记可以单独使用,也可以组合使用。表 1 列出了各种模式标记单独使用时的作用,以及常见的两种模式标记组合的作用。
文件打开模式标记
模式标记 | 适用对象 | 作用 |
---|---|---|
ios::in | ifstream fstream | 打开文件用于读取数据。如果文件不存在,则打开出错。 |
ios::out | ofstream fstream | 打开文件用于写入数据。如果文件不存在,则新建该文件;如 果文件原来就存在,则打开时清除原来的内容。 |
ios::app | ofstream fstream | 打开文件,用于在其尾部添加数据。如果文件不存在,则新建该文件。 |
ios::ate | ifstream | 打开一个已有的文件,并将文件读指针指向文件末尾(读写指 的概念后面解释)。如果文件不存在,则打开出错。 |
ios:: trunc | ofstream | 单独使用时与 ios:: out 相同。 |
ios::binary | ifstream ofstream fstream | 以二进制方式打开文件。若不指定此模式,则以文本模式打开。 |
ios::in | ios::out | fstream | 打开已存在的文件,既可读取其内容,也可向其写入数据。文件刚打开时,原有内容保持不变。如果文件不存在,则打开出错。 |
ios::in | ios::out | ofstream | 打开已存在的文件,可以向其写入数据。文件刚打开时,原有内容保持不变。如果文件不存在,则打开出错。 |
ios::in | ios::out | ios::trunc | fstream | 打开文件,既可读取其内容,也可向其写入数据。如果文件本来就存在,则打开时清除原来的内容;如果文件不存在,则新建该文件。 |
ios::binary 可以和其他模式标记组合使用,例如:
ios::in | ios::binary表示二进制模式,读取的方式打开文件
ios::out | ios::binary 表示二进制模式,写入的方式打开文件
打开文件的属性取值为:
0 | 普通文件,打开访问 |
---|---|
1 | 只读文件 |
2 | 隐含文件 |
4 | 系统文件 |
同样可以用”或”操作符(|)或者“+”将以上属性连接起来,如1|2就是以只读和隐含属性打开文件。
文本方式与二进制方式打开文件的区别很小:
在UNIX/Linux平台中,用文本方式或者二进制方式打开文件没有任何区别
在UNIX/Linux平台中,文本文件以\n(ASCII码为0x0a)作为换行符号,而是Windows平台中,文本文件以连在一起的\r\n(、r的ASCII码是0x0d)作为换行符号。
在Windows平台中,如果以文本方式打开文件,当读取文件时,系统会将文件中所有的\r\n转换成一个字符\n,如果文件中有连续的两个字节是0x0d0a,则系统会丢弃前面的0x0d这个字节,只读0x0a。当写入文件时,系统会将\n装换成\r\n写入。
如果要写入的内容中有字节为 0x0a,则在写人该字节前,系统会自动先写入一个 0x0d。因此,如果用文本方式打开二进制文件进行读写,读写的内容就可能和文件的内容有出入。
在流对象上执行 open 成员函数,给出文件名和打开模式,就可以打开文件。判断文件打开是否成功,可以看“对象名”这个表达式的值是否为 true,如果为 true,则表示文件打开成功。
写入文件步骤:
创建一个ofstream对象
将该对象和相关的文件对应起来,也就是打开文件,使用open函数
使用<<往里面写东西
使用close()关闭对象。
注意:以这种方法打开的文件,如果没有这个文件,会创建一个。如果有这个文件,会首先清空他!!后面会将如何追加写入
读取文件操作:
创建一个ifstream对象
将该对象和相关的文件对应起来,也就是打开文件,使用open函数
使用get()函数或者getline()获取内容
使用close()关闭对象
注意:关于读取内容时,我们通常是将内容读取出来赋值给另外一个字符或者字符串,如果想要赋值给一个字符,可以用下面的形式来打印内容
while(file2.get(a))//因为get(a)只要不是末尾就返回true
{
cout << a;
}
也可以读取一排:
char buff[1000];
file2.getline(buff,100);
文件打开将文件与一个流对象建立关系,一般是用成员函数open打开文件的,若文件存在则打开文件,否则建立文件
文件流对象.open(文件名,打开方式);
out.open("test.dat",ios::out);
由于输入输出都有自己的默认的形式
ifstream 的默认形式是 ios::in
ofstream 的默认形式是 ios::out
所以可以简写成为
out.open("test.dat");
文件的多个打开方式
frestream mystream;
mystream.open("test.dat",ios::in | ios::out | ios::binary);
定义对象时,调用构造函数打开文件
ofstream out("test.dat");
等价于
ofstream out;
out.open("test.dat");
说明 文件打开失败的时候 与文件联系的流对象的值将会是0
if(!out){
cout<<"connot open file!\n";
}
文件关闭
out.close();
三、读写文件
1.文本文件
一般来说,我们将使用这些类与同控制台(console)交互同样的成员函数(cin 和 cout)来进行输入输出。如下面的例题所示,我们使用重载的插入操作符<<:
#include<iostream>
#include<fstream>
using namespace std;
int main(){
ofstream out("test.txt");
if(!out)
return false;
else{
out<<"This is a line.\n";
out<<"This is a another\n";
out.close();
}
return 0;
}
文件里现在的内容是:
This is a line.
This is another line.
从文件中读入数据也可以用与cin的使用同样的方法:
#include<iostream>
#include<fstream>
using namespace std;
int main(){
char buffer[256];
ifstream in("test.txt");
if(!in.is_open()){
cout<<"Error opening file";
exit(1);
}
while(!in.eof()){
in.getline(buffer,100);
cout<<buffer<<endl;
}
return 0;
}
将从文件中逐行读取文件内容,直至文件的末尾,然后将其在屏幕上显示出来。我们使用了一个新的成员函数叫做eof() ,它是ifstream 从类 ios中继承过来的,当到达文件末尾时返回true 。
状态标志符的验证
除了eof()以外,还有一些验证的状态的成员函数(所有都返回bool型返回值),调用方式为
xxstream s;
s.bad();
bad()
如果读写过程中出错,返回true,例如:当我们要对一个不是打开为写状态的文件进行写入时,或者我们要写入的设备没有剩余空间的时候。
fail()
除了与bad() 同样的情况下会返回 true 以外,加上格式错误时也返回true ,例如当想要读入一个整数,而获得了一个字母的时候。
eof()
如果读文件到达文件末尾,返回true
good()
如果调用以上任何一个函数返回true 的话,此函数返回 false 。
要想重置以上成员函数所检查的状态标志,你可以使用成员函数clear(),没有参数。
rdstate()
返回流的当前状态标志字
clear(strm::iostate flag)
将流的状态设置为flag,clear()函数无参数时,清除错误信息,并将重置所以状态标志
I/O对象在任意时候都对应一种状态:比如有效状态(还没处理或者正确处理完毕时状态),比如数据流被破坏(文件错误)等等。)流的状态主要是由状态标记位表示,状态标记位有几个常量:goodbit、eofbit、failbit、badbit,分别对应不同的流状态,这几个标志位常量分别由以上的几种流状态验证函数置位。
strm::iostate // 机器相关的整型名,由各个iostream类定义,用于定义条件状态
strm::goodbit // strm::iostate类型的值,用于指出正常的流
strm::badbit // strm::iostate类型的值,用于指出被破坏的流
strm::failbit // strm::iostate类型的值,用于指出失败的IO操作
strm::eofbit // strm::iostate类型的值,用于指出流已经到达文件结束符
状态标记位常量 | 含义 | good() | eof() | fail() | bad() |
ios::goodbit | 没有错误 | 1 | 0 | 0 | 0 |
ios::eofbit | 已到达文件尾 | 0 | 1 | 0 | 0 |
ios::failbit | I/O流出现非致命错误(读数字遇到字母),流可继续使用,可挽回 | 0 | 0 | 1 | 0 |
ios::badbit | I/O流发生了(或许是物理上)致命性错误,流不可继续使用,不可挽回 | 0 | 0 | 1 | 1 |
如果对应的标记位被置位,则返回1;当到达文件的结束位置时,eofbit 和 failbit 都会被置位;当badbit被置位时,fail()也会返回1,所以使用good()和fail()是确定流能否使用的正确方法,流当做条件使用的代码就等价于(!xx.fail()),而且eof() 和bad() 操作只能表示特定的错误。
为提高程序的可靠性,应在程序中检测I/O流的操作是否正常。当检测到流操作出现错误时,可以通过异常处理来解决问题。
#include<iostream>
#include<sstream> //stringstream流的头文件
#include<fstream>
using namespace std;
int main(){
string str;
ifstream in;
//保证ifstream对象可以抛出异常
/*failbit和badbit均是用来检测流的状态(flags)是否正常
failbit是逻辑错误,badbit是读写错误,其返回值是true或false*/
in.exceptions(ifstream::failbit | ifstream::badbit);
try{
in.open("test.txt");
//读取文件的缓冲内容到数据流中
stringstream strStream;
/*缓冲区类streambuf,供输入输出流对象使用,每个标准C++输入输出对象,都包含一根指向streambuf的指针,通过调用成员函数rdbuf()获取指针,直接访问底层streambuf对象,进行数据的直接读写,跳过上层的格式化输入输出操作,对于文件流,字符串流类,都可以使用*/
strStream<<in.rdbuf();
in.close();
str=strStream.str();
}
catch(ifstream::failure e){
cout<<"ERROR::SHADER::File not succesfully read"<<endl;
}
}
获得和设置流指针
所有输入/输出流对象(i/o streams objects)都有至少一个流指针:
ifstream, 类似istream, 有一个被称为get pointer的指针,指向下一个将被读取的元素。ofstream, 类似 ostream, 有一个指针 put pointer ,指向写入下一个元素的位置。fstream, 类似 iostream,同时继承了get 和 put 。我们可以通过使用以下成员函数来读出或配置这些指向流中读写位置的流指针:
tellg()和tellp()
这两个成员函数不用传入参数,返回pos_type 类型的值(根据ANSI-C++ 标准) ,就是一个整数(一般设为long int),代表当前get 流指针的位置 (用tellg) 或 put 流指针的位置(用tellp).
seekg()和seekp()
这对函数分别用来改变流指针get 和put的位置。两个函数都被重载为两种不同的原型:
seekg ( pos_type position );
seekp ( pos_type position );
使用这个原型,流指针被改变为指向从文件开始计算的一个绝对位置。要求传入的参数类型与函数 tellg 和tellp 的返回值类型相同。
seekg ( off_type offset, seekdir direction );
seekp ( off_type offset, seekdir direction );
使用这个原型可以指定由参数direction决定的一个具体的指针开始计算的一个位移(offset)。它可以是:、
ios::beg | 从流开始位置计算的位移 |
---|---|
ios::cur | 从流指针当前位置开始计算的位移 |
ios::end | 从流末尾处开始计算的位移 |
流指针 get 和 put 的值对文本文件(text file)和二进制文件(binary file)的计算方法都是不同的,因为文本模式的文件中某些特殊字符可能被修改。由于这个原因,建议对以文本文件模式打开的文件总是使用seekg 和 seekp的第一种原型,而且不要对tellg 或 tellp 的返回值进行修改。对二进制文件,你可以任意使用这些函数,应该不会有任何意外的行为产生。
以下例子使用这些函数来获得一个二进制文件的大小:
#include<iostream>
#include<fstream>
const char * name="test.txt";
int main(){
long l,m;
ifstream in(name,ios::in | ios::binary);
l=in.tellg();
in.seekg(0,ios::end);
m=in.tellg();
in.close();
cout<<"size of"<<name;
cout<<"is"<<(m-l)<<"bytes.\n";
return 0;
}
2.二进制文件
在二进制文件中,使用插入器<< 和析取器>>,以及函数(如getline)来操作符输入和输出数据,没有什么实际意义,虽然它们是符合语法的,向二进制文件输入输出数据有几种函数形式:
一、put()函数
put()函数向流写入一个字符,其原型是ofstream &put(char ch),使用也比较简单,如file1.put(‘c’);就是向流写一个字符’c’
二、get()函数
get()函数比较灵活,有3种常用的重载形式:
1、一种就是和put()对应的形式:ifstream &get(char &ch);功能是从流中读取一个字符,结果保存在引用ch中,如果到文件尾,返回空字符。如file2.get(x);表示从文件中读取一个字符,并把读取的字符保存在x中。
2、另一种重载形式的原型是: int get();这种形式是从流中返回一个字符,如果到达文件尾,返回EOF,如x=file2.get();和第一种形式的功能是一样的。
3、还有一种形式的原型是:ifstream &get(char *buf,int num,char delim=‘n’);这种形式把字符读入由 buf 指向的数组,直到读入了 num 个字符或遇到了由 delim 指定的字符,如果没使用 delim 这个参数,将使用缺省值换行符’\n’。例如:file2.get(str1,127,‘A’); 从文件中读取字符到字符串str1,当遇到字符’A’或读取了127个字符时终止。
三、文件流包括两个为顺序读写数据特殊设计的成员函数:write 和 read。第一个函数 (write) 是ostream 的一个成员函数,都是被ofstream所继承。而read 是istream 的一个成员函数,被ifstream 所继承。类 fstream 的对象同时拥有这两个函数。它们的原型是:
write ( char * buffer, streamsize size );
read ( char * buffer, streamsize size );
这里 buffer 是一块内存的地址,用来存储或读出数据。参数size 是一个整数值,表示要从缓存(buffer)中读出或写入的字符数(size值很重要,因为二进制文件内容没有行的概念(’\n’),字节之间是紧挨着的)。
//读取二进制文件
#include <iostream>
#include <fstream>
using namespace std;
int main ()
{
const char* filename = "test.txt";
char* buffer;
long size;
ifstream file(filename, ios::in|ios::binary|ios::ate);//读取到文件尾
size = file.tellg();//确定文件的大小
file.seekg(0, ios::beg);//重新将文件流指针置于文件开始的位置
buffer = new char [size];
file.read(buffer, size);
file.close();
cout <<"the complete file is in a buffer";
delete[] buffer;
return 0;
}
//向二进制文件写入数据
#include <iostream>
#include <fstream>
using namespace std;
int main ()
{
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();
}
四、缓存和同步
当我们对文件流进行操作的时候,它们与一个streambuf 类型的缓存(buffer)联系在一起。这个缓存(buffer)实际是一块内存空间,作为流(stream)和物理文件的媒介(也称为缓冲区)。例如,对于一个输出流, 每次成员函数put (写一个单个字符)被调用,这个字符不是直接被写入该输出流所对应的物理文件中的,而是首先被插入到该流的缓存(buffer)中。
缓冲分为:全缓冲、行缓冲和不带缓冲
1.全缓冲
在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。全缓冲的典型代表是对磁盘文件的读写。
2.行缓冲
在这种情况下,当在输入和输出中遇到换行符时,执行真正的I/O操作,这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的I/O操作,典型代表是键盘输入数据。
3.不带缓冲
也就是不进行缓冲,标准出错情况stderr是典型代表,这使得出错信息可以直接尽快的显示出来。
当缓存被排放出来(flush)时,它里面的所有数据或者被写入物理媒质中(如果是一个输出流的话),或者简单的被抹掉(如果是一个输入流的话)。这个过程称为同步(synchronization),它会在以下任一情况下发生:
当文件被关闭时: 在文件被关闭之前,所有还没有被完全写出或读取的缓存都将被同步。
当缓存buffer 满时:缓存Buffers 有一定的空间限制,当缓存满时,它会被自动同步。控制符明确指明:当遇到流中某些特定的控制符时,同步会发生, 这些控制符包括:flush 和endl。cout<<flush;(cout<<endl ; 等价于 cout<<”\n” <<flush ; )
明确调用函数sync(): 调用成员函数sync() (无参数)可以引发立即同步。这个函数返回一个int 值,等于-1 表示流没有联系的缓存或操作失败。
在状态标志符的验证一节中的示例代码中出现了rdbuf()函数,其就是指向文件流对象的steambuf缓冲的指针,可以通过插入器(<<)直接将数据输入一个stringstream流对象中(重载操作符<<,以streambuf指针为参数),直接进行底层的数据读写,跳过了上层的格式化输入输出,提高了读写效率。字符串流stringstream也同样可以使用。
string str;
ifstream in("Hello.txt",ios::in);
stringstream strstrm;
strstrm<<in.rdbuf();
cout<<in.rdbuf();//直接输出到屏幕显示
in.close();
str=strstrm.str();
参考文献:https://blog.csdn.net/shs1992shs/article/details/83043522