数据结构课设预习报告

补充C++相关知识

文件操作

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

  • ifstream:专用于从文件中读取数据;

  • ofstream:专用于向文件中写入数据;

  • fstream:既可用于从文件中读取数据,又可用于向文件中写入数据。

这 3 个文件流类都位于 < fstream> 头文件中,因此在使用它们之前,程序中应先引入此头文件。

要在 C++ 中进行文件处理,必须在 C++ 源代码文件中包含头文件 <iostream> 和 <fstream>。

这 3 个文件流类的继承关系,如图 1 所示。

图1: C++类库中的流类

可以看到,ifstream 类和 fstream 类是从 istream 类派生而来的,因此 ifstream 类拥有 istream 类的全部成员方法。同样地,ofstream 和 fstream 类也拥有 ostream 类的全部成员方法。这也就意味着,istream 和 ostream 类提供的供 cin 和 cout 调用的成员方法,也同样适用于文件流。并且这 3 个类中有些成员方法是相同的,比如 operator <<()、operator >>()、peek()、ignore()、getline()、get() 等。

值得一提的是,和 < iostream> 头文件中定义有 ostream 和 istream 类的对象 cin 和 cout 不同,< fstream> 头文件中并没有定义可直接使用的 fstream、ifstream 和 ofstream 类对象。因此,如果我们想使用该类操作文件,需要自己创建相应类的对象。

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

值得一提的是,无论是读取文件中的数据,还是向文件中写入数据,最先要做的就是调用 open() 成员方法打开文件。同时在操作文件结束后,还必须要调用 close() 成员方法关闭文件。

在对文件进行读写操作之前,先要打开文件。打开文件有以下两个目的:

  • 通过指定文件名,建立起文件和文件流对象的关联,以后要对文件进行操作时,就可以通过与之关联的流对象来进行。

  • 指明文件的使用方式。使用方式有只读、只写、既读又写、在文件末尾添加数据、以文本方式使用、以二进制方式使用等多种。

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

  • 调用流对象的 open 成员函数打开文件。

  • 定义文件流对象时,通过构造函数打开文件。

使用 open 函数打开文件

先看第一种文件打开方式。以 ifstream 类为例,该类有一个 open 成员函数,其他两个文件流类也有同样的 open 成员函数:

void open(const char* szFileName, int mode)

第一个参数是指向文件名的指针,第二个参数是文件的打开模式标记。

文件的打开模式标记代表了文件的使用方式,这些标记可以单独使用,也可以组合使用。下表列出了各种模式标记单独使用时的作用,以及常见的两种模式标记组合的作用。

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

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

  • ios::in | ios::binary表示用二进制模式,以读取的方式打开文件。

  • ios::out | ios::binary表示用二进制模式,以写入的方式打开文件。

一般来说,如果处理的是文本文件,那么用文本方式打开会方便一些。但其实任何文件都可以以二进制方式打开来读写。

在流对象上执行 open 成员函数,给出文件名和打开模式,就可以打开文件。判断文件打开是否成功,可以看“对象名”这个表达式的值是否为 true,如果为 true,则表示文件打开成功。

下面的程序演示了如何打开文件:

#include <iostream>
#include <fstream>
using namespace std;
int main()
{
    ifstream inFile;
    inFile.open("c:\\tmp\\test.txt", ios::in);
    if (inFile)  //条件成立,则说明文件打开成功
        inFile.close();
    else
        cout << "test.txt doesn't exist" << endl;
    ofstream oFile;
    oFile.open("test1.txt", ios::out);
    if (!oFile)  //条件成立,则说明文件打开出错
        cout << "error 1" << endl;
    else
        oFile.close();
    oFile.open("tmp\\test2.txt", ios::out | ios::in);
    if (oFile)  //条件成立,则说明文件打开成功
        oFile.close();
    else
        cout << "error 2" << endl;
    fstream ioFile;
    ioFile.open("..\\test3.txt", ios::out | ios::in | ios::trunc);
    if (!ioFile)
        cout << "error 3" << endl;
    else
        ioFile.close();
    return 0;
}

调用 open 成员函数时,给出的文件名可以是全路径的,如第 7 行的c:\\tmp\\test.txt, 指明文件在 c 盘的 tmp 文件夹中;也可以只给出文件名,如第 13 行的test1.txt,这种情况下程序会在当前文件夹(也就是可执行程序所在的文件夹)中寻找要打开的文件。

