85、Zlib:集成zlib压缩库

压缩库这东西,说实话在项目里太常见了。日志要压缩、网络传输要压缩、存档文件也要压缩。zlib 算是 C/C++ 世界里最老牌的压缩库之一,几乎成了事实标准。我最早接触它是在做一个嵌入式日志系统的时候,那时候存储空间紧张,一条日志动不动几百字节,不压缩根本存不下几天数据。

今天咱们就聊聊,怎么在 CMake 项目里优雅地集成 zlib。

zlib 是什么?

zlib 是一个通用的数据压缩库,提供了 deflate(压缩)和 inflate(解压)算法。它不处理文件格式,只处理原始数据流。你给它一段内存,它给你压缩好的一段内存。就这么简单。

常见的 gzip 格式、png 图片压缩,底层用的都是 zlib 的算法。所以学会集成 zlib,你就能在很多场景下复用这套技能。

CMake 里集成 zlib 的几种方式

我个人习惯把第三方库的集成方式分成三类:系统预装、源码编译、包管理器。zlib 这三种都支持,咱们一个一个看。

方式一:用 find_package 找系统安装的 zlib

如果你的开发环境里已经装了 zlib(比如 Ubuntu 上 apt install zlib1g-dev),那直接用 CMake 的 find_package 就行。

find_package(ZLIB REQUIRED)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE ZLIB::ZLIB)
target_include_directories(myapp PRIVATE ${ZLIB_INCLUDE_DIRS})

嗯,这里要注意:ZLIB::ZLIB 是 CMake 3.4 之后才支持的导入目标。如果你还在用老版本,那就得用 ${ZLIB_LIBRARIES}${ZLIB_INCLUDE_DIRS} 变量。不过我个人建议,能升级就升级,导入目标的方式更干净。

小提示: 用 find_package 之前,最好先确认一下系统里有没有 zlib。可以用 dpkg -l | grep zlib(Debian/Ubuntu)或者 rpm -qa | grep zlib(CentOS/RHEL)查一下。

方式二:用 FetchContent 自动下载编译

这种方式我特别喜欢,尤其是在做跨平台项目的时候。你不需要操心每个平台上怎么装 zlib,CMake 自己帮你搞定。

include(FetchContent)

FetchContent_Declare(
  zlib
  GIT_REPOSITORY https://github.com/madler/zlib.git
  GIT_TAG v1.3
)

FetchContent_MakeAvailable(zlib)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE zlibstatic)
target_include_directories(myapp PRIVATE ${zlib_SOURCE_DIR} ${zlib_BINARY_DIR})

这里我链接的是 zlibstatic,也就是静态库。如果你想要动态库,可以改成 zlib。不过我个人习惯用静态库,部署的时候少操心依赖问题。

注意: FetchContent 会在构建时下载源码,所以第一次构建会比较慢。而且如果你的网络环境不好,可能下载失败。我建议在 CMakeLists.txt 里加个 message(STATUS "Fetching zlib..."),至少让开发者知道当前在干什么。

方式三:用 vcpkg 或 Conan 管理

如果你团队里已经用了包管理器,那集成 zlib 就更简单了。以 vcpkg 为例:

# 先安装 zlib
vcpkg install zlib

# 然后在 CMake 里
find_package(ZLIB REQUIRED)
target_link_libraries(myapp PRIVATE ZLIB::ZLIB)

用 vcpkg 的好处是,它帮你处理了所有平台的编译细节。Windows 上不用愁 MSVC 的运行时库问题,Linux 上也不用愁 ABI 兼容性。我有个项目就是靠 vcpkg 统一了三个平台的依赖管理,省了不少事。

zlib 的核心 API 速览

集成完了,咱们得会用。zlib 的 API 其实不多,核心就这几个:

函数 作用
deflateInit() 初始化压缩上下文
deflate() 执行压缩
deflateEnd() 结束压缩,释放资源
inflateInit() 初始化解压上下文
inflate() 执行解压
inflateEnd() 结束解压,释放资源

