不能直接读整个大文件进内存算SHA256,因易OOM;应分块(如64KB)用EVP_DigestUpdate增量计算,注意二进制模式、gcount()判末块、EVP_DigestFinal_ex收尾及显式EVP_MD_CTX_free。

为什么不能直接读整个大文件进内存算 SHA256
因为会 OOM——哪怕只是 4GB 的文件,在 32 位环境或内存受限容器里,std::ifstream::read 一次性读取就可能失败;64 位下虽不崩,但占满几百 MB 内存毫无必要,还拖慢其他 IO。SHA256 本身支持增量更新(EVP_DigestUpdate 或 std::hasher::update),分块才是正解。
常见错误现象:std::bad_alloc、程序卡死、校验结果和 sha256sum 不一致(通常是因为没处理末尾不足块的边界)。
- 块大小建议选
65536(64KB):太小(如 1KB)系统调用开销大;太大(如 1MB)对缓存不友好,且在低内存设备上仍可能出问题 - 必须用二进制模式打开文件:
std::ios::binary,否则 Windows 下换行符会被悄悄转换,哈希值必然错 - 每次
read()后要检查gcount(),不能只看eof()——文件末尾读不满一块时,gcount()才是真实字节数
OpenSSL 的 EVP 接口怎么安全分块更新
别碰 SHA256_Init/SHA256_Update 这类底层 C 接口,它们不校验指针/长度,容易传错参数导致段错误或哈希错。用 EVP_MD_CTX + EVP_DigestInit_ex 是 OpenSSL 官方推荐路径,支持多线程安全重用上下文。
实操要点:
立即学习“C++免费学习笔记(深入)”;
- 初始化必须指定算法和 engine:
EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr),漏掉nullptr可能触发未定义行为 -
EVP_DigestUpdate第二个参数是const void*,传buffer.data()没问题,但确保 buffer 生命周期覆盖整个 update 过程 - 最后一定要调
EVP_DigestFinal_ex,不能只靠EVP_DigestUpdate——否则摘要没完成,输出全是 0 - 错误检查不能省:
EVP_DigestInit_ex和EVP_DigestFinal_ex返回 1 才成功,否则查ERR_print_errors_fp(stderr)
用 std::filesystem::file_size 避免重复 stat
有人边读边调 std::filesystem::file_size(path),其实没必要——它本质就是一次 stat() 系统调用。在校验前调一次就够了,还能提前判断文件是否为空或权限不足。
注意点:
- 如果文件被其他进程截断,
file_size返回的是初始大小,但读的时候可能提前 EOF;所以仍要依赖gcount()判断每块实际读了多少 - 某些网络文件系统(如 NFS)可能返回不准确 size,此时更得靠
gcount()做唯一依据 - 不要用
std::ifstream::seekg(0, std::ios::end)+tellg()替代——对管道、设备文件会失败,且不跨平台(Windows 下 text 模式下不可靠)
完整可跑的最小代码片段(含错误处理)
下面这段代码只依赖 OpenSSL 1.1.1+ 和 C++17,编译时加 -lssl -lcrypto,重点看三处:块循环、gcount() 使用、以及 final 调用时机:
#include <openssl/evp.h>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <vector>
<p>std::string sha256_file(const std::string& path) {
std::ifstream file(path, std::ios::binary);
if (!file.is_open()) return {};</p><pre class='brush:php;toolbar:false;'>auto size = std::filesystem::file_size(path);
std::vector<uint8_t> buffer(65536);
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
if (!ctx || EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr) != 1) {
EVP_MD_CTX_free(ctx);
return {};
}
while (file.read(reinterpret_cast<char*>(buffer.data()), buffer.size())) {
if (EVP_DigestUpdate(ctx, buffer.data(), file.gcount()) != 1) break;
}
// 处理最后一块(read() 失败但 gcount() > 0 表示 EOF 刚好在块中间)
if (file.gcount() > 0 && EVP_DigestUpdate(ctx, buffer.data(), file.gcount()) != 1) {
EVP_MD_CTX_free(ctx);
return {};
}
unsigned char md[EVP_MAX_MD_SIZE];
unsigned int md_len;
if (EVP_DigestFinal_ex(ctx, md, &md_len) != 1) {
EVP_MD_CTX_free(ctx);
return {};
}
EVP_MD_CTX_free(ctx);
std::string hash;
hash.reserve(md_len * 2);
for (unsigned int i = 0; i < md_len; ++i) {
char buf[3];
sprintf(buf, "%02x", md[i]);
hash += buf;
}
return hash;}
最容易被忽略的一点:EVP_MD_CTX 必须显式 EVP_MD_CTX_free,它不是 RAII 对象;漏掉会导致 OpenSSL 内部资源泄漏,尤其在频繁校验多个文件时,内存占用会缓慢上涨。



















