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提供了deflate和inflate接口,可以分块处理数据。
我曾经写过一个日志压缩工具,每天产生几十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_FLUSH和Z_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::copy、std::find、std::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"的分水岭。别怕那点复杂度,封装好了以后用起来真的很顺手。
嗯,今天就聊到这里。压缩这件事,用对了地方,效果立竿见影。