Loading

C++ 基础系列——文件操作

一、C++文件类及用法

C++ 标准库提供了 3 个类用于实现文件操作,它们统称为文件流类,这 3 个类分别为:

  • ifstream:专用于从文件读取数据
  • ofstream:专用于向文件写入数据
  • fstream:可读可写

这三个文件流类都位于 fstream 头文件中

  • fstream 类拥有 istream、ostream 类的全部成员方法。
  • fstream 头文件中并没有定义可直接使用的 fstream、ifstream 和 ofstream 类对象

fstream 类常用成员方法

成员方法名 适用类对象 功能
open() fstream 打开指定文件,使其与文件流对象关联
is_open() ifstream 检查指定文件是否已打开。
close() ofstream 关闭文件,切断和文件流对象的关联。
swap() ofstream 交换 2 个文件流对象。
operator>> (i)fstream 重载 >> 运算符,用于从指定文件中读取数据。
gcount() (i)fstream 返回上次从文件流提取出的字符个数。该函数常和 get()、getline()、ignore()、 peek()、read()、readsome()、putback() 和 unget() 联用。
get() (i)fstream 从文件流中读取一个字符,同时该字符会从输入流中消失。
getline(str,n,ch) (i)fstream 从文件流中接收 n-1 个字符给 str 变量,当遇到指定 ch 字符时会停止读取,默 认情况下 ch 为 '\0'。
ignore(n,ch) (i)fstream 从文件流中逐个提取字符,但提取出的字符被忽略,不被使用,直至提取出 n 个 字符,或者当前读取的字符为 ch。
peek() (i)fstream 返回文件流中的第一个字符,但并不是提取该字符。
putback(c) (i)fstream 将字符 c 置入文件流(缓冲区)。
operator<< (o)fstream 重载 << 运算符,用于向文件中写入指定数据。
put() (o)fstream 向指定文件流中写入单个字符。
write() (o)fstream 向指定文件中写入字符串。
tellp() (o)fstream 用于获取当前文件输出流指针的位置。
seekp() (o)fstream 设置输出文件输出流指针的位置。
flush() (o)fstream 刷新文件输出流缓冲区。
good() (i/o)fstream 操作成功,没有发生任何错误。
eof() (i/o)fstream 到达输入末尾或文件尾。
const string url = "asdfasdfasasdf";

//创建一个 fstream 类对象
fstream fs;
//将 test.txt 文件和 fs 文件流关联
fs.open("test.txt", ios::out);
//向 test.txt 文件中写入 url 字符串
fs.write(url,30);
fs.close();

二、open开打模式

打开文件目的:

  • 通过指定文件名,建立起文件和文件流对象的关联
  • 指明文件的使用方式。使用方式有只读、只写、既读又写、在文件末尾添加数据、以文本方式使用、以二进制 方式使用等多种。

打开文件可以通过两种方式:

  • 调用流对象的 open 成员函数打开文件。
  • 定义文件流对象时,通过构造函数打开文件。

void open(const char* szFileName, int mode)

  • 第一个参数:指向文件名的指针
  • 第二个参数:打开模式

文件打开模式

模式标记 适用对象 作用
ios::in (i)fstream 打开文件用于读取数据。如果文件不存在,则打开出错。
ios::out (o)fstream 打开文件用于写入数据。如果文件不存在,则新建该文件;如果文件原来就存 在,则打开时清除原来的内容。
ios::app (o)fstream 打开文件,用于在其尾部添加数据。如果文件不存在,则新建该文件。
ios::ate (i)fstream 打开一个已有的文件,并将文件读指针指向文件末尾(读写指 的概念后面解 释)。如果文件不存在,则打开出错。
ios::trunc (o)fstream 打开文件时会清空内部存储的所有数据,单独使用时与 ios::out 相同。
ios::binary (i/o)fstream 以二进制方式打开文件。若不指定此模式,则以文本模式打开。
ios::in | ios::out fstream 打开已存在的文件,既可读取其内容,也可向其写入数据。文件刚打开时,原有内容保持不变。如果文件不存在,则打开出错。
ios::in | ios::out ofstream 打开已存在的文件,可以向其写入数据。文件刚打开时,原有内容保持不变。如果文件不存在,则打开出错。
ios::in | ios::out | ios::trunc fstream 打开文件,既可读取其内容,也可向其写入数据。如果文件本来就存在,则打 开时清除原来的内容;如果文件不存在,则新建该文件。

