42、压缩与解压:gzip/zlib库的C接口调用。标准库与第三方库的协作。
说到数据压缩,很多C语言新手第一反应是:“标准库里有没有现成的?”
嗯,很遗憾,C标准库确实没提供压缩功能。
但别急,我们有zlib——这个库几乎成了C语言压缩领域的“事实标准”。
我个人习惯把zlib称为“C语言压缩界的瑞士军刀”。
它轻量、稳定、跨平台,而且接口设计得非常干净。
今天我们就聊聊怎么在C项目里调用zlib,以及它和标准库怎么配合。
zlib是什么?为什么选它?
zlib是一个开源的数据压缩库,由Jean-loup Gailly和Mark Adler开发。
它实现了DEFLATE压缩算法,也就是gzip和PNG图片背后用的那个算法。
我遇到过不少项目,明明只需要简单的压缩功能,却自己手写算法。
结果呢?压缩率低、速度慢、还有bug。
其实直接用zlib就好,它经过了二十多年的工业验证。
| 特性 | 说明 |
|---|---|
| 压缩算法 | DEFLATE(兼容gzip) |
| API风格 | 流式接口 + 一次性接口 |
| 内存占用 | 约256KB(默认级别) |
| 跨平台 | Linux/Windows/macOS/嵌入式 |
| 许可证 | zlib许可证(非常宽松) |
安装zlib
在Linux上,一条命令搞定:
sudo apt-get install zlib1g-dev # Debian/Ubuntu
sudo yum install zlib-devel # CentOS/RHEL
Windows下,我建议用vcpkg或者直接下载预编译的DLL。
编译时链接 -lz 就行。
核心API速览
zlib的API分两类:一次性压缩/解压 和 流式压缩/解压。
先看一次性接口,适合处理小数据块:
#include <zlib.h>
// 压缩
int compress(Bytef *dest, uLongf *destLen,
const Bytef *source, uLong sourceLen);
// 解压
int uncompress(Bytef *dest, uLongf *destLen,
const Bytef *source, uLong sourceLen);
这两个函数用起来很简单。
你给一块源数据,它给你一块压缩后的数据。
但要注意:destLen 既是输入也是输出。调用前要传入缓冲区大小,调用后它会被改写为实际压缩后的长度。
我曾经见过有人忘了给 destLen 赋初始值,结果压缩出来的数据全是乱的。
嗯,这种坑踩过一次就记住了。
实战:压缩一段字符串
我们写个完整的例子:
#include <stdio.h>
#include <string.h>
#include <zlib.h>
int main() {
const char *src = "Hello, zlib! 这是一段测试数据,用来演示压缩和解压。";
uLong srcLen = strlen(src) + 1; // 包含结尾的'\0'
// 计算压缩后的最大可能长度
uLong destLen = compressBound(srcLen);
char *dest = (char *)malloc(destLen);
if (!dest) return -1;
// 压缩
int ret = compress((Bytef *)dest, &destLen,
(Bytef *)src, srcLen);
if (ret != Z_OK) {
printf("压缩失败: %d\n", ret);
free(dest);
return -1;
}
printf("原始大小: %lu 字节\n", srcLen);
printf("压缩后大小: %lu 字节\n", destLen);
printf("压缩率: %.2f%%\n", (float)destLen / srcLen * 100);
// 解压
uLong uncompLen = srcLen;
char *uncomp = (char *)malloc(uncompLen);
ret = uncompress((Bytef *)uncomp, &uncompLen,
(Bytef *)dest, destLen);
if (ret == Z_OK) {
printf("解压后: %s\n", uncomp);
}
free(dest);
free(uncomp);
return 0;
}
编译命令:
gcc -o zlib_demo zlib_demo.c -lz
compressBound() 用来估算压缩后最大长度。别自己瞎算,容易溢出。
流式接口:处理大文件
一次性接口有个问题:它要求输入和输出缓冲区一次性准备好。
如果你要压缩一个几百MB的文件,内存就炸了。
这时候要用流式接口。
说白了就是分块处理:读一块,压一块,写一块。
核心结构体是 z_stream:
typedef struct z_stream_s {
z_const Bytef *next_in; // 输入缓冲区
uInt avail_in; // 输入缓冲区可用字节数
uLong total_in; // 已处理的输入字节数
Bytef *next_out; // 输出缓冲区
uInt avail_out; // 输出缓冲区可用字节数
uLong total_out; // 已输出的字节数
// ... 内部字段省略
} z_stream;
流式压缩的典型流程:
- 调用
deflateInit()初始化 - 循环:填充输入 → 调用
deflate()→ 取走输出 - 调用
deflateEnd()清理
解压流程类似,只是把 deflate 换成 inflate。
流式压缩示例(核心片段)
z_stream strm;
memset(&strm, 0, sizeof(strm));
deflateInit(&strm, Z_DEFAULT_COMPRESSION);
char in[4096], out[4096];
int flush;
do {
// 读文件到 in 缓冲区
strm.avail_in = fread(in, 1, sizeof(in), fin);
strm.next_in = (Bytef *)in;
flush = feof(fin) ? Z_FINISH : Z_NO_FLUSH;
do {
strm.avail_out = sizeof(out);
strm.next_out = (Bytef *)out;
deflate(&strm, flush);
// 将 out 缓冲区内容写入文件
fwrite(out, 1, sizeof(out) - strm.avail_out, fout);
} while (strm.avail_out == 0);
} while (flush != Z_FINISH);
deflateEnd(&strm);
while (strm.avail_out == 0) 很关键。因为 deflate() 可能一次没输出完,需要循环直到输出缓冲区有空余。
zlib与标准库的协作
zlib本身不依赖标准库之外的任何东西。
它只用了 malloc、free、memcpy 这些标准C函数。
所以你可以放心地在任何标准C环境中使用它。
我个人的做法是:
把zlib的压缩/解压封装成工具函数,放在项目的基础模块里。
这样上层业务代码完全不用关心压缩细节。
举个例子:
// 封装成简单的文件压缩函数
int compress_file(const char *src, const char *dst) {
// 打开源文件,创建目标文件
// 用流式接口压缩
// 返回0成功,-1失败
}
这样写的好处是:
哪天你想换压缩库(比如换成LZ4),只需要改这一个函数就行。
上层代码完全不受影响。
避坑指南
我这些年用zlib踩过几个坑,分享给你:
- 缓冲区大小估算:永远用
compressBound(),别自己算。我曾经算错过一次,导致内存越界。 - 流式接口的flush参数:最后一次一定要传
Z_FINISH,否则数据不完整。 - 多线程安全:zlib的上下文是独立的,每个线程创建自己的
z_stream就行。但全局的deflateInit()不是线程安全的,建议在初始化阶段加锁。 - 内存泄漏:每次
deflateInit必须对应一次deflateEnd,否则内存泄漏。我见过线上服务跑了几天后OOM,查下来就是zlib没释放。
知识体系图
下面这张图帮你理清zlib的核心脉络:
总结
zlib是C语言生态里不可或缺的第三方库。
它填补了标准库在压缩领域的空白。
一次性接口适合快速开发,流式接口适合处理大文件。
记住那几个核心函数,再配合标准库的IO操作,你就能轻松实现压缩解压功能。
最后说一句:
别自己造轮子。zlib已经足够好,而且经过了无数项目的考验。
你省下来的时间,不如去优化业务逻辑。
公众号:蓝海资料掘金营,微信deep3321