第 18 行的tmp\\test2.txt给出的是相对路径,说明 test2.txt 位于当前文件夹的 tmp 子文件夹中。第 24 行的..\\test3.txt也是相对路径,代表上一层文件夹,此时要到当前文件夹的上一层文件夹中查找 test3.txt。此外,..\\..\\test4.txt..\\tmp\\test4.txt等都是合法的带相对路径的文件名。

使用流类的构造函数打开文件

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

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

第一个参数是指向文件名的指针;第二个参数是打开文件的模式标记,默认值为ios::in; 第三个参数是整型的,也有默认值,一般极少使用。

在定义流对象时打开文件的示例程序如下(用流类的构造函数打开文件):

#include <iostream>
#include <fstream>
using namespace std;
int main()
{
    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();
    return 0;
}

注意,当不再对打开的文件进行任何操作时,应及时调用 close() 成员方法关闭文件。

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

close() 方法的用法很简单,其语法格式如下:

void close()

可以看到,该方法既不需要传递任何参数,也没有返回值。

举个例子:

#include <fstream>
using namespace std;
int main()
{
    const char *url="http://c.biancheng.net/cplus/";
    ofstream outFile("url.txt", ios::out);
    //向 url.txt 文件中写入字符串
    outFile.write(url, 30);
    //关闭已打开的文件
    outFile.close();
    return 0;
}

运行程序,在该程序同目录下会生成一个 url.txt 文件,其内部存储的数据为:

http://c.biancheng.net/cplus/

即便上面程序中不调用 close() 方法,也能成功向 url.txt 文件中写入 url 字符串。这是因为,当文件流对象的生命周期结束时,会自行调用其析构函数,该函数内部在销毁对象之前,会先调用 close() 方法切断它与任何文件的关联,最后才销毁它。

强烈建议读者,使用 open() 方法打开的文件,一定要手动调用 close() 方法关闭,这样可以避免程序发生一些奇葩的错误!

在讲解具体读写文件的方法之前,读者首先要搞清楚的是,对文件的读/写操作又可以细分为 2 类,分别是以文本形式读写文件和以二进制形式读写文件。

(1) 我们知道,文件中存储的数据并没有类型上的分别,统统都是字符。所谓以文本形式读/写文件,就是直白地将文件中存储的字符(或字符串)读取出来,以及将目标字符(或字符串)存储在文件中。

(2) 而以二进制形式读/写文件,操作的对象不再是打开文件就能看到的字符,而是文件底层存储的二进制数据。更详细地讲,当以该形式读取文件时,读取的是该文件底层存储的二进制数据;同样,当将某数据以二进制形式写入到文件中时,写入的也是其对应的二进制数据。

举个例子,假设我们以文本形式将浮点数 19.625 写入文件,则该文件会直接将 "19.625" 这个字符串存储起来。当我们双击打开此文件,也可以看到 19.625。值得一提的是,由非字符串数据(比如这里的浮点数 19.625)转换为对应字符串(转化为 "19.625")的过程,C++ 标准库已经实现好了,不需要我们操心。

但如果以二进制形式将浮点数 19.625 写入文件,则该文件存储的不再是 "19.625" 这个字符串,而是 19.625 浮点数对应的二进制数据。以 float 类型的 19.625 来说,文件最终存储的数据如下所示:

0100 0001 1001 1101 0000 0000 0000 0000

显然,如果直接将以上二进制数据转换为 float 类型,仍可以得到浮点数 19.625。但对于文件来说,它只会将存储的二进制数据根据既定的编码格式(如 utf-8、gbk 等)转换为一个个字符。这也就意味着,如果我们直接打开此文件,看到的并不会是 19.625,往往是一堆乱码。

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

  1. 使用 >> 和 << 读写文件:适用于以文本形式读写文件;

  2. 使用 read() 和 write() 成员方法读写文件:适用于以二进制形式读写文件。

C++ >>和<<读写文本文件

当 fstream 或者 ifstream 类对象打开文件(通常以 ios::in 作为打开模式)之后,就可以直接借助 >> 输入流运算符,读取文件中存储的字符(或字符串);当 fstream 或者 ofstream 类对象打开文件(通常以 ios::out 作为打开模式)后,可以直接借助 << 输出流运算符向文件中写入字符(或字符串)。

举个例子:

#include <iostream>
#include <fstream>
using namespace std;
int main()
{
    int x,sum=0;
    ifstream srcFile("in.txt", ios::in); //以文本模式打开in.txt备读
    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) {
        sum += x;
        //可以像 cout 那样使用 ofstream 对象
        destFile << x << " ";
    }
    cout << "sum:" << sum << endl;
    destFile.close();
    srcFile.close();
    return 0;
}