ios::binary 可以和其他模式标记组合使用,如 ios::in| ios::binary。

定义流对象时,在构造函数中给出文件名和打开模式也可以打开文件。以 ifstream 类为例,它有如下构造函数:

ifstream::ifstream (const char* szFileName, int mode = ios::in, int);

  • szFileName:文件名指针
  • mode:打开文件模式,默认 ios::in
  • 极少使用
ifstream inFile("c:\\tmp\\test.txt", ios::in);
if (inFile) 
    inFile.close();
else 
    cout << "test.txt doesn't exist" << endl;

ofstream oFile("test1.txt", ios::out);
if (!oFile) 
    cout << "error 1"; 
else 
    oFile.close();

fstream oFile2("tmp\\test2.txt", ios::out | ios::in); 
if (!oFile2)
    cout << "error 2"; 
else
    oFile.close();

三、文本打开方式和二进制打开方式

文本文件通常用来保存肉眼可见的字符,比如 .txt 文件、.c 文件、.dat 文件等,文本文件中通常采用 ASCII、UTF-8、GBK 等字符编码,文本编辑器可以识别出这些编码格式,并将编码值转换成字符展示出来。

二进制文件通常用来保存视频、图片、程序等不可阅读的内容,用文本编辑器打开这些文件,会看到一堆乱码。

文本方式和二进制方式并没有本质上的区别,只是对于换行符的处理不同。

  • 在 UNIX/Linux 平台中,用文本方式或二进制方式打开文件没有任何区别,因为文本文件以 \n(ASCII 码为 0x0a)作为换行符号。
  • Windows 平台上,文本文件以连在一起的 \r\n 作为换行符号。如果以文本方式打开文件,当读取文件时, 程序会将文件中所有的 \r\n 转换成一个字符 \n。
  • 同样当写入文件时,程序会将 \n 转换成 \r\n 写入。

总的来说,Linux 平台使用哪种打开方式都行;Windows 平台上最好用 "ios::in | ios::out" 等打开文本文件, 用 "ios::binary" 打开二进制文件。但无论哪种平台,用二进制方式打开文件总是最保险的。

四、close() 文件方法

调用 close() 方法关闭已打开的文件,可以理解为是切断文件流对象和文件之间的关联。注意,close() 方法的功能仅是切断文件流与文件之间的关联,该文件流并会被销毁,其后续还可用于关联其它的文件。

close() 方法语法格式:

void close();

实际上,当文件流对象的生命周期结束时,会自行调用其析构函数,该函数内部在销毁对象之前,会先调用 close() 方法切断它与任何文件的关联,最后才销毁它。但是一定要手动调用 close() 方法关闭,避免出现未知错误,如读写文件失败。

如果不想频繁地打开/关闭文件,可以使用 flush() 方法及时刷新输出流缓冲区,也能起到防止写入文件失败的作用。

C++ 中使用 open() 打开的文件,在读写操作执行完毕后,应及时调用 close() 方法关闭文件,或者对 文件执行写操作后及时调用 flush() 方法刷新输出流缓冲区。

五、文本文件读写操作详解

  • 文件中存储的数据并没有类型上的分别,统统都是字符。所谓以文本形式读/写文件,就是直白地 将文件中存储的字符(或字符串)读取出来,以及将目标字符(或字符串)存储在文件中。
  • 以二进制形式读/写文件,操作的对象不再是打开文件就能看到的字符,而是文件底层存储的二进制数据。 更详细地讲,当以该形式读取文件时,读取的是该文件底层存储的二进制数据;同样,当将某数据以二进制形式写入到文件中时,写入的也是其对应的二进制数据。

C++ 标准库中,提供了 2 套读写文件的方法组合,分别是:

  1. 使用 >> 和 << 读写文件:适用于以文本形式读写文件;
  2. 使用 read() 和 write() 成员方法读写文件:适用于以二进制形式读写文件。
int x,sum=0; 

ifstream srcFile("in.txt", ios::in);    //以文本模式打开 in.txt 备读
// 假设 in.txt 中为 "10 20 30 40 50" 字符串
if(!srcFile){       // 打开失败    
    cout << "error opening source file." << endl;
    return 0;
}

