C 风格输入输出 选读

为兼容 C 语言,C++ 仍然提供了 C 风格的输入输出库。

注意

如无特殊情形,建议总是使用 C++ 风格的流式输入输出

std::FILE

虽然 C 语言没有面向对象的语法,但你仍然可以写出面向对象风格的代码。C 风格输入输出就是这样的一个例子。它的基础是结构体 std::FILE(在 C 语言中,是 FILE;下简称 FILE)。

FILE 类似于 std::*fstream,包含了关于文件读写的所有信息。但我们并不清楚 FILE 的布局;我们甚至看不到 FILE 的定义。但这无妨,在使用 FILE 的过程中并不会用到 FILE 类型,而是指向 FILE 的指针。

首先,使用 std::fopen (在 C 中是 fopen,下类似不再赘述)函数来创建一个 FILE 对象。它会返回一个 FILE* 供你使用。fopen 的声明是:

FILE* fopen(const char* filename, const char* mode);

在 C99 中,filenamemode 具有 restrict 限定。

其中,filename 是存储打开文件名的 C 风格字符串,mode"r""w""rb" "wb" 等,指示打开文件的方式是“读”还是“写”,是否是二进制模式。比如:

#include <cstdio> // C 风格 IO 库
                  // C 中为 <stdio.h>
int main() {
    // 以读取模式打开文件 "a.txt"
    std::FILE* fp = std::fopen("a.txt", "r");
    // 随后,操作 fp 以读取其内容...
}

当持有了指向 FILE 的指针后,就可以开始读文件或写文件了。

格式化读写

还是从格式化读写说起。类似 scanf,使用 fscanf 格式化地读。特别地,第一参数是 FILE*,指示从哪个文件格式化读取:

#include <cstdio>
int main() {
    // 以读取模式打开文件 "a.txt"
    std::FILE* fp = std::fopen("a.txt", "r");
    
    int a, b, c;
    // 从 a.txt 中读取三个空白字符分隔的整数
    std::fscanf(fp, "%d%d%d", &a, &b, &c);
    // [...]
}

所有的占位符规定(如 %d %c %s)都和 scanf 相同。

类似 printf,使用 fprintf 格式化地写。同样第一参数是 FILE*

#include <cstdio>
int main() {
    // 以写入模式打开文件 "b.txt"
    std::FILE* fp = std::fopen("b.txt", "w");
    
    int a = 42, b = 56, c = 71;
    // 写入 a b c 三个整数到 b.txt
    std::fscanf(fp, "%d %d %d\n", a, b, c);
    // [...]
}

所有的占位符规定都和 printf 相同。

无格式化读写

函数 freadfwrite 实现无格式化的读写。

char* fread(void* buffer, unsigned size, unsigned count, std::FILE* stream);
char* fwrite(const void* buffer, unsigned size, unsigned count, std::FILE* stream);

freadstream 从指代的文件读取 size * count 个字节,并存储到 buffer 所指向的位置上。

#include <cstdio>
int main() {
    std::FILE* fp = std::fopen("a.bin", "r");
    
    // 从 fp 读取 sizeof(int) 个字节,
    // 将内容读到 a 的存储空间
    int a;
    std::fread(&a, sizeof(a), 1, fp);

    // 类似,但读取到 b[0] b[1] b[2]
    int b[3];
    std::fread(&b, sizeof(int), 3, fp);

    // [...]
}

fwritebuffer 地址开始的 size * count 个字节的内容写入到 stream 所指代的文件。

#include <cstdio>
int main() {
    std::FILE* fp = std::fopen("b.bin", "w");

    // 将 a 的二进制存储写入到 "b.bin"
    int a = 42;
    std::fwrite(&a, sizeof(a), 1, fp);

    // 类似,但写入一个数组
    int b[3] = {42, 56, 71};
    std::fread(&b, sizeof(int), 3, fp);
}

关闭文件

可以注意到,刚刚 FILE* 的使用和 std::*fstream 很类似。fopen 的作用类似于构造函数,fread fscanf 等等函数就相当于成员函数,而传入的 FILE* 参数其实就是在面向对象语法中的 this

但最后还差一环,就是析构函数。在 C 语言中,变量的生存期结束并不会自动调用析构函数(也没有更好的自动化方案),所以我们需要手动通过 fclose 来“析构”掉 FILE* 所指向的对象。

int fclose(std::FILE* stream);

所以,我们之前的所有代码,都需要在结尾加上 fclose,来保证对内存和文件资源的正确清理。

#include <cstdio>
int main() {
    std::FILE* fp = std::fopen("b.txt", "w");
    int a = 42, b = 56, c = 71;
    std::fscanf(fp, "%d %d %d\n", a, b, c);
    
    // 必须手动关闭 fp!
    std::fclose(fp);
}

std::freopen

<cstdio> 中定义了一些,比如 stdinstdout。这两个宏可以展开称 FILE* 类型的表达式,从 stdin 这个 FILE* 读取就是从标准输入读取(也就是黑框框里的键盘输入),而写入到 stdout 就是写入到标准输出(也就是黑框框界面中显示字符)。

换而言之,scanf("%d", &a); 就相当于 fscanf(stdin, "%d", &a);;而 printf("%d", a); 就相当于 fprintf(stdout, "%d", a);

了解这些基本概念之后,再来看 std::freopen。它类似 std::fopen,也是打开文件;但它打开文件时会“抢占”一个已有的 FILE*。所谓的“抢占”,就是关闭这个 FILE* 原先所指代的文件,然后让这个 FILE* 指代我所打开的这个新文件。

std::FILE* freopen(const char* filename, const char* mode, std::FILE* stream);

比如:

#include <cstdio>
int main() {
    std::FILE* fp = std::fopen("a.txt", "w");
    // 本应对 fp 写入是写入到 a.txt...

    // 但 freopen 抢夺了 fp 原先的“指向”
    std::freopen("b.txt", "w", fp);

    // 现在向 fp 写入会写入到 b.txt。
    std::fprintf(fp, "Hello!\n");
    // 结果:b.txt 中出现 Hello!

    // 不要忘记关闭文件
    std::fclose(fp);
}

freopen 是唯一改变 I/O “指向”的标准方法,即“重定向”。当我们使用 freopen 改变 stdinstdout 的“指向”后,那接下来的所以 scanfprintf 都会变成对 freopen 指定文件的读写。

#include <cstdio>
int main() {
    std::freopen("input.txt", "r", stdin);
    std::freopen("output.txt", "w", stdout);

    // 从 input.txt 读取一个整数
    int a;
    std::scanf("%d", &a);

    // 写入到 output.txt
    std::printf("Hello!\n");

    // 关闭 input.txt 和 output.txt 文件
    std::fclose(stdin);
    std::fclose(stdout);
}

这种技巧在竞赛中较为常见。值得一提的是,<iostream> 中的流式输入输出 cin coutstdin stdout 默认是关联的;当使用 freopen 重定向 stdin stdout 后,cincout 的输入输出也会随之重定向。

最近更新:
代码未运行