注意,此程序中分别采用 ios::in 和 ios::out 打开文件,即以文本模式而非二进制模式打开文件。感兴趣的读者可在其基础上添加 ios::binary,即以二进制模式打开文件,程序依旧会正常执行。这是因为,以文本模式打开文件和以二进制模式打开文件,并没有很大的区别。

执行此程序之前,必须在和该程序源文件同目录中手动创建一个 in.txt 文件,假设其内部存储的字符串为:

10 20 30 40 50

建立之后,执行程序,其执行结果为:

sum:150

同时在 in.txt 文件同目录下,会生成一个 out.txt 文件,其内部存储的字符和 in.txt 文件完全一样,读者可自行打开文件查看。

通过分析程序的执行结果不难理解,对于 in.txt 文件中的 "10 20 30 40 50" 字符串,srcFile 对象会依次将 "10"、"20"、"30"、"40"、"50" 读取出来,将它们解析成 int 类型的整数 10、20、30、40、50 并赋值给 x,同时完成和 sum 的加和操作。

同样,对于每次从 in.txt 文件读取并解析出的整形 x,destFile 对象都会原封不动地将其再解析成对应的字符串(如整数 10 解析成字符串 "10"),然后和 " " 空格符一起写入 out.txt 文件。

文件位置指针

istream 和 ostream 都提供了用于重新定位文件位置指针的成员函数。这些成员函数包括关于 istream 的 seekg("seek get")和关于 ostream 的 seekp("seek put")。

seekg 和 seekp 的参数通常是一个长整型。第二个参数可以用于指定查找方向。查找方向可以是 ios::beg(默认的,从流的开头开始定位),也可以是 ios::cur(从流的当前位置开始定位),也可以是 ios::end(从流的末尾开始定位)。

文件位置指针是一个整数值,指定了从文件的起始位置到指针所在位置的字节数。下面是关于定位 "get" 文件位置指针的实例:

// 定位到 fileObject 的第 n 个字节(假设是 ios::beg)
fileObject.seekg( n );
 
// 把文件的读指针从 fileObject 当前位置向后移 n 个字节
fileObject.seekg( n, ios::cur );
 
// 把文件的读指针从 fileObject 末尾往回移 n 个字节
fileObject.seekg( n, ios::end );
 
// 定位到 fileObject 的末尾
fileObject.seekg( 0, ios::end );

不过介绍具体的实现方法前,先给读者介绍一下相比以文本形式读写文件,以二进制形式读写文件有哪些好处?

举个例子,现在要做一个学籍管理程序,其中一个重要的工作就是记录学生的学号、姓名、年龄等信息。这意味着,我们需要用一个类来表示学生,如下所示:

class CStudent
{
    char szName[20];  //假设学生姓名不超过19个字符,以 '\0' 结尾
    char szId[l0];  //假设学号为9位,以 '\0' 结尾
    int age;  //年龄
};

前面章节中,我们学会了如何以文本形式读写文件,如果使用此方式存储学生的信息,则最终的文件中存储的学生信息可能是这个样子:

Micheal Jackson 110923412 17
Tom Hanks 110923413 18
......

要知道,这种存储学生信息的方式不但浪费空间,而且后期不利于查找指定学生的信息(查找效率低下),因为每个学生的信息所占用的字节数不同。

这种情况下,以二进制形式将学生信息存储到文件中,是非常不错的选择,因为以此形式存储学生信息,可以直接把 CStudent 对象写入文件中,这意味着每个学生的信息都只占用 sizeof(CStudent) 个字节。

值得一提的是,要实现以二进制形式读写文件,<< 和 >> 将不再适用,需要使用 C++ 标准库专门提供的 read() 和 write() 成员方法。其中,read() 方法用于以二进制形式从文件中读取数据;write() 方法用于以二进制形式将数据写入文件。

C++ ostream::write()方法写文件

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

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

其中,buffer 用于指定要写入文件的二进制数据的起始位置;count 用于指定写入字节的个数。

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

同时,该方法会返回一个作用于该函数的引用形式的对象。举个例子,obj.write() 方法的返回值就是对 obj 对象的引用。

需要注意的一点是,write() 成员方法向文件中写入若干字节,可是调用 write() 函数时并没有指定这些字节写入文件中的具体位置。事实上,write() 方法会从文件写指针指向的位置将二进制数据写入。所谓文件写指针,是是 ofstream 或 fstream 对象内部维护的一个变量,文件刚打开时,文件写指针指向的是文件的开头(如果以 ios::app 方式打开,则指向文件末尾),用 write() 方法写入 n 个字节,写指针指向的位置就向后移动 n 个字节。

