前言

虽然我已经大三了,但是面对夏令营随时可能犯病的编程要求,我还是要准备一下文件读取的。毕竟文件读写还是挺重要的,而编程题目里的文件格式乱七八糟的,不清楚是否允许使用 C++ 的读写方式,所以做一做总结吧。

打开关闭文件

使用 C 标准库 stdio.h

FILE *fopen(const char *filename, const char *mode);
模式 描述
r 打开一个已有的文本文件,允许读取文件。
w 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。
a 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。
r+ 打开一个文本文件,允许读写文件。
w+ 打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。
a+ 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。

如果处理的是二进制文件,则需使用下面的访问模式来取代上面的访问模式:

"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"

如果需要关闭文件,使用 fclose() 函数:

int fclose(FILE *fp);

如果成功关闭文件,fclose() 函数返回零,如果关闭文件时发生错误,函数返回 EOF。这个函数实际上,会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。EOF 是一个定义在头文件 stdio.h 中的常量。

C 标准库提供了各种函数来按字符或者以固定长度字符串的形式读写文件。

使用 C++ 标准库 fstream,它定义了三个新的数据类型:

数据类型 描述
ofstream 该数据类型表示输出文件流,用于创建文件并向文件写入信息。
ifstream 该数据类型表示输入文件流,用于从文件读取信息。
fstream 该数据类型通常表示文件流,且同时具有 ofstreamifstream 两种功能,这意味着它可以创建文件,向文件写入信息,从文件读取信息。

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

同样的,利用 open() 函数打开文件。

void open(const char *filename, ios::openmode mode);

在这里,open() 成员函数的第一参数指定要打开的文件的名称和位置,第二个参数定义文件被打开的模式。

模式标志 描述
ios::app 追加模式。所有写入都追加到文件末尾。
ios::ate 文件打开后定位到文件末尾。
ios::in 打开文件用于读取。
ios::out 打开文件用于写入。
ios::trunc 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。

对于 ifstream,可以直接在构造函数中提供以上参数,则不需要 open()

当 C++ 程序终止时,它会自动关闭刷新所有流,释放所有分配的内存,并关闭所有打开的文件。但程序员应该养成一个好习惯,在程序终止前关闭所有打开的文件。

下面是 close() 函数的标准语法,close() 函数是 fstreamifstreamofstream 对象的一个成员。

void close();

读取文件

int fgetc(FILE * fp); // 读取单个字符
char *fgets(char *buf, int n, FILE *fp); // 读取一段字符串
int fscanf(FILE *fp, const char *format, ...) // 格式化读取
  • fgetc() 函数从 fp 所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回 EOF

  • fgets() 函数从 fp 所指向的输入流中读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在最后追加一个 null 字符来终止字符串。

    如果这个函数在读取最后一个字符之前就遇到一个换行符 '\n' 或文件的末尾 EOF,则只会返回读取到的字符,包括换行符。当 fget()读取失败,如已经达到 EOF 时,返回 NULL,借此可以判断文件是否读取完成。

  • fscanf() 函数从文件中读取字符串,但是在遇到第一个空格和换行符时,它会停止读取。如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回 EOFformat 的格式请见 C 库函数 – fscanf() | 菜鸟教程 (runoob.com)

    也就是说,如果使用类似 %*d 的方式忽视了匹配的内容,则不会计入返回的成功匹配和赋值计数中。

  • fread()函数用于读取二进制文件。

  • 如果想再从头开始读取,使用以下函数:

    void rewind(FILE *stream)

读取文件时需要注意这两个代码的差异

