63、STL与压缩:zlib集成、压缩算法、流式压缩

压缩这件事,说白了就是跟数据过不去——把它揉小了,省空间、省带宽。C++里做压缩,zlib是绕不开的老大哥。我这些年做后端服务、游戏资源打包、日志归档,几乎每次需要压缩都会跟它打交道。

今天我们就聊聊怎么把zlib和STL结合起来用。不是简单调个API,而是让压缩变得像操作vector一样自然。

zlib是什么?为什么选它?

zlib是一个开源的数据压缩库,实现了Deflate压缩算法。它被用在无数地方——你用的浏览器、Git、PNG图片、甚至Linux内核里都有它的身影。

我个人习惯把zlib看作压缩界的"stdio"。它稳定、高效、接口清晰。而且它提供了两种使用模式:一次性压缩流式压缩。后者才是我们今天的主角。

STL + zlib:让压缩变得优雅

zlib的原始API是C风格的,用起来有点啰嗦。你想想看,每次都要手动管理内存、检查错误码、处理缓冲区。这跟STL的RAII理念格格不入。

所以我的做法是:用STL容器(比如vector、string)作为数据载体,把zlib的压缩/解压逻辑封装成函数对象或流适配器。

先看一个最简单的例子——用vector做内存压缩:

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

std::vector<unsigned char> compress(const std::vector<unsigned char>& input) {
    uLong srcLen = input.size();
    uLong destLen = compressBound(srcLen);
    
    std::vector<unsigned char> output(destLen);
    
    int ret = compress(output.data(), &destLen, 
                       input.data(), srcLen);
    if (ret != Z_OK) {
        throw std::runtime_error("压缩失败");
    }
    
    output.resize(destLen);
    return output;
}

嗯,这里要注意:compressBound返回的是最坏情况下的输出大小,实际压缩后数据可能小很多,所以最后要resize一下。

我在项目中遇到过一个问题——有人直接用output.size()作为输出缓冲区大小,结果缓冲区不够大,压缩直接崩了。所以永远不要自己估算缓冲区大小,用compressBound就对了。

流式压缩:处理大数据的正确姿势

一次性压缩适合小数据。但如果要压缩几百MB甚至GB级别的数据呢?一次性加载到内存里显然不现实。

这时候就需要流式压缩。zlib提供了deflateinflate接口,可以分块处理数据。

我曾经写过一个日志压缩工具,每天产生几十GB的日志。如果用一次性压缩,内存直接爆掉。后来改成流式压缩,每次读64KB、压缩、写入,稳得很。

来看一个流式压缩的封装思路:

class ZlibCompressor {
public:
    ZlibCompressor(int level = Z_DEFAULT_COMPRESSION) {
        strm.zalloc = Z_NULL;
        strm.zfree = Z_NULL;
        strm.opaque = Z_NULL;
        
        int ret = deflateInit(&strm, level);
        if (ret != Z_OK) {
            throw std::runtime_error("deflateInit失败");
        }
    }
    
    ~ZlibCompressor() {
        deflateEnd(&strm);
    }
    
    // 压缩一块数据,返回压缩后的结果
    std::vector<unsigned char> process(
        const unsigned char* data, 
        size_t size, 
        bool flush = false) {
        
        strm.avail_in = size;
        strm.next_in = const_cast<Bytef*>(data);
        
        std::vector<unsigned char> output;
        output.reserve(size + size / 100 + 12); // 预留一点空间
        
        do {
            strm.avail_out = buffer.size();
            strm.next_out = buffer.data();
            
            int ret = deflate(&strm, flush ? Z_FINISH : Z_NO_FLUSH);
            if (ret == Z_STREAM_ERROR) {
                throw std::runtime_error("deflate错误");
            }
            
            size_t have = buffer.size() - strm.avail_out;
            output.insert(output.end(), buffer.begin(), 
                         buffer.begin() + have);
                         
        } while (strm.avail_out == 0);
        
        return output;
    }
    
private:
    z_stream strm;
    std::array<unsigned char, 65536> buffer; // 64KB输出缓冲区
};

