简介:这份资源面向具备一定C++基础、需要在Windows项目中集成压缩解压能力的开发者,聚焦于通过7z SDK实现文件解压这一实用场景。压缩包共488个文件,以186个h头文件与136个cpp源文件为核心,辅以40个c文件、71个obj中间文件及若干makefile、dsp、dsw等工程配置,整体约2.9MB,完整保留了7z SDK的源码结构与Visual Studio工程组织,便于直接编译调试。内容涉及IInArchive接口调用、CLibArchive实现类、Open与Extract方法使用、文件列表遍历及错误码处理等关键环节,并覆盖Lzma编解码、Ppmd7等底层算法模块,可帮助读者理解非内存解压的按需读取机制与CPU占用特点。目前已有1822人学习下载,适合希望掌握7z SDK集成、多文件解压与线程安全优化的中高级开发者参考。
1. C++ 调用7z SDK解压:为什么我最后放弃了命令行方案
去年做一个跨平台桌面工具时,需要在程序里静默解压一批.7z和.zip混合包。第一版图省事,直接system("7z x ...")拼命令行,结果在中文路径、带空格的文件名、以及用户机器上没装 7-Zip 的环境里连续翻车。命令行方案看着简单,实际是把进程创建、路径转义、错误码解析、依赖分发四件事全甩给了自己,而且每台机器的 7z 版本行为还不一致。
后来换成 C++ 直接调用 7z SDK(也就是 7-Zip 官方那套 C++ 源码里的解压接口),把解压逻辑编进自己的进程,路径、进度、错误码全部可控,也不再依赖用户环境。这篇就把这条路走通:SDK 从哪来、怎么编、核心接口怎么调、参数怎么设、哪些坑我踩过。适合已经会写 C++、需要在程序内嵌解压能力、又不想被外部进程绑架的开发者。新手能照着把最小 demo 跑起来,熟手能直接看边界和参数。
2. 7z SDK 的组成与最小可编译工程
2.1 SDK 里到底有哪几块,别一股脑全塞进工程
7-Zip 官方发布的源码包里,和解压相关的核心目录大致是这几块,理解它们的分工能帮你少编一堆没用的文件:
| 目录/文件 | 作用 | 是否必须 |
|---|---|---|
C/ | 纯 C 写的底层压缩/解压算法与 7z 格式解析 | 必须 |
CPP/7zip/ | C++ 封装层,IInArchive等接口在这里 | 必须 |
CPP/Common/ | 字符串、文件流、异常等公共设施 | 必须 |
CPP/Windows/ | Windows 平台封装(文件、注册表等) | Windows 下必须 |
CPP/7zip/Bundles/ | 官方给的示例工程,含 Format7zExtract | 参考用 |
关键点是:你不需要把整个 SDK 编成库。7z SDK 的惯例做法是把需要的.cpp直接加进你自己的工程一起编译,而不是先编出一个7z.lib再链接。官方Bundles/Format7zExtract就是一个「把解压所需源文件列成清单」的示例工程,照着它的文件列表抄是最省事的路径。
提示:不同版本的 SDK 目录结构会有微调,但
C/、CPP/7zip/、CPP/Common/这三块的分工基本稳定。以你手上那份源码的实际结构为准。
2.2 用 CMake 把解压所需源文件组织起来
我一般不去手动维护一长串源文件列表,而是用 CMake 的file(GLOB_RECURSE ...)把必要目录收进来,再排除掉压缩、UI、命令行相关的部分。下面是一个能跑通解压的最小CMakeLists.txt骨架:
cmake_minimum_required(VERSION 3.15) project(sevenzip_extract_demo CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 7z SDK 源码根目录,按你本地实际路径改 set(SEVENZIP_ROOT "${CMAKE_SOURCE_DIR}/third_party/7zip") # 收集解压必需的源文件 file(GLOB_RECURSE SEVENZIP_SOURCES "${SEVENZIP_ROOT}/C/*.c" "${SEVENZIP_ROOT}/CPP/7zip/Common/*.cpp" "${SEVENZIP_ROOT}/CPP/7zip/Archive/*.cpp" "${SEVENZIP_ROOT}/CPP/Common/*.cpp" ) # 排除掉压缩编码器、UI、命令行等解压用不到的部分,减小体积 list(FILTER SEVENZIP_SOURCES EXCLUDE REGEX ".*/Compress/.*") list(FILTER SEVENZIP_SOURCES EXCLUDE REGEX ".*/UI/.*") list(FILTER SEVENZIP_SOURCES EXCLUDE REGEX ".*/Bundles/.*") add_executable(sevenzip_extract_demo src/main.cpp ${SEVENZIP_SOURCES} ) target_include_directories(sevenzip_extract_demo PRIVATE "${SEVENZIP_ROOT}" "${SEVENZIP_ROOT}/CPP" ) # Windows 下需要链接的库 if(WIN32) target_link_libraries(sevenzip_extract_demo PRIVATE ole32 oleaut32 user32 advapi32) endif()逻辑说明:file(GLOB_RECURSE)把C/下的 C 文件和CPP/下的 C++ 文件全部收进来,这是 7z SDK 的常规用法——它本身就不是为「预编译成库」设计的。list(FILTER ... EXCLUDE)把压缩编码器、UI、命令行示例排除掉,能显著减少编译时间和最终二进制体积,因为解压只需要解码器,不需要编码器。
参数说明:SEVENZIP_ROOT指向你解压出来的 SDK 源码根目录,务必改成实际路径。target_include_directories里同时加了根目录和CPP目录,是因为 SDK 内部头文件引用既有#include "7zip/..."也有#include "Common/..."两种写法,两个 include 路径都要给。Windows 下ole32、oleaut32这些是 SDK 内部用到 COM 相关设施时需要的,漏了会在链接期报一堆未解析符号。
2.3 第一次编译最常见的三类报错
第一次编 7z SDK 基本不会一次过,我遇到的高频报错就三类。第一类是「找不到头文件」,多半是 include 路径没给全,把 SDK 根目录和CPP目录都加上基本能解决。第二类是「未解析的外部符号」,通常是某个.cpp没被收进源文件列表,或者 Windows 下漏链了系统库,对着报错的符号名去 SDK 里搜它定义在哪个文件,补进去即可。第三类是字符集问题,SDK 内部大量使用wchar_t宽字符,工程如果设成多字节字符集,会在字符串转换处报错,统一用 Unicode 字符集最省心。
注意:不要试图只挑几个
.cpp编,7z 的格式解析和编解码器之间有大量交叉引用,漏一个文件往往引发连锁的未解析符号。宁可先全收进来编通,再逐步裁剪。
3. 用 IInArchive 接口跑通一次解压
3.1 打开压缩包、遍历条目、定位你要解的文件
7z SDK 解压的核心接口是IInArchive。整体流程是:创建对应格式的处理器对象 → 用Open打开文件流 → 用GetNumberOfItems和GetProperty遍历条目 → 用Extract解压。下面这段代码演示如何打开一个包并打印所有条目名:
#include "7zip/Archive/IArchive.h" #include "7zip/IPassword.h" #include "Common/MyCom.h" #include "Windows/PropVariant.h" // 假设已通过 CreateObject 拿到某个格式的 IInArchive 实例 archive // 以及一个打开好的 IInStream 实例 stream CMyComPtr<IInArchive> archive; CMyComPtr<IInStream> stream; // 打开压缩包,第二个参数是最大检查深度,-1 表示不限制 HRESULT hr = archive->Open(stream, nullptr, nullptr); if (hr != S_OK) { // 打开失败,可能是格式不匹配或文件损坏 return; } UInt32 numItems = 0; archive->GetNumberOfItems(&numItems); for (UInt32 i = 0; i < numItems; ++i) { // 取条目路径,属性 ID 用 kpidPath NWindows::NCOM::CPropVariant prop; archive->GetProperty(i, kpidPath, &prop); if (prop.vt == VT_BSTR) { // prop.bstrVal 就是宽字符形式的条目路径 // 这里可以判断是不是你要解的那个文件 } // 取条目大小,属性 ID 用 kpidSize NWindows::NCOM::CPropVariant sizeProp; archive->GetProperty(i, kpidSize, &sizeProp); }逻辑说明:Open的第二个参数是「最大检查深度」,传nullptr表示用默认行为;第三个参数是密码回调,不需要密码时传nullptr。GetNumberOfItems拿到条目总数后逐个GetProperty,kpidPath取路径、kpidSize取原始大小、kpidIsDir判断是不是目录。CPropVariant是 SDK 对PROPVARIANT的封装,取值前一定要判断vt类型,直接读bstrVal而不判类型是新手最常见的崩溃来源。
参数说明:kpidPath返回的是包内相对路径,可能是a/b/c.txt这种带子目录的形式,解压时目录结构要自己维护。kpidSize对目录条目可能无效,遍历时先判kpidIsDir再决定要不要读大小。
3.2 配置解压参数:输出目录、覆盖策略、密码
真正解压用Extract方法,它接受一个索引数组和一个IArchiveExtractCallback回调。输出路径、是否覆盖、密码这些都在回调里控制。下面是一个最小回调实现的关键部分:
class ExtractCallback : public IArchiveExtractCallback, public ICryptoGetTextPassword { public: // 省略 COM 引用计数样板代码 // SDK 通过这个回调询问每个条目的输出流 STDMETHOD(GetStream)(UInt32 index, ISequentialOutStream** outStream, Int32 askExtractMode) override { *outStream = nullptr; // 取当前条目的路径 NWindows::NCOM::CPropVariant prop; _archive->GetProperty(index, kpidPath, &prop); if (prop.vt != VT_BSTR) return S_OK; // 拼出完整输出路径:输出目录 + 包内相对路径 UString fullPath = _outputDir + L"/" + prop.bstrVal; // 创建输出文件流,覆盖策略在这里决定 // 常见做法:先尝试创建,已存在则按配置决定覆盖或跳过 _outFileStream = new COutFileStream; if (!_outFileStream->Create(fullPath, /*overwrite=*/true)) { return E_ABORT; } // 把输出流交给 SDK *outStream = _outFileStream; (*outStream)->AddRef(); return S_OK; } // 需要密码时 SDK 会调这个 STDMETHOD(CryptoGetTextPassword)(BSTR* password) override { // 把用户提供的密码写进 *password return S_OK; } private: CMyComPtr<IInArchive> _archive; UString _outputDir; CMyComPtr<COutFileStream> _outFileStream; };逻辑说明:GetStream是解压回调里最核心的方法,SDK 每处理一个条目就调一次,问你「这个条目要写到哪个流」。你在里面拼出输出路径、创建文件流、把流指针交回去。askExtractMode参数区分是「解压数据」还是「只创建目录」,目录条目要单独处理。CryptoGetTextPassword只在包加密时被调用,不加密的包不会走到这里。
参数说明:COutFileStream::Create的第二个参数控制覆盖行为,传true表示已存在就覆盖,传false则在文件已存在时失败。实际产品里我一般会先判断文件是否存在,再按用户配置决定覆盖、跳过还是重命名,而不是无脑覆盖。输出路径拼接时要注意包内路径可能含..,必须做路径规范化,否则会有目录穿越风险。
3.3 触发解压并处理返回值
配置好回调后,调用Extract触发实际解压:
CMyComPtr<ExtractCallback> callback = new ExtractCallback(archive, outputDir); // 第一个参数传 nullptr 表示解压全部条目 // 也可以传一个 UInt32 数组只解压指定索引 HRESULT hr = archive->Extract(nullptr, (UInt32)(Int32)-1, /*testMode=*/false, callback); if (hr == S_OK) { // 全部成功 } else if (hr == S_FALSE) { // 部分成功,可能有条目被跳过 } else { // 失败,hr 里是错误码 }逻辑说明:Extract第二个参数是索引数组的元素个数,传(UInt32)(Int32)-1是 SDK 约定的「全部条目」写法。testMode传true时只校验不写文件,可以用来做完整性检查。返回值S_FALSE表示有部分条目没成功,比如某个文件被占用跳过,这种情况不能当成完全失败,要结合回调里记录的具体信息判断。
参数说明:如果只想解压包里的某几个文件,构造一个UInt32索引数组传进去,第二个参数改成数组长度。索引就是前面遍历时GetProperty用的那个i。
4. 避坑:调用 7z SDK 时我踩过的五个坑
4.1 中文路径乱码,解压出来文件名全是问号
现象:包内条目名是中文时,解压出来的文件名变成乱码或问号。原因:7z 内部统一用wchar_t宽字符,而很多人在拼输出路径时用了std::string或char*,中间发生了一次有损转换。解决:全程用UString(SDK 的宽字符串类型)拼接路径,只在最后调用系统 API 创建文件时才转成平台需要的类型,Windows 下用CreateFileW而不是CreateFileA。
4.2 大文件解压到一半内存暴涨
现象:解压几百 MB 以上的文件时进程内存持续上涨。原因:GetStream里每次 new 一个COutFileStream却没在条目处理完后释放,或者把整个文件读进了内存缓冲。解决:确保每个条目的输出流在SetOperationResult回调里被正确释放,用流式写入而不是一次性缓冲。SDK 本身是流式处理的,内存暴涨基本都是回调里自己攒的。
4.3 密码包调用 Extract 直接返回失败
现象:加密包解压时Extract返回错误码,没有任何提示。原因:没有实现ICryptoGetTextPassword接口,或者实现了但没在Open时把回调传进去。解决:回调类同时继承IArchiveExtractCallback和ICryptoGetTextPassword,在Open的第三个参数传入实现了密码接口的对象,CryptoGetTextPassword里返回正确密码。密码错误时 SDK 会再次调用该接口,要注意限制重试次数,否则会死循环。
4.4 路径穿越:包内../把文件写到了输出目录外
现象:某些恶意构造的包解压后,文件出现在了输出目录之外。原因:直接拿kpidPath拼输出路径,没有过滤..。解决:拼路径前对包内相对路径做规范化,拒绝任何包含..或以/开头的条目,确保最终路径始终在输出目录之下。这是安全底线,不是可选项。
4.5 编译通过但运行时报「类未注册」
现象:程序启动后创建解压对象失败,提示类未注册或返回CLASS_E_CLASSNOTAVAILABLE。原因:没有把对应格式的编解码器.cpp编进工程,或者CreateObject时传的 CLSID 和实际编进去的格式对不上。解决:确认你要解的格式(7z、zip、rar 等)对应的编解码器源文件都在编译列表里,CreateObject用的 CLSID 要和格式匹配。只编了 7z 却去解 zip,就会报这个错。
5. 进阶:把解压封装成可复用组件并做完整性校验
跑通最小 demo 之后,真正要落地的是把它封装成一个干净的组件,并加上校验能力。我一般的做法是定义一个SevenZipExtractor类,对外只暴露Open、List、ExtractAll、ExtractTo四个方法,把 COM 引用计数、回调生命周期、错误码翻译全部藏在内部。这样业务代码里看不到任何 SDK 类型,将来换实现也不影响调用方。
完整性校验这块,Extract的testMode参数很好用。传true时 SDK 只解压到内存做 CRC 校验、不写磁盘,可以在正式解压前先跑一遍,确认包没损坏再动手。代价是要读两遍数据,对大包可以做成可选。下面是一个校验加解压的组合用法:
// 先做完整性校验,testMode = true 不写磁盘 HRESULT testHr = archive->Extract(nullptr, (UInt32)(Int32)-1, /*testMode=*/true, testCallback); if (testHr != S_OK) { // 包损坏或密码错误,直接返回,不要进入正式解压 return false; } // 校验通过再正式解压 HRESULT hr = archive->Extract(nullptr, (UInt32)(Int32)-1, /*testMode=*/false, extractCallback);参数说明:testMode为true时回调里的GetStream仍会被调用,但askExtractMode会是校验模式,此时不要真的创建文件,返回一个丢弃型的流即可。校验回调可以复用解压回调,只要在GetStream里根据askExtractMode分支处理。
进度反馈也值得单独做。SDK 通过IArchiveExtractCallback::SetTotal和SetCompleted两个回调汇报总大小和已完成大小,把这两个值接到你的 UI 进度条上即可。注意SetTotal给的是所有待解压条目的总字节数,SetCompleted是累计完成量,两者单位一致,直接相除就是进度百分比。我踩过的坑是:某些包的SetTotal可能给 0,这时要做除零保护,退化成按条目数算进度。
最后说个习惯问题。7z SDK 的接口大量返回HRESULT,我见过太多代码只判断== S_OK就完事,把S_FALSE这种「部分成功」当成失败,或者把真正的错误码直接吞掉。我的做法是写一个HrToString辅助函数,把常见错误码翻译成人能看懂的话,出错时连同条目索引一起记进日志。解压这种批量操作,出问题时能定位到是哪个文件、哪一步,比什么都重要。这套封装我在两个项目里复用下来,最大的体会是:SDK 本身很稳,翻车基本都翻在回调的生命周期和路径处理上,把这两块盯死,剩下的就是体力活。希望帮到你。
本文还有配套的精品资源,点击获取