使用模式也很固定:先 Init,然后循环调用 deflate/inflate,最后 End。我刚开始用的时候老忘记调 End,结果内存泄漏查了半天。后来养成了习惯,Init 之后立马写对应的 End 调用,中间再填业务逻辑。

一个完整的压缩示例

咱们写个简单的 demo,把一段字符串压缩再解压:

#include <iostream>
#include <vector>
#include <zlib.h>

std::vector<char> compress(const std::string& input) {
    uLong srcLen = input.size();
    uLong destLen = compressBound(srcLen);
    std::vector<char> dest(destLen);

    int ret = compress(
        reinterpret_cast<Bytef*>(dest.data()), &destLen,
        reinterpret_cast<const Bytef*>(input.data()), srcLen
    );

    if (ret != Z_OK) {
        throw std::runtime_error("compress failed");
    }

    dest.resize(destLen);
    return dest;
}

std::string decompress(const std::vector<char>& compressed, uLong srcLen) {
    uLong destLen = srcLen;
    std::vector<char> dest(destLen);

    int ret = uncompress(
        reinterpret_cast<Bytef*>(dest.data()), &destLen,
        reinterpret_cast<const Bytef*>(compressed.data()), compressed.size()
    );

    if (ret != Z_OK) {
        throw std::runtime_error("decompress failed");
    }

    return std::string(dest.data(), destLen);
}

int main() {
    std::string original = "Hello, zlib! This is a test string for compression.";
    auto compressed = compress(original);
    auto decompressed = decompress(compressed, original.size());

    std::cout << "Original: " << original.size() << " bytes\n";
    std::cout << "Compressed: " << compressed.size() << " bytes\n";
    std::cout << "Decompressed: " << decompressed.size() << " bytes\n";
    std::cout << "Match: " << (original == decompressed ? "yes" : "no") << "\n";

    return 0;
}

这段代码里用了 compressuncompress 这两个高级函数,它们内部帮你处理了 Init 和 End。如果你需要流式压缩(比如边读边压),那就得用 deflate/inflate 那套底层 API 了。

重要: compressBound 这个函数一定要用。它返回的是最坏情况下的压缩后大小(实际上压缩后可能比原数据还大,比如数据已经是随机的了)。别自己估算缓冲区大小,我吃过这个亏。

zlib 集成知识体系

我把今天讲的内容画了张图,方便你理解整体脉络:

zlib 集成知识体系 集成方式 find_package 系统安装 FetchContent 自动下载 vcpkg/Conan 包管理 核心 API compress / uncompress deflate / inflate deflateInit / inflateInit 注意事项 用 compressBound 算大小 Init/End 要成对出现 静态库 vs 动态库选择 总结:选一种集成方式 + 掌握核心 API + 注意内存管理 = 搞定 zlib

避坑指南

最后分享几个我踩过的坑:

  • 我曾经在 Windows 上用 MSVC 编译 zlib,结果链接的时候报了一堆 LNK2019。后来发现是 zlib 的源码里用了 __declspec(dllexport),而我的项目是静态链接。解决方案是定义 ZLIB_WINAPI 宏,或者在 CMake 里设置 target_compile_definitions(zlibstatic PRIVATE ZLIB_WINAPI)
  • 还有一次,我在一个嵌入式 Linux 项目里用 FetchContent 下载 zlib,结果目标板子上没有 git。后来改成用 URL 参数直接下载 tar.gz 包才解决。
  • 另外,zlib 的 compress 函数默认压缩级别是 Z_DEFAULT_COMPRESSION(6)。如果你对压缩率有要求,记得用 deflateInit2 手动设置级别。级别越高越慢,但压缩率也越高。我一般存盘用 9,网络传输用 3。

好了,zlib 的集成就聊到这儿。说白了就是三步:选方式、写 CMake、调 API。你想想看,是不是挺简单的?


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