直接用 seekg(0, ios::end) 去算文件大小?这事儿真不靠谱,尤其是在文本模式下或者跨平台的时候。这不仅仅是 bug,而是 C++ 标准对流行为的明确定义——这种“未指定”特性,本身就意味着你不能依赖它。
为什么 seekg(0, ios::end) 在 ifstream 中可能返回错误大小
问题的根源在于文件的打开方式。ifstream 默认以文本模式(ios::in)打开文件,而文本模式下会有一些“看不见”的字符转换:
- 在 Windows 上,文件中的
\r\n会被当作一个字符\n读取,但底层文件指针仍按真实字节数跳转。这就导致seekg(0, ios::end)返回的是真实字节数,与后续tellg()的语义完全对不上。 - C++ 标准明确说了:文本流的
seekg到ios::end的结果是“未指定”(unspecified)。也就是说,编译器可以自由实现,GCC/Clang 在文本模式下经常返回 -1 或一个随意值。 - 更糟的是,某些 libc++ 实现甚至直接拒绝这个操作,直接置位
failbit,让你彻底拿不到结果。
正确做法:必须用二进制模式打开 + 检查流状态
只有二进制模式才能保证 seekg 和 tellg 映射到真实的字节偏移。操作路径其实很清晰:
- 用
ios::binary标志打开文件,比如:std::ifstream file("data.bin", std::ios::binary) - 调用
file.seekg(0, std::ios::end)后,必须检查file.good()或!file.fail(),一旦失败就不能继续。 - 再调用
file.tellg()获取长度,返回类型是std::streampos,需要转为std::size_t。这里有个坑:如果文件超过 2GB 且系统的streampos是 32 位,结果会被截断。 - 最后,别忘了
file.seekg(0)回到开头,否则后续所有读取操作都会从文件末尾开始,直接返回 EOF。
一段标准代码示例:
std::ifstream file("input.txt", std::ios::binary);
if (!file.is_open()) return -1;
file.seekg(0, std::ios::end);
if (!file.good()) return -1; // seek 失败,比如文件不可寻址(管道、stdin)
std::streampos size = file.tellg();
file.seekg(0); // 重置读位置
更健壮的替代方案:绕过流,用系统 API
如果你的目标只是获取文件大小,而不是后续用同一个 ifstream 读取内容,那么推荐直接使用系统原生接口,彻底避开流模式的陷阱:
- Linux/macOS:用
stat(),st_size字段直接给出字节数,不依赖文件是否可 seek。 - Windows:用
GetFileSizeEx(),支持大文件且无需打开文件句柄。 - C++17 起,直接用
std::filesystem::file_size(path),内部自动选择最优系统调用,且明确不抛异常(仅设置ec)。
C++17 示例:
std::error_code ec;
auto size = std::filesystem::file_size("input.txt", ec);
if (ec) { /* 处理错误 */ }
常见误用与静默陷阱
下面这些写法看起来简洁,实际隐患很大:
- 省略
ios::binary:在 Windows 上用ifstream f("a.txt"); f.seekg(0, ios::end);,tellg()经常返回 -1。 - 不检查
good()就直接用tellg():如果seekg失败,tellg()也返回 -1,但程序可能继续运行,导致后续逻辑完全崩溃。 - 用
int接收tellg()返回值:32 位int无法表示超过 2GB 的文件,应使用std::streamoff或显式static_cast。 - 忘记
seekg(0):后续的getline()或read()会立即遇到 EOF,读不到任何内容。
一句话总结:真正安全的文件大小获取,从来不是“一行代码的事”。取决于你是否愿意接受文本模式的不确定性——如果必须用流,就老老实实开二进制模式并检查每一步;如果只是要大小,filesystem 或 stat 更直接、更可移植。