下面的程序演示了如何将学生信息以二进制形式写入文件:

#include <iostream>
#include <fstream>
using namespace std;
class CStudent
{
public:
    char szName[20];
    int age;
};
int main()
{
    CStudent s;
    ofstream outFile("students.dat", ios::out | ios::binary);
    while (cin >> s.szName >> s.age)
        outFile.write((char*)&s, sizeof(s));
    outFile.close();
    return 0;
}

输入:

Tom 60↙
Jack 80↙
Jane 40↙
^Z↙

其中,表示输出换行符,^Z 表示输入Ctrl+Z组合键结束输入。

执行程序后,会自动生成一个 students.dat 文件,其内部存有 72 字节的数据,如果用“记事本”打开此文件,可能看到如下乱码:

Tom 烫烫烫烫烫烫烫烫<  Jack 烫烫烫烫烫烫烫蘌  Jane 烫烫烫烫烫烫烫?

值得一提的是,程序中第 13 行指定文件的打开模式为 ios::out | ios::binary,即以二进制写模式打开。在 Windows平台中,以二进制模式打开文件是非常有必要的,否则可能出错。

另外,第 15 行将 s 对象写入文件。s 的地址就是要写入文件的内存缓冲区的地址,但是 &s 不是 char * 类型,因此要进行强制类型转换;第 16 行,文件使用完毕一定要关闭,否则程序结束后文件的内容可能不完整。

C++ istream::read()方法读文件

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

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

其中,buffer 用于指定读取字节的起始位置,count 指定读取字节的个数。同样,该方法也会返回一个调用该方法的对象的引用。

和 write() 方法类似,read() 方法从文件读指针指向的位置开始读取若干字节。所谓文件读指针,可以理解为是 ifstream 或 fstream 对象内部维护的一个变量。文件刚打开时,文件读指针指向文件的开头(如果以 ios::app 方式打开,则指向文件末尾),用 read() 方法读取 n 个字节,读指针指向的位置就向后移动 n 个字节。因此,打开一个文件后连续调用 read() 方法,就能将整个文件的内容读取出来。

通过执行 write() 方法的示例程序,我们将 3 个学生的信息存储到了 students.dat 文件中,下面程序演示了如何使用 read() 方法将它们读取出来:

#include <iostream>
#include <fstream>
using namespace std;
class CStudent
{
public:
    char szName[20];
    int age;
};
int main()
{
    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();
    return 0;
}

程序的输出结果是:

Tom 60
Jack 80
Jane 40

注意,程序中第 18 行直接将 read() 方法作为 while 循环的判断条件,这意味着,read() 方法会一直读取到文件的末尾,将所有字节全部读取完毕,while 循环才会终止。

另外,在使用 read() 方法的同时,如果想知道一共成功读取了多少个字节(读到文件尾时,未必能读取 count 个字节),可以在 read() 方法执行后立即调用文件流对象的 gcount() 成员方法,其返回值就是最近一次 read() 方法成功读取的字节数。

在某些特殊的场景中,我们可能需要逐个读取文件中存储的字符,或者逐个将字符存储到文件中。这种情况下,就可以调用 get() 和 put() 成员方法实现。

C++ ostream::put()成员方法

fstream 和 ofstream 类继承自 ostream 类,因此 fstream 和 ofstream 类对象都可以调用 put() 方法。

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

ostream& put (char c);

其中,c 用于指定要写入文件的字符。该方法会返回一个调用该方法的对象的引用形式。例如,obj.put() 方法会返回 obj 这个对象的引用。

举个例子:

#include <iostream>
#include <fstream>
using namespace std;
int main()
{
    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();
    return 0;
}

执行程序,输入:

http://c.biancheng.net/cplus/↙
^Z↙

其中,表示输入换行符;^Z是 Ctrl+Z 的组合键,表示输入结束。

由此,程序中通过执行 while 循环,会将 "C++入门教程,C++基础教程(更新完毕)" 字符串的字符逐个复制给变量 c,并逐个写入到 out.txt 文件。

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

C++ istream::get()成员方法

和 put() 成员方法的功能相对的是 get() 方法,其定义在 istream 类中,借助 cin.get() 可以读取用户输入的字符。在此基础上,fstream 和 ifstream 类继承自 istream 类,因此 fstream 和 ifstream 类的对象也能调用 get() 方法。

