复制文件有三种实现:逐字符、逐行、二进制块。 前两种简单、好懂,但只能复制文本文件;二进制块那一种才是"通用复制" ——图片、音乐、exe、压缩包,全靠它。 一句话判据:"要复制的文件是不是纯文本?" ——是 → 三种都行(选最省事的);不是 → 必须二进制块。 一句话概括:复制文件的本质是"搬运字节",所以"一字节都不许改"是底线——而只有二进制模式能保证这一点。
本节是 15.3 组的收尾,把前面五节的知识合起来用:
- 15.3.1 的读写闭环 + 15.3.4 的 read/write = 三种复制方案;
- 15.4 即将讲的 seekg/tellg 在本节先露个脸("求文件大小"就是复制程序要的第一个能力);
- 13.1.1 的 vector 可以当缓冲区用(可选方案,比固定数组灵活)。
一句话:本节是"小工具程序"的第一次成型——从这以后,你能写出有用的命令行工具了。
一、三种方案,一段代码看清差别
方案一:逐字符(最简单,最慢)
char ch;
while (in.get(ch)) out.put(ch);
优点:短、好懂、不会漏字节(连 '\0' 都读)。
缺点:一个字符一次读写 ——对一个 100 MB 的文件,就是 1 亿次循环。
方案二:逐行(对文本文件更"语义化")
string line;
while (getline(in, line)) out << line << "\n";
优点:行的结构清楚了(想顺手改点什么很方便,比如跳过空行、加行号)。
缺点:① 一样慢;② 有个隐蔽的 Bug ——getline 把换行符丢掉了,你得自己补 "\n";如果原文件最后一行没有换行符,复制出来会多一个换行。
方案三:二进制块(最快,最通用)
const int BUF = 4096;
char buf[BUF];
while (in.read(buf, BUF)) out.write(buf, BUF); // 整块搬运
long long rest = in.gcount(); // 最后没满的那一块
if (rest > 0) out.write(buf, rest);
优点:
- 快(一次搬 4 KB,系统调用次数少了几千倍);
- 通用(不认识内容的格式,所以什么文件都能复制);
- 准确(配
ios::binary,一字节都不改)。
缺点:多了一点"处理零头"的代码(最后一块常常读不满)。
关键点就在那个"零头":
文件 10000 字节,块 4096:
第 1 次 read:读到 4096 ✓
第 2 次 read:读到 4096 ✓
第 3 次 read:想读 4096,但只剩 1808 → read 失败,但读到了 1808 ✓
→ in.gcount() = 1808,必须把这 1808 字节单独写出去
漏掉这一步,复制出来的文件会"缺一小截尾巴" ——这是"手写文件复制"最常见的 Bug。
二、为什么"文本方式"不能复制任意文件
因为文本模式会"顺手改你的字节"(15.3.4 讲过):
| 文本模式 | 二进制模式 | |
|---|---|---|
| 写入时 | \n → \r\n(Windows) |
原样 |
| 读取时 | \r\n → \n |
原样 |
| 后果 | 凡是内容里有 \n 的文件,复制后都会"变大" |
一样大、内容完全一致 |
对纯文本:读时变回去、写时又变回来 →结果是对的,你感觉不到。
对图片/压缩包/exe:里面只要有一个字节恰好是 0x0A,就会被多插一个字节 → 文件损坏。
所以:
复制文件 = 搬运字节 = 必须用二进制模式。 文本模式只在"你知道它是文本、而且不介意转换"时才用。
三、动手 Demo:三种复制 + 一个"致命对照"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
// ── 工具:取文件字节数(15.4 会讲 seekg/tellg)──
long long fileSize(const string &f) {
ifstream in(f, ios::binary);
if (!in) return -1;
in.seekg(0, ios::end);
return (long long)in.tellg();
}
// ── 方案一:逐字符 ──────────────────────────
bool copyByChar(const string &src, const string &dst) {
ifstream in(src);
ofstream out(dst);
if (!in || !out) return false;
char ch;
while (in.get(ch)) out.put(ch);
return true;
}
// ── 方案二:逐行 ────────────────────────────
bool copyByLine(const string &src, const string &dst) {
ifstream in(src);
ofstream out(dst);
if (!in || !out) return false;
string line;
while (getline(in, line)) out << line << "\n";
return true;
}
// ── 方案三:二进制块(通用)─────────────────
bool copyBinary(const string &src, const string &dst, long long &total) {
ifstream in(src, ios::binary);
ofstream out(dst, ios::binary);
if (!in || !out) return false;
const int BUF = 4096;
char buf[BUF];
total = 0;
while (in.read(buf, BUF)) { // 满块:整块搬
out.write(buf, BUF);
total += BUF;
}
long long rest = (long long)in.gcount(); // 零头:单独搬
if (rest > 0) {
out.write(buf, rest);
total += rest;
}
return true;
}
// ── 工具:按行打出来看 ──────────────────────
void show(const string &f) {
ifstream in(f, ios::binary);
cout << " [" << f << "] ";
string line;
int n = 0;
while (getline(in, line)) cout << "(" << ++n << ")" << line << " ";
cout << endl;
}
int main() {
// ══ 第一部分:复制一个文本文件,三种方式内容都对 ══
{
ofstream o("src.txt", ios::binary); // 用二进制写,保证磁盘字节精确
o << "line 1\nline 2\nline 3\n";
o.close();
}
cout << "源文件 src.txt 大小 = " << fileSize("src.txt") << " 字节" << endl;
cout << "── 三种方式复制的结果 ──" << endl;
copyByChar("src.txt", "cp_char.txt"); show("cp_char.txt");
copyByLine("src.txt", "cp_line.txt"); show("cp_line.txt");
long long n = 0;
copyBinary("src.txt", "cp_bin.txt", n); show("cp_bin.txt");
// ══ 第二部分:致命对照——复制一个"含 0x0A 字节"的文件 ══
{
char data[6] = {'A', '\n', 'B', '\n', 'C', '\0'};
ofstream o("bin_src.dat", ios::binary);
o.write(data, 6);
o.close();
}
cout << "\n── 关键对照:复制一个含换行字节的二进制文件 ──" << endl;
cout << "原文件 bin_src.dat = " << fileSize("bin_src.dat") << " 字节" << endl;
copyByChar("bin_src.dat", "bin_cp_text.dat");
cout << "④ 文本方式复制(逐字符) = " << fileSize("bin_cp_text.dat")
<< " 字节 ← 字节被改了!" << endl;
copyBinary("bin_src.dat", "bin_cp_bin.dat", n);
cout << "⑤ 二进制方式复制(块搬运) = " << fileSize("bin_cp_bin.dat")
<< " 字节 ← 一字不差" << endl;
return 0;
}
看到什么算成功(Windows 上):
源文件 src.txt 大小 = 21 字节
── 三种方式复制的结果 ──
[cp_char.txt] (1)line 1 (2)line 2 (3)line 3
[cp_line.txt] (1)line 1 (2)line 2 (3)line 3
[cp_bin.txt] (1)line 1 (2)line 2 (3)line 3
── 关键对照:复制一个含换行字节的二进制文件 ──
原文件 bin_src.dat = 6 字节
④ 文本方式复制(逐字符) = 8 字节 ← 字节被改了!
⑤ 二进制方式复制(块搬运) = 6 字节 ← 一字不差
数字怎么来的(都验算过):
src.txt是 21 字节:"line 1\nline 2\nline 3\n"= 6 + 1 + 6 + 1 + 6 + 1 = 21(用二进制方式写的,所以磁盘上就是 21)。bin_src.dat是 6 字节:就是A\nB\nC\0六个字节。- ④ 变成 8 字节:文本模式写入时,两个
\n各被替换成\r\n(每个多 1 字节)→ 6 + 2 = 8。 - ⑤ 还是 6 字节:二进制模式一个字节都不改。
读结果:
- 第一部分三个文件的内容完全一样 ——对文本文件来说,三种方式都能用。
- 第二部分才是本节的结论:同一个"逐字符复制"函数,用在二进制文件上就把文件改坏了(6 → 8 字节)。这就是"文本模式不能复制任意文件"的实证。
- 注意 ④ 用的还是"方案一"那段完全正确的代码 ——代码没错,错的是"用文本模式去搬二进制"。
补充说明:这组数字(21 / 6 / 8)是 Windows 上的结果。在 Linux/macOS 上文本模式不做换行转换,④ 也会是 6 字节。 "行为依平台而变"本身就是"不要用文本模式复制二进制"的另一个理由。
四、大文件怎么复制得更好
三个可以继续优化的点:
| 优化 | 做法 | 收益 |
|---|---|---|
| 加大块 | BUF 从 4096 提到 64 KB 或 1 MB |
系统调用更少,通常更快 |
用 vector 当缓冲区 |
vector<char> buf(BUF); → in.read(&buf[0], BUF) |
缓冲区大小可以运行时决定(13.1.1) |
| 先求大小、一次分配 | 用 fileSize 求总长,一次 read 读全 |
对中等大小的文件最快;对超大文件会吃光内存 |
真实工具(copy、robocopy、cp)的做法:大缓冲区 + 二进制模式 + 循环搬运 ——就是你写的那个 copyBinary。
再加一个实用改进:让"命令行参数"决定源和目标(7.8.3 讲过的 argc / argv):
int main(int argc, char *argv[]) {
if (argc != 3) {
cout << "用法: program <源文件> <目标文件>" << endl;
return 1;
}
long long n = 0;
if (copyBinary(argv[1], argv[2], n))
cout << "已复制 " << n << " 字节" << endl;
else
cout << "复制失败" << endl;
return 0;
}
这样就变成了一个真正能用的命令行工具(在 Dev-C++ 里通过「运行 → 参数」传参)。
五、30 秒实验
- 把
copyBinary里"处理零头"的那三行删掉,再跑 →cp_bin.txt会少最后 21 字节(因为 21 < 4096,第一次read就没读满 → 一块都没写出去)。这就是"漏掉零头"的 Bug。 - 把源文件换成一张图片(从网上下一个) → ① 逐字符复制出来的图片打不开(文本模式改坏了),③ 二进制复制的能正常打开。这是最有说服力的实验。
- 把
BUF改成 1,再复制一个大文件 → 慢得肉眼可见。证明"块越大越快"。 - 用复制出来的
cp_bin.txt当新源、再复制一次,反复几轮 → 大小一直不变(二进制复制是"无损"的)。换成文本方式反复复制 → 每轮都变大一些(\n被越插越多)。这个实验最能说明问题。
六、常见误区
- "复制文件只要读字符、写字符就行" ——对纯文本行,对任何含"非文本字节"的文件不行。文本模式会改字节。
- "加
ios::binary会让getline读不到行" ——不会。getline认的是\n这个字符,跟模式无关。二进制模式只是"不做转换",所以你读到的\n还是\n(在 Windows 上文本模式会把\r\n变成\n,二进制模式会原样读成两个字符)。 - "
read一次读不满就是出错了" ——不一定。最后一次读不满是正常的(文件长度不是块长的整数倍) ——要用gcount()取出真正读到的字节数。 - "复制完不用检查" ——要检查。源文件打不开、目标写不了(磁盘满/无权限)都会失败,不检查就会得到一个"看起来成功、其实是空的"目标文件。
- "复制出来的文件大小应该和源一样" ——二进制模式复制一定一样;文本模式复制可能不一样(换行符转换)。大小不一样本身就是一个警报:你用了错误的模式。
- "逐行复制比逐字符复制更准" ——不一定:
getline丢掉了换行符,要自己补\n,补得不对(比如原文件最后一行没有换行)就会多/少内容。逐字符和二进制块"漏字节"的风险更小。
自测三问(点开对答案)
1. **复制文件的三种实现是什么?哪一种能复制任意文件?为什么?** ——**逐字符、逐行、二进制块**。**只有二进制块能复制任意文件** ——**因为它用 `ios::binary` 打开,不做任何字节转换**;前两种是文本模式,**Windows 上会把 `\n` 变成 `\r\n`,破坏非文本文件。** 2. **二进制块复制时,为什么必须处理"最后一次没读满"的情况?** ——**因为文件的长度通常不是块长的整数倍**:**最后一次 `read` 会失败(没读满),但已经读到了若干个字节**,这些字节要用 **`gcount()` 取出来、单独写出去**。**不处理就会丢掉文件末尾一小截。** 3. **逐行复制有什么隐蔽的坑?** ——**`getline` 会把换行符读走丢掉**,**所以写的时候必须自己补 `"\n"`**;而且**如果源文件最后一行没有换行符,复制出来会多一个换行**(多一字节)。- ☐ 下一节(15.4 文件指针移动操作)
相关笔记
- 二进制文件的读-写 —— 上一节:
read/write的用法(含实例06) - 文件指针移动操作 —— 下一节:读位置和写位置
- 文件流 —— 读写闭环(含实例03)
- 使用数组作为函数参数 ——
argc/argv命令行参数 - 向量类模板 —— 用
vector<char>当缓冲区 - 🔷 C++基础 · 索引 —— 全课路线图