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 就行。

小提示: 如果你在嵌入式环境里,zlib也提供了最小化配置选项,可以裁剪到几十KB。

核心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;

流式压缩的典型流程:

  1. 调用 deflateInit() 初始化
  2. 循环:填充输入 → 调用 deflate() → 取走输出
  3. 调用 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本身不依赖标准库之外的任何东西。
它只用了 mallocfreememcpy 这些标准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 核心知识体系 zlib 库 一次性接口 流式接口 工具函数 compress() uncompress() deflate() inflate() compressBound() crc32() 一次性接口:适合小数据块,简单易用 流式接口:适合大文件/网络流,分块处理 工具函数:辅助计算缓冲区大小、校验和等 所有接口均基于标准C,无平台依赖

总结

zlib是C语言生态里不可或缺的第三方库。
它填补了标准库在压缩领域的空白。
一次性接口适合快速开发,流式接口适合处理大文件。
记住那几个核心函数,再配合标准库的IO操作,你就能轻松实现压缩解压功能。

最后说一句:
别自己造轮子。zlib已经足够好,而且经过了无数项目的考验。
你省下来的时间,不如去优化业务逻辑。


公众号:蓝海资料掘金营,微信deep3321