当 fstream 和 ifstream 文件流对象调用 get() 方法时,其功能就变成了从指定文件中读取单个字符(还可以读取指定长度的字符串)。值得一提的是,get() 方法的语法格式有很多,这里仅介绍最常用的 2 种:

int get();
istream& get (char& c);

其中,第一种语法格式的返回值就是读取到的字符,只不过返回的是它的 ASCII 码,如果碰到输入的末尾,则返回值为 EOF。第二种语法格式需要传递一个字符变量,get() 方法会自行将读取到的字符赋值给这个变量。

本节前面在讲解 put() 方法时,生成了一个 out.txt 文件,下面的样例演示了如何通过 get() 方法逐个读取 out.txt 文件中的字符:

#include <iostream>
#include <fstream>
using namespace std;
int main()
{
    char c;
    //以二进制形式打开文件
    ifstream inFile("out.txt", ios::out | ios::binary);
    if (!inFile) {
        cout << "error" << endl;
        return 0;
    }
    while ( (c=inFile.get())&&c!=EOF )   //或者 while(inFile.get(c)),对应第二种语法格式
    {
        cout << c ;
    }
    inFile.close();
    return 0;
}

程序执行结果为:

http://c.biancheng.net/cplus/

注意,和 put() 方法一样,操作系统在接收到 get() 方法的请求后,哪怕只读取一个字符,也会一次性从文件中将很多数据(通常至少是 512 个字节,因为硬盘的一个扇区是 512 B)读到一块内存空间中(可称为文件流输入缓冲区),这样当读取下一个字符时,就不需要再访问硬盘中的文件,直接从该缓冲区中读取即可。

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

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

istream & getline(char* buf, int bufSize);
istream & getline(char* buf, int bufSize, char delim);

其中,第一种语法格式用于从文件输入流缓冲区中读取 bufSize-1 个字符到 buf,或遇到 \n 为止(哪个条件先满足就按哪个执行),该方法会自动在 buf 中读入数据的结尾添加 '\0'。

第二种语法格式和第一种的区别在于,第一个版本是读到 \n 为止,第二个版本是读到 delim 字符为止。\n 或 delim 都不会被读入 buf,但会被从文件输入流缓冲区中取走。

以上 2 种格式中,getline() 方法都会返回一个当前所作用对象的引用。比如,obj.getline() 会返回 obj 的引用。

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

举个例子:

#include <iostream>
#include <fstream>
using namespace std;
int main()
{
    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();
    return 0;
}

假设 in.txt 文件中存有如下字符串:

http://c.biancheng.net/cplus/

则程序执行结果为:

http://c.biancheng.net/cplus/

当然,我们也可以使用 getline() 方法的第二种语法格式。例如,更改上面程序中第 15 行代码为:

inFile.getline(c,40,'c');

这意味着,一旦遇到字符 'c',getline() 方法就会停止读取。 再次运行程序,其输出结果为:

http://

另外,如果想读取文件中的多行数据,可以这样做:

#include <iostream>
#include <fstream>
using namespace std;
int main()
{
    char c[40];
    ifstream inFile("in.txt", ios::in | ios::binary);
    if (!inFile) {
        cout << "error" << endl;
        return 0;
    }
    //连续以行为单位,读取 in.txt 文件中的数据
    while (inFile.getline(c, 40)) {
        cout << c << endl;
    }
    inFile.close();
    return 0;
}

假设 in.txt 文件中存有如下数据:

http://c.biancheng.net/cplus/
http://c.biancheng.net/python/
http://c.biancheng.net/java/

则程序执行结果为:

http://c.biancheng.net/cplus/
http://c.biancheng.net/python/
http://c.biancheng.net/java/

在读写文件时,有时希望直接跳到文件中的某处开始读写,这就需要先将文件的读写指针指向该处,然后再进行读写。

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

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

所谓“位置”,就是指距离文件开头有多少个字节。文件开头的位置是 0。

这两个函数的原型如下:

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();

要获取文件长度,可以用 seekg 函数将文件读指针定位到文件尾部,再用 tellg 函数获取文件读指针的位置,此位置即为文件长度。

例题:假设学生记录文件 students.dat 是按照姓名排好序的,编写程序,在 students.dat 文件中用折半查找的方法找到姓名为 Jack 的学生记录,并将其年龄改为 20(假设文件很大,无法全部读入内存)。程序如下:

#include <iostream>
#include <fstream>
#include <cstring>
using namespace std;
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;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值