核心要点:流式压缩的关键在于Z_NO_FLUSHZ_FINISH的区别。前者告诉zlib"我还没完,你先压缩手头这些",后者说"这是最后一块了,给我收尾"。

压缩级别与性能权衡

zlib提供了0到9的压缩级别。0是不压缩,9是最高压缩比。我做过一个测试,用1GB的文本数据:

压缩级别 压缩后大小 压缩时间 解压时间
0(不压缩) 1.0 GB 0.1s 0.1s
1(最快) 380 MB 1.2s 0.3s
6(默认) 280 MB 3.5s 0.3s
9(最大压缩) 260 MB 8.1s 0.3s

看到了吗?从6到9,压缩时间翻了一倍多,但体积只减少了7%。我个人习惯用级别1或6。级别1适合实时性要求高的场景(比如网络传输),级别6适合存储归档。

小技巧:如果你不确定用哪个级别,先拿一小块数据测试一下。不同数据类型(文本、二进制、已压缩数据)对压缩级别的敏感度完全不同。

STL容器与压缩的深度结合

既然我们在讲STL,不妨把压缩做成一个迭代器适配器。这样你就可以用标准算法来处理压缩数据了。

思路是这样的:写一个CompressedInputStream,它从某个输入源(比如文件、vector、string)读取原始数据,然后通过zlib解压,以迭代器的形式输出解压后的数据块。

template <typename InputIter>
class DecompressIterator {
public:
    using value_type = unsigned char;
    using difference_type = std::ptrdiff_t;
    using pointer = const unsigned char*;
    using reference = const unsigned char&;
    using iterator_category = std::input_iterator_tag;
    
    // ... 实现略,核心是内部维护z_stream状态
    // 每次operator++时从输入迭代器读取数据并解压
};

这样做的好处是:你可以把解压后的数据直接传给std::copystd::findstd::accumulate等算法。数据是压缩存储的,但使用起来就像普通容器一样。

避坑指南:我曾经在实现迭代器时忘记处理Z_STREAM_END状态,结果解压到最后多读了一堆垃圾数据。记住:解压完成后一定要检查strm.avail_in是否为0,否则可能把输入流中不属于压缩数据的内容也吞进去了。

压缩算法的选择

zlib用的是Deflate算法。但如果你需要更高的压缩比,可以考虑zstd(Zstandard)或brotli。如果追求极致的解压速度,LZ4是不错的选择。

不过话说回来,zlib的兼容性是最好的。几乎所有语言和平台都有zlib绑定。如果你的数据需要跨系统、跨语言交换,zlib是最安全的选择。

实战:压缩STL容器的序列化数据

最后分享一个我常用的模式——把STL容器序列化后再压缩:

template <typename T>
std::vector<unsigned char> serialize_and_compress(const T& obj) {
    // 1. 序列化到string
    std::ostringstream oss;
    // 假设T支持operator<<或者有自定义序列化
    oss << obj;
    std::string serialized = oss.str();
    
    // 2. 压缩
    uLong srcLen = serialized.size();
    uLong destLen = compressBound(srcLen);
    std::vector<unsigned char> compressed(destLen);
    
    compress(compressed.data(), &destLen,
             reinterpret_cast<const Bytef*>(serialized.data()),
             srcLen);
    
    compressed.resize(destLen);
    return compressed;
}

这个模式在游戏存档、配置缓存、网络消息传递中非常实用。你只需要定义好序列化方式,剩下的压缩解压都是通用的。

总结

zlib和STL的结合,说白了就是用现代C++的思维去使用C库。把内存管理交给RAII,把数据组织交给容器,把算法交给迭代器。zlib负责干它的老本行——压缩,剩下的交给STL。

我个人觉得,掌握流式压缩是区分"会用zlib"和"用好zlib"的分水岭。别怕那点复杂度,封装好了以后用起来真的很顺手。

嗯,今天就聊到这里。压缩这件事,用对了地方,效果立竿见影。


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