ofstream destFile("out.txt", ios::out);     //以文本模式打开 out.txt 备写 
if (!destFile) {
    srcFile.close();    //程序结束前不能忘记关闭以前打开过的文件 
    cout << "error opening destination file." << endl; 
    return 0;
}

//可以像用 cin 那样用 ifstream 对象 
while (srcFile >> x) {      //将"10 20 30 40 50" 字符串解析成 int 整数
    sum += x;
    //可以像 cout 那样使用 ofstream 对象 destFile << x << " ";
    destFile << x << " ";   // destFile 最终会写入 "10 20 30 40 50" 字符串
}

cout << "sum:" << sum << endl;     // 150
destFile.close(); 
srcFile.close(); 

六、read()和write()读写二进制文件

对于类对象,如果以文本形式写入,每个对象的信息所占的字节数将不同,不利于查找指定学生信息,查找效率低下,这种情况应该以二进制形式写入对象。

read() 方法用于以二进制形式从文件中读取数据;write() 方法用于以二进 制形式将数据写入文件。

write

ofstream 和 fstream 的 write() 成员方法实际上继承自 ostream 类,其功能是将内存中 buffer 指向的 count 个字节的内容写入文件,基本格式如下:

ostream & write(char* buffer, int count);

  • buffer:指定要写入文件的二进制数据的起始位置
  • count:指定写入字节的个数

该方法可以被 ostream 类的 cout 对象调用,常用于向屏幕上输出字符串。同时,它还可以被 ofstream 或者 fstream 对象调用,用于将指定个数的二进制数据写入文件。

write() 方法会从文件写指针指向的位置将二进制数据写入。文件刚打开时,文件写指针指向的是文件的开头(如果以 ios::app 方式打开,则指向文件末尾),用 write() 方法写入 n 个字节,写指针指向的位置就向后移动 n 个字节。

CStudent s; 
ofstream outFile("students.dat", ios::out | ios::binary); 
while (cin >> s.szName >> s.age)
    outFile.write((char*)&s, sizeof(s));
outFile.close();

read

ifstream 和 fstream 的 read() 方法实际上继承自 istream 类,其功能正好和 write() 方法相反,即从文件中读 取 count 个字节的数据。该方法的语法格式如下:

istream & read(char* buffer, int count);

  • buffer:指定读取字节的起始位置
  • count:指定读取字节的个数

read() 方法从文件读指针指向的位置开始读取若干字节。

CStudent s;
ifstream inFile("students.dat",ios::in|ios::binary); //二进制读方式打开 if(!inFile) {
    cout << "error" <<endl;
    return 0;
}
while(inFile.read((char *)&s, sizeof(s))) { //一直读到文件结束 
    cout << s.szName << " " << s.age << endl;
}
inFile.close();

七、get()和 put() 逐个读写

逐个读取文件中存储的字符,或者逐个将字符存储到文件中的情形,可以使用 get() 和 put() 成员方法实现。

当 fstream 和 ofstream 文件流对象调用 put() 方法时,该方法的功能就变成了向指定文件中写入单个字符。 put() 方法的语法格式如下:

ostream& put (char c);

  • c:指定要写入文件的字符
char c;
// 以二进制形式打开文件
ofstream outFile("out.txt", ios::out | ios::binary);
if (!outFile) { 
    cout << "error" << endl;
    return 0;
}

while (cin >> c) { 
    //将字符 c 写入 out.txt 文件
    outFile.put(c);
}

outFile.close();

注意,由于文件存放在硬盘中,硬盘的访问速度远远低于内存。如果每次写一个字节都要访问硬盘,那么文件 的读写速度就会慢得不可忍受。因此,操作系统在接收到 put() 方法写文件的请求时,会先将指定字符存储在 一块指定的内存空间中(称为文件流输出缓冲区),等刷新该缓冲区(缓冲区满、关闭文件、手动调用 flush()方法等,都会导致缓冲区刷新)时,才会将缓冲区中存储的所有字符“一股脑儿”全写入文件。

get()方法的语法格式有很多,这里仅介绍常见的2中:

int get(); // 返回读取到的字符的 ASCII码,末尾返回EOF
istream& get (char& c); // 将读到字符赋值给 c

八、getline() 读取一行字符串

getline() 方法定义在 istream 类中,而 fstream 和 ifstream 类继承自 istream 类,因此 fstream 和 ifstream 的类对象可以调用 getline() 成员方法。

当文件流对象调用 getline() 方法时,该方法的功能就变成了从指定文件中读取一行字符串。该方法有以下 2 种 语法格式:

istream & getline(char* buf, int bufSize); // 从文件输入流缓冲区中读取 bufSize-1 个字符到 buf,或遇到 \n 为止
istream & getline(char* buf, int bufSize, char delim); // 从文件输入流缓冲区中读取 bufSize-1 个字符到 buf,或读到 delim 字符为止

如果文件输入流中 \n 或 delim 之前的字符个数达到或超过 bufSize,就会导致读取失败。

char c[40]; 
//以二进制模式打开 in.txt 文件
ifstream inFile("in.txt", ios::in | ios::binary); 
//判断文件是否正常打开 
if (!inFile) {
    cout << "error" << endl;
    return 0;
}

//从 in.txt 文件中读取一行字符串,最多不超过 39 个 
inFile.getline(c, 40); 
cout << c ;
inFile.close();

九、移动和获取文件读写指针(seekp、seekg、tellg、tellp)

  • ifstream 类和 fstream 类有 seekg 成员函数,可以设置文件读指针的位置;
  • ofstream 类和 fstream 类有 seekp 成员函数,可以设置文件写指针的位置。

位置指距离文件开头多少个字节。

这两个函数的原型如下:

ostream & seekp (int offset, int mode);
istream & seekg (int offset, int mode);

mode 代表文件读写指针的设置模式,有以下三种选项:

  • ios::beg:让文件读指针(或写指针)指向从文件开始向后的 offset 字节处。offset 等于 0 即代表文件开头。 在此情况下,offset 只能是非负数。
  • ios::cur:在此情况下,offset 为负数则表示将读指针(或写指针)从当前位置朝文件开头方向移动 offset 字节, 为正数则表示将读指针(或写指针)从当前位置朝文件尾部移动 offset 字节,为 0 则不移动。
  • ios::end:让文件读指针(或写指针)指向从文件结尾往前的 |offset|(offset 的绝对值)字节处。在此情况下,offset 只能是 0 或者负数。
  • ifstream 类和 fstream 类还有 tellg 成员函数,能够返回文件读指针的位置;
  • ofstream 类和 fstream 类还有 tellp 成员函数,能够返回文件写指针的位置。

这两个成员函数的原型如下:

int tellg(); // 返回文件读指针的位置
int tellp(); // 返回文件写指针的位置

class CStudent{
public:
    char szName[20];
    int age;
};

int main(){
    CStudent s; 
    fstream ioFile("students.dat", ios::in|ios::out);   //用既读又写的方式打开 
    if(!ioFile) {
        cout << "error" ;
        return 0;   
    }

    ioFile.seekg(0,ios::end); //定位读指针到文件尾部,以便用以后 tellg 获取文件长度
    int L = 0,R;    // L 是折半查找范围内第一个记录的序号,R 是折半查找范围内最后一个记录的序号
    R = ioFile.tellg() / sizeof(CStudent) - 1;
    
    //首次查找范围的最后一个记录的序号就是: 记录总数- 1 
    do {
        int mid = (L + R)/2; //要用查找范围正中的记录和待查找的名字比对
        ioFile.seekg(mid *sizeof(CStudent),ios::beg);  //定位到正中的记录 
        ioFile.read((char *)&s, sizeof(s));
        int tmp = strcmp( s.szName,"Jack"); 
        if( tmp == 0 ) { //找到了 
            s.age = 20;
            ioFile.seekp(mid*sizeof(CStudent),ios::beg); 
            ioFile.write((char*)&s, sizeof(s));
            break;
        }
        else if (tmp > 0) //继续到前一半查找 
            R = mid - 1 ;
        else //继续到后一半查找 } 
            L = mid + 1; 
    }while(L <= R); 
    ioFile.close();
    return 0;
}
posted @ 2021-10-26 10:47  锦瑟,无端  阅读(1356)  评论(0编辑  收藏  举报