/* --- data.txt --- 
3
3N,1E,1N,3E,2S,1W
*/
FILE* infile = fopen("data.txt", "r");
int i = 0;
char c1 = 0;
fscanf(infile, "%d", &i); // 这里没有空格
fscanf(infile, "%c", &c1); // c1='\n'
FILE* infile = fopen("data.txt", "r");
int i = 0;
char c1 = 0;
fscanf(infile, "%d ", &i);// 这里有空格
fscanf(infile, "%c", &c1); // c1='3'
infile >> tmp;
std::istream &getline(char* _Str, std::streamsize _count);
  • 使用类似 cin的写法,利用 >> 运算符读取。这种方法首先跳过流中连续的空白字符(空格、换行、制表符),然后开始按变量类型读取非空白字符,中间再遇到流中的空格、换行、制表符等空白字符就会停止读取,但是它不会读入这些空白字符,所以可以连续使用进行读取。

  • 类方法 getline() 与 C 语言中的 fgets() 类似。如果它读取的字符少于给定的大小 n - 1,或到达 EOF ,它会追加空字符后读入内存中。但是,它会把每行结尾的 '\n' 丢弃。

    • 如果是读取了 n - 1 个字符,还不能达到设定的分隔符,或者换行符,则 getline() 会设置 failbit 如果要继续读取,需要调用 clear() 进行重置。

    • 如果使用 infile >> tmp 读取了某一行,则行末的换行还留在文件流中,那么再使用 getline() 就会读入该行末尾的换行,也就是读入一个空字符串。如果要文件位置指针跳过换行,需要用 infile.seekg(2,ios::cur),因为 windows中换行是 \r\n 占两个字节。

  • read()函数用于读取二进制文件。一定要注意配合相应的打开模式!一定要看我!

  • 判断文件是否读取完毕需要用 eof()方法。

    注意,以下写法会出现问题。

    /* --- data.txt --- 
    1234
    */
    fstream infile ("data.txt");
    char c = 0;
    cout << "输出结果:";
    while (!infile.eof())
    {
    infile >> c;
    cout << c;
    }
    //输出结果:12344

    原因是 C++ 判定文件读取到 EOF 的时机,是尝试读取最后一个元素后面的数据时,而不是读取完最后一个数据就到了 EOF。所以,当 '4' 被读取后,还未达到 EOF, !infile.eof()true ,还会再进入一次循环,经过一次读取后才达到 EOF, !infile.eof() 才会变为 false

  • istreamostream 都提供了用于重新定位文件位置指针的成员函数。这些成员函数包括关于 istreamseekg(“seek get”)和关于 ostreamseekp(“seek put”)。

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

    文件位置指针是一个整数值,指定了从文件的起始位置到指针所在位置的字节数。

    // 定位到 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);

写入文件

int fgetc( FILE * fp );
char *fgets( char *buf, int n, FILE *fp );
int fprintf(FILE *fp,const char *format, ...)
  • 函数 fputc() 把参数 c 的字符值写入到 fp 所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回 EOF
  • 函数 fputs() 把字符串 s 写入到 fp 所指向的输出流中。如果写入成功,它会返回一个非负值,如果发生错误,则会返回 EOF
  • 函数 fprintf() 把一个字符串写入到文件中。
  • 函数 fwrite()用于写入二进制文件
  • 使用类似 cout的写法,利用 << 运算符写入。
outfile << tmp;

写入的格式与 cout 的写法相同。需要包含 <iomanip>

注意:“流操纵算子”一栏中的星号*不是算子的一部分,星号表示在没有使用任何算子的情况下,就等效于使用了该算子。例如,在默认情况下,整数是用十进制形式输出的,等效于使用了 dec 算子。

流操纵算子 作  用
*dec 以十进制形式输出整数 常用
hex 以十六进制形式输出整数
oct 以八进制形式输出整数
fixed 以普通小数形式输出浮点数
scientific 以科学计数法形式输出浮点数
left 左对齐,即在宽度不足时将填充字符添加到右边
*right 右对齐,即在宽度不足时将填充字符添加到左边
setbase(b) 设置输出整数时的进制,b=8、10 或 16
setw(w) 指定输出宽度为 w 个字符,或输入字符串时读入 w 个字符
setfill(c) 在指定输出宽度的情况下,输出的宽度不足时用字符 c 填充(默认情况是用空格填充)
setprecision(n) 设置输出浮点数的精度为 n。

