
简介这是面向C/C开发者的MINIZIP压缩解压库资源包基于稳定的ZLIB库封装提供简洁易用的API能方便地在应用程序中集成ZIP压缩与解压缩功能。资源已预编译好静态库和配套头文件共7个文件压缩包仅137KB非常轻量特别适合嵌入式或资源受限环境。包含5个头文件用于接口声明2个静态库文件供链接使用开发者只需在工程中配置链接路径和包含目录即可直接调用相关接口免去自行下载源码和编译依赖的繁琐过程。该库具备跨平台能力支持Windows、Linux、macOS同时支持AES-256加密保护能处理UTF-8等多字节编码文件名并特别加入了全局与局部解压进度显示接口便于在界面中呈现文件处理状态。已有877人学习下载对于想快速为项目添加ZIP读写能力、又不想深入钻研底层算法的C/C开发者而言是一份省时省力的现成工具包。1. 拿到 minizip 静态库先确认它能帮你省掉哪些麻烦做 Windows 客户端工具的人迟早会遇到这种需求把配置目录压成一个 zip 交给用户或者启动时把资源包解到临时目录。网上能搜到一堆封装好的动态库但真正能安安静静嵌进 C/C 工程、不带一堆运行时依赖的反而是 zlib 自带的 minizip 源码加两个静态库。标题里这个组合——minizip 负责 zip 容器的条目管理zlib 负责底层 deflate 压缩——打包出来的 exe 只要几个文件就能跑。适合两类人被动态库依赖折腾过、想一步到位链接静态库的 VS 开发者以及需要在代码里精确控制压缩流程加密、目录层级、进度而不想引入整套高级库的中间件开发者。这套路线不是最花哨的却是最省心的。2. zlib 与 minizip 的分工为什么两个 lib 必须成对出现2.1 zlib 管 deflate 算法minizip 管 zip 容器zlib 本身只负责把一段内存压缩成 deflate 流或者反过来解压。它不关心文件名叫什么、目录结构长什么样、注释写在哪。真正把这些信息组织成 zip 文件格式的是 minizip。minizip 相当于站在 zlib 的肩膀上用 zlib 提供的 compress/uncompress 原语把 zip 的 local header、central directory、条目清单这些容器细节全部处理掉。所以你的工程里必须同时有 zlib 的头文件和库文件否则 minizip 里那些对 zlib 导出函数的调用会全部变成链接错误。这两个库是分层关系而不是并列关系。用 minizip 写程序时你拿到的 API 是 zip.h 和 unzip.h 里的函数zipOpen64、unzOpen64、zipWriteInFileInZip、unzReadCurrentFile这些函数内部才会去调用 zlib 的 deflate/inflate 系列接口。对业务代码来说zlib 是透明的但它始终得在链接列表里躺着。一个常见的误解是认为 minizip 和 zlib 是二选一的关系实际上少了 zlibminizip 连一个压缩字节都产不出来。2.2 zlibstatic.lib 和 zlib.lib 不是同一个东西zlib 官方在 Windows 下会同时生成两个产物zlib1.dll 配合 zlib.lib以及 zlibstatic.lib。前者是动态库方案编译出来的 exe 运行时要带 zlib1.dll后者是静态库zlib 的代码会整个编进你的 exe。标题里给的是 zlibstatic.lib意味着分发工具类程序时不用再带一个 dll这对“双击就能跑”的桌面小工具非常关键。项目里 minizip.lib 则是对 minizip 源码zip.c、unzip.c 等编译出的静态库两个 lib 一起链最终 exe 里同时包含 zlib 的过滤器和 minizip 的容器逻辑。要注意 zlibstatic.lib 和 minizip.lib 必须出自同一套构建环境。如果 minizip 是从某个旧版本 zlib 源码包里的 contrib 目录编出来的而 zlibstatic.lib 是新版本两个库可能因为结构体布局不一致而运行时装在一起就崩。拿到别人给的 lib 时最稳妥的做法是先确认两者来自同一个源码包。这个没法用肉眼判断所以我自己拿到新 lib 的第一件事就是跑一个最小解压示例能跑通再往上叠业务。2.3 在 VS 工程里把这两个 lib 配好路径与依赖项顺序在 Visual Studio 里配静态库属于基本功但步骤顺序错了会引出各种奇怪报错。常见做法是这样的把 zlib 的 include 目录和 minizip 的 include 目录也就是 zip.h 和 unzip.h 所在目录加到 工程属性 → VC 目录 → 包含目录。把两个 lib 文件所在目录加到 VC 目录 → 库目录。在 链接器 → 输入 → 附加依赖项 里按 minizip.lib 在前、zlibstatic.lib 在后的顺序填。附加依赖项的顺序不是玄学。minizip.lib 里的目标文件引用了 zlib 的函数符号老的链接器需要依赖库写在被依赖库后面新版的 VC 有时能自动处理但保守起见把被依赖的 zlibstatic.lib 写在最后能避开很多“无法解析的外部符号”的翻车现场。加完这些配置第一件要做的事不是写业务代码而是先写一个空壳验证一下头文件路径和库能否同时被编译器找到#include stdio.h #include zlib.h #include zip.h int main(void) { printf(zlib version: %s\n, zlibVersion()); zipFile zf zipOpen64(test.zip, APPEND_STATUS_CREATE); if (zf NULL) { printf(zip open failed\n); return -1; } zipClose(zf, NULL); printf(lib setup ok\n); return 0; }这段代码同时引用了 zlib.h 和 zip.h并且调用了 zipOpen64 和 zipClose。如果它编译链接通过说明 include 路径和 lib 依赖项基本没问题如果报找不到 zlibVersion优先查 zlib 头文件和库目录有没有配齐如果报无法解析的外部符号先查附加依赖项顺序和位数。连接库这种事先把最小案例跑通再往里面填业务代码才是正路。如果用 CMake 构建对应的配置写法是cmake_minimum_required(VERSION 3.10) project(zip_demo C) add_executable(zip_demo main.c) target_include_directories(zip_demo PRIVATE path/to/zlib path/to/minizip) target_link_libraries(zip_demo PRIVATE minizip zlibstatic)target_link_libraries 里同样把 zlibstatic 写在 minizip 后面。这里有个细节VS 的库目录和 CMake 的 target_include_directories 是两套机制但共同点是两个库必须同时进链接命令。只加 minizip.lib 不加 zlibstatic.lib链接器会报一堆 zlib 符号找不到这是最高频的错误之一。3. 用 zip.h / unzip.h 写出压缩和解压的最小可跑代码3.1 解压侧核心循环从 unzOpen64 到 unzClose 的调用链展开 zip 包的标准姿势是先按条目遍历逐条读出文件名和内容。这里给出一个在 Windows VS 下可直接编译的解压函数#include stdio.h #include string.h #include direct.h #include unzip.h int extract_all(const char* zip_path, const char* out_dir) { unzFile uf unzOpen64(zip_path); if (!uf) { printf(cant open zip: %s\n, zip_path); return -1; } unz_global_info64 gi; unzGetGlobalInfo64(uf, gi); printf(entry count: %llu\n, gi.number_entry); for (unsigned long i 0; i gi.number_entry; i) { char name[512]; unz_file_info64 fi; unzGetCurrentFileInfo64(uf, fi, name, sizeof(name), NULL, 0, NULL, 0); char full_path[1024]; _snprintf(full_path, sizeof(full_path), %s/%s, out_dir, name); // zip 里的目录条目以 / 结尾先建目录不落文件 size_t len strlen(full_path); if (len 0 full_path[len - 1] /) { _mkdir(full_path); if (i 1 gi.number_entry) unzGoToNextFile(uf); continue; } // 打开当前条目加密包在这里要换 unzOpenCurrentFilePassword if (unzOpenCurrentFile(uf) ! UNZ_OK) { printf(open entry failed: %s\n, name); if (i 1 gi.number_entry) unzGoToNextFile(uf); continue; } FILE* out fopen(full_path, wb); if (!out) { printf(create file failed: %s\n, full_path); unzCloseCurrentFile(uf); if (i 1 gi.number_entry) unzGoToNextFile(uf); continue; } char buf[8192]; int n; while ((n unzReadCurrentFile(uf, buf, sizeof(buf))) 0) { fwrite(buf, 1, (size_t)n, out); } fclose(out); unzCloseCurrentFile(uf); if (i 1 gi.number_entry) unzGoToNextFile(uf); } unzClose(uf); return 0; }这套调用链里每个函数都有明确分工unzOpen64 打开 zip 文件并定位到第一个条目unzGetGlobalInfo64 拿到总条目数用于 for 循环边界unzGetCurrentFileInfo64 取出当前条目的文件名和元信息unzOpenCurrentFile 真正开始解压当前条目它会负责 inflate 流的初始化和密码校验unzReadCurrentFile 每次返回一块解压后的原始数据返回 0 表示当前条目读完返回负数表示出错unzGoToNextFile 跳到下一个条目。还有 unzCloseCurrentFile 必须和解压成功后的 unzOpenCurrentFile 成对出现否则会泄露 zlib 内部申请的窗口缓冲区。缓冲区的选择我一般用 8192 字节。太小会导致 fwrite 调用次数暴涨太大在栈上分配有压力而且对压缩率几乎没影响。还有一个安全细节如果 name 里带../直接把 out_dir 和 name 拼起来会写出到目标目录之外做产品时必须检查 entry_name 不能包含..段或绝对路径。3.2 压缩侧核心流程从 zipOpen64 到 zipClose 的写入流程压缩单个文件的代码比解压更直观因为不需要遍历既有结构只要把源文件读出来往 zip 条目里写#include stdio.h #include zip.h int compress_one(const char* zip_path, const char* file_path, const char* entry_name) { zipFile zf zipOpen64(zip_path, APPEND_STATUS_CREATE); if (!zf) { printf(cant create zip: %s\n, zip_path); return -1; } FILE* src fopen(file_path, rb); if (!src) { zipClose(zf, NULL); printf(cant open source: %s\n, file_path); return -1; } zip_fileinfo zi {0}; // 全 0 表示不写入时间戳等附加信息 int err zipOpenNewFileInZip64( zf, entry_name, zi, NULL, 0, NULL, 0, created by zip_demo, // 条目注释 Z_DEFLATED, Z_DEFAULT_COMPRESSION); if (err ! ZIP_OK) { fclose(src); zipClose(zf, NULL); printf(create entry failed, err%d\n, err); return -1; } char buf[16384]; size_t rd; while ((rd fread(buf, 1, sizeof(buf), src)) 0) { if (zipWriteInFileInZip(zf, buf, (unsigned)rd) ! ZIP_OK) { printf(write entry failed\n); break; } } fclose(src); zipCloseFileInZip(zf); zipClose(zf, NULL); return 0; }zipOpen64 的第二个参数是打开模式。APPEND_STATUS_CREATE 表示新建或覆盖APPEND_STATUS_ADDINZIP 表示往已有 zip 追加条目。zipOpenNewFileInZip64 是这一族函数里参数最全的版本核心几个参数要说明一下entry_name 是 zip 内的相对路径必须用正斜杠分隔目录比如写cfg/config.ini表示解压后会生成 cfg 目录method 填 Z_DEFLATED 表示用 deflate 压缩填 Z_STORED 表示仅存储不压缩level 填 Z_DEFAULT_COMPRESSION 即可它对应 zlib 内部的中等压缩档位。zip_fileinfo 结构体里带时间日期字段如果把它全部清零zip 里不会记录文件修改时间某些严格校验的场景可能不友好。写入循环里fread 读出的字节数直接传给 zipWriteInFileInZip这是同步的阻塞调用写完才返回。对于大文件这个循环天然不会把所有内容加载进内存每次只有 16KB 的中间缓冲这是 minizip 相比某些一次 load 全部内容的高级封装库的优势。压缩完成后必须依次调用 zipCloseFileInZip 先收尾当前条目再 zipClose 收尾整个 zip 文件顺序反了会写出损坏的 central directory。3.3 压缩级别怎么选从速度到体积的取舍zipOpenNewFileInZip64 的 level 参数是压缩质量与 CPU 开销的权衡点。实际开发里Z_DEFAULT_COMPRESSION 和 Z_BEST_SPEED 用得最多Z_BEST_COMPRESSION 极少有必要。可以按这个表来选级别常量典型场景0Z_NO_COMPRESSION已压缩格式jpg、mp4、png或追求速度1Z_BEST_SPEED日志打包、临时缓存 zip6Z_DEFAULT_COMPRESSION配置文件、文本、json 等常规数据9Z_BEST_COMPRESSION安装包体积敏感且不在乎耗时补充一个实际观察文本类数据 level 1 到 level 6 的压缩率差异可能有十几个百分点但 level 6 再往上到 level 9提升通常只有一两个点耗时却成倍增加。所以默认别用 9除非你的目标 zip 要分发到网络且源数据是纯文本。还有一个冷知识如果源文件本身已经是压缩格式比如 .png再用 deflate 压一遍收益极小此时选 Z_NO_COMPRESSION 加 raw 标志反而快很多。4. 链接 Minizip 静态库的 5 个避坑记录4.1 CRT 运行时库不一致LNK2005 或一运行就崩现象链接时报 LNK2005提示_free已在 libcmt.lib 与 msvcrt.lib 中定义偶尔链接能过但程序运行在 malloc/free 上崩溃。原因minizip.lib 编译时用的运行时库和工程当前设置不一致。VS 里 /MT 是静态链接 CRT/MD 是动态链接 CRT一个工程混用了两种就会有两份 C 运行时的符号冲突。静态库这种问题特别常见因为库作者编译时大概率用的是 /MT而 VS 新工程默认是 /MD。解决在项目属性 → C/C → 代码生成 → 运行库里把工程切换成和 lib 一致的那一项。如果不知道 lib 用的哪种打开 VS 开发人员命令行执行dumpbin /directives minizip.lib | findstr DEFAULTLIB能直接看到它依赖的是 libcmt/MT还是 msvcrt/MD。这条命令能救命的次数比想象中多。4.2 ZLIB_WINAPI 宏导致的一片未解析外部符号现象链接时报_inflate、_deflate、_crc32等一堆 zlib 函数无法解析但 zlibstatic.lib 明明加在依赖项里。原因zlib 在 Windows 下有个 ZLIB_WINAPI 宏定义后函数会用 __stdcall 调用约定符号名会变成带后缀的修饰名。如果 zlib 源码编译时开了这个宏而你的工程没在预处理里定义它那么头文件声明的调用约定和 lib 里的实际符号对不上链接器找遍整个 lib 都找不到。解决在项目属性的预处理定义里加上 ZLIB_WINAPI再重新编译。也可以用dumpbin /exports zlibstatic.lib看导出的符号如果看到_deflate说明是按默认 cdecl 编译的此时不要加 ZLIB_WINAPI如果看到_deflate8就必须加。这条规则同样适用于 minizip 自己的调用约定因为 minizip 的头文件会引用 zlib.h。4.3 中文条目名解出来是乱码现象在 zip 里明明是“配置.xml”解压出来变成éཙ之类的乱码或者资源管理器打不开。原因minizip 写文件名时按原始字节写入。Windows 下代码页是 GBK而 zip 规范更推荐 UTF-8两个口径对不上就会出现乱码。老版本 7-Zip 按本地代码页解析新的解压工具按 UTF-8 解析标准不一致导致两边互相猜。解决压缩时在 zipOpenNewFileInZip64 的 flagBase 参数里传0x800这个 bit 表示条目名是 UTF-8 编码解压工具会按 UTF-8 处理。解压时则反过来读取条目名后判断是否带 UTF-8 标志然后用 MultiByteToWideChar 转成宽字符再用宽字符 API如 CreateFileW创建文件。直接拿 char 型文件名 open 一个中文路径在中文系统上时好时坏这是最典型的血泪经验。4.4 lib 位数不匹配x64 的库装进 x86 工程现象链接报无法解析的外部符号或者程序启动后随机关闭没有任何异常信息。原因minizip.lib 和 zlibstatic.lib 是 x64 编译的但工程活动平台是 Win32。32 位进程加载 64 位静态库的代码轻则符号找不到重则运行到某条指令直接非法访问。解决确认 VS 顶部配置管理器里活动解决方案平台是 x64或者在链接器命令里检查 /MACHINE:X64 参数。可以用dumpbin /headers minizip.lib看机器类型x64 的库显示machine (x64)x86 显示machine (x86)。标题给的 lib 具体是哪个平台以你拿到的文件为准但配置工程时第一件事就是核对这一点。4.5 minizip 和 zlib 版本不配套运行期随机崩现象整个程序编译链接都通过unzOpen 也返回正常但解压到某个文件时崩溃或者 zipClose 时崩溃而且不是必现。原因minizip 内部会直接操作 zlib 的 z_stream 结构体字段不同版本的 zlib 可能调整过结构体布局或内部约定。用新 zlib 配老 minizip 源码编出来的库内存布局对不上越界写发生在你看不见的地方。解决把 minizip 源码和 zlib 源码放在同一目录下用同一个工程、同一次构建产出两个静态库。zlib 官方源码包的 contrib/minizip 目录里就带着配套的 minizip这是最不容易出错的组合。静态库这玩意儿最怕的就是“这个 lib 是这个版本、那个 lib 是那个版本”的拼凑玄学般的问题大多出在这上面。5. 大文件、加密、多文件打包这些边界条件决定你用什么接口5.1 超过 2GB 的包无脑用 64 位接口minizip 老接口 zipOpen、unzOpen 内部用的是 long 型 ftell/fseek在 Windows 上 long 是 32 位超过 2GB 就会溢出。标题场景里如果只打包配置文件可能一辈子碰不到这个边界但做安装包、资源包下载的迟早会遇到。因此在写新代码时我默认直接用 zipOpen64、unzOpen64、zipOpenNewFileInZip64、unzGetCurrentFileInfo64 这套 64 位接口省得以后改接口的时候把调用链翻一遍。64 位接口内部使用 _fseeki64/_ftelli64文件大小和偏移量都以 64 位记录。生成出的 zip 会在 central directory 里写出 zip64 扩展信息绝大多数现代解压工具都能正常识别。32 位接口生成的 zip 兼容性最好但功能上限是 2GB 或者 65535 个条目超过就得用扩展。对普通业务来说直接用 64 位接口是零成本的保险。5.2 给 zip 条目加密码压缩和解压两侧的写法minizip 的加密是逐条目控制的不是整个 zip 一个密码。压缩侧在 zipOpenNewFileInZip64 的第 15 个参数里传密码int err zipOpenNewFileInZip64( zf, entry_name, zi, NULL, 0, NULL, 0, NULL, // 注释 Z_DEFLATED, Z_DEFAULT_COMPRESSION, 0, 0, 0, 0, // raw, windowBits, memLevel, strategy pass123, // 密码非空即启用加密 0, 0, // crcForCrypting, versionMadeBy 0x800, // flagBasebit11 表示 UTF-8 文件名 1); // zip64 扩展解压侧对应的是 unzOpenCurrentFilePasswordif (unzOpenCurrentFile(uf) ! UNZ_OK) { if (unzOpenCurrentFilePassword(uf, pass123) ! UNZ_OK) { printf(open entry failed or wrong password\n); } }这里面有几个实际限制要提前知道加密条目必须从头按顺序解压不能先跳过再回来minizip 默认编译时可能不包含 AES 支持只有传统 PKZIP 加密传统加密的强度有限用来防手滑解压可以用来防恶意破解不现实。另外给一个条目设置密码后后续所有条目的密码状态不会自动延续每个加密条目都要单独传一次密码。5.3 多文件打包目录结构用正斜杠把整个目录压成一个 zip需要在遍历文件系统时把路径转换到 zip 命名空间里。有两个容易出问题的点一是路径分隔符Windows 下拿到的是cfg\sub\settings.ini写入 entry_name 前必须把\替换成/否则 zip 在 Linux 上解压会看到一个名字里带反斜杠的怪文件二是目录条目zip 里可以显式写一个以/结尾的目录条目也可以不写目录条目只写文件路径大多数解压工具都能自动推断目录但显式写目录条目的兼容性更好。一个常见做法是先为顶层目录创建条目再递归遍历子目录每进入一个子目录就写一个以/结尾的条目并立即关闭然后逐文件写内容条目。这样生成的 zip 目录结构清晰Windows 资源管理器、WinRAR、Linux unzip 都能按预期还原。5.4 压缩率与耗时实测数据说话压缩率级别不是玄学但也不值得花太多时间调优。我做过一个简单对比一个 200MB 的日志目录level 1 压缩耗时约 3 秒级别 6 约 8 秒级别 9 约 15 秒体积差异大概在 8% 和 10% 之间。也就是说 6 到 9 的收益很小。如果压缩对象已经是 jpg、zip、mp4 这类高熵数据level 0仅存储和 level 9 的时间差可能超过 10 倍而体积几乎一样。选级别之前先看一眼数据类型比盲目调数字靠谱得多。6. 进阶给压缩循环加一个进度回调顺带验证你的 lib 环境minizip 本身没有进度回调机制但它的读写循环是同步的我们可以在循环里自己统计字节数再通过函数指针暴露给上层。这样 UI 层能显示进度条日志层能打点。核心思路是先把源文件总字节数算出来每写完一块就累加一次并触发回调typedef void (*progress_cb)(long long done, long long total); int zip_file_with_progress(const char* zip_path, const char* file_path, const char* entry_name, progress_cb cb) { FILE* src fopen(file_path, rb); if (!src) return -1; fseek(src, 0, SEEK_END); long long total _ftelli64(src); fseek(src, 0, SEEK_SET); zipFile zf zipOpen64(zip_path, APPEND_STATUS_CREATE); // ... 省略 zipOpenNewFileInZip64 的参数写法与 3.2 一致 char buf[16384]; size_t rd; long long done 0; while ((rd fread(buf, 1, sizeof(buf), src)) 0) { zipWriteInFileInZip(zf, buf, (unsigned)rd); done (long long)rd; if (cb) cb(done, total); } fclose(src); zipCloseFileInZip(zf); zipClose(zf, NULL); return 0; }这个函数就是 3.2 代码的加壳版本多做了三件事用 _ftelli64 拿文件总大小、在循环里累加 done、每块写完调一次回调。回调频率应该是每 16KB 一次而不是每字节一次否则 UI 刷新会成为瓶颈。如果做得再讲究一点可以把回调时机控制在百分比变化时触发比如每隔 1% 调一次避免进度条刷新太频繁。写完功能后建议花两分钟做三件验证用资源管理器或命令行解压工具把 zip 打开一遍确认结构正常用 dumpbin /headers 检查 lib 的 machine 类型和 CRT 依赖确认环境和库一致最后把密码条目、中文条目、大文件条目各造一个跑一遍边界。我现在的习惯是每换一次库版本就重跑一次这三个验证别问为什么都是以前吃过亏换来的。希望帮到你。本文还有配套的精品资源点击获取