在使用非 fixed 且非 scientific 方式输出的情况下,n 即为有效数字最多的位数,如果有效数字位数超过 n,则小数部分四舍五人,或自动变为科学计数法输出并保留一共 n 位有效数字。

在使用 fixed 方式和 scientific 方式输出的情况下,n 是小数点后面应保留的位数。
setiosflags(flag) 将某个输出格式标志置为 1
resetiosflags(flag) 将某个输出格式标志置为 0
boolapha 把 true 和 false 输出为字符串 不常用
*noboolalpha 把 true 和 false 输出为 0、1
showbase 输出表示数值的进制的前缀
*noshowbase 不输出表示数值的进制.的前缀
showpoint 总是输出小数点
*noshowpoint 只有当小数部分存在时才显示小数点
showpos 在非负数值中显示 +
*noshowpos 在非负数值中不显示 +
*skipws 输入时跳过空白字符
noskipws 输入时不跳过空白字符
uppercase 十六进制数中使用 A~E。若输出前缀,则前缀输出 0X,科学计数法中输出 E
*nouppercase 十六进制数中使用 a~e。若输出前缀,则前缀输出 0x,科学计数法中输出 e。
internal 数值的符号(正负号)在指定宽度内左对齐,数值右对 齐,中间由填充字符填充。
  • 函数 write()用于写入二进制文件

关于二进制读取的重要补充说明

看我看我一定要看我!

这部分内容是在夏令营之后添加的。我其实根本想不到题目居然会出二进制文件的读取,二进制文件读取我也没认真准备。 这和前两年的题目风格完全不一样啊!

起因

在夏令营机试的时候,使用 C++ 风格的二进制文件读取 infile.read() 只成功读入了42个正确数据,之后就到了 EOF。其中 fval.dat 是存储了512*512个 float 数据的。

fstream infile1 ("fval.dat",ios::in);
float* f1 = new float[512 * 512];
infile1.read((char*)f1, 512 * 512 * sizeof(float));
infile1.close();

我 Debug 了半个小时还没有读进去,当时就慌了,心一狠,换了 C 语言风格的二进制文件读取,然后就成功读取了全部的数据。

FILE* infile2 = fopen("fval.dat", "rb");
float* f2 = new float[512 * 512];
fread(f2, sizeof(float), 512 * 512, infile2);
fclose(infile2);

你发现什么问题了吗?

经过

夏令营之后,我查找了 c++ 文件读写出错的问题,最后在同学的指点下,意识到一个自己从来没注意但很严重的问题。由于我从来没有用 C++ 进行二进制文件读写过,所以我对 ios::binary 没有太多的印象。而之前提及的表格中,也少了一个用于打开二进制文件的 ios::binary

模式标志 描述
ios::app 追加模式。所有写入都追加到文件末尾。
ios::ate 文件打开后定位到文件末尾。
ios::in 打开文件用于读取。
ios::out 打开文件用于写入。
ios::trunc 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。

不使用 ios::binary 虽然在原理上也能进行读取。但是,如果不使用二进制模式,对于某些情况,存在转换的情况。在本次的数据(二进制)中

C7 91 1C C0    10 67 1b C0    47 3B 1A C0    6E 0E 19 C0

前8个字节能正常读取数据,float 占4个字节,因此成功读取了两个 float 数值。但是在读取第三个 float 数据时, 1A 对应的是 SUB,或者说是 ^Z。因此会导致文件读取提前结束。当我们把 1A 换成 3B 等不会被转换的字节时,读取就不会提前结束。

结论

  • 二进制文件就好好用二进制模式读取。
  • 二进制模式下,不会存在特殊字符的转换问题。

参考网页