Varint是Protocol Buffers定义的紧凑整数编码方式,小数值用更少字节(如0仅占1字节),大数值逐步扩展,节省网络带宽;C++需手动实现或借助Protobuf库,编码时取低7位、高位标续位,解码须防越界、超长和ZigZag误处理。

Varint 是什么,为什么 C++ 网络传输要用它
Varint 不是 C++ 标准库内置类型,而是 Google Protocol Buffers 定义的一种紧凑整数编码方式:小数值用更少字节表示(比如 0 只占 1 字节),大数值才逐步扩展。这对网络传输很关键——多数 ID、状态码、长度字段实际值都很小,用 int32_t(固定 4 字节)或 int64_t(固定 8 字节)会浪费带宽。
直接手写 Varint 编解码不难,但必须注意字节序、符号处理和边界检查。C++ 没有现成的 encode_varint 函数,得自己实现或借力 Protobuf 库。
手动实现无符号整数的 Varint 编码(uint32_t / uint64_t)
Protobuf 的 Varint 只支持无符号整数;如果要编码有符号数(如 int32_t),得先做 ZigZag 编码(后面会提)。先看最常用的 uint64_t 编码:
核心逻辑:每次取低 7 位作为数据,最高位设为 1 表示“还有后续字节”,直到只剩 ≤7 位时设最高位为 0。
- 编码时从低位开始写,所以结果字节流是 little-endian 风格(但不是传统意义的字节序,而是“变长单元序”)
- 务必检查输入是否为
0:它应编码为单字节0x00,不是空字节或越界读 - 输出缓冲区至少预留 10 字节(
uint64_t最坏情况需 10 字节,因为 64 ÷ 7 ≈ 9.14 → 向上取整为 10) - 不要用
std::vector在循环里反复扩容,影响性能;建议预分配或传入固定大小缓冲区 + 返回实际长度::push_back
示例编码函数片段:
size_t encode_varint(uint64_t value, uint8_t* buf) {
size_t len = 0;
while (value >= 0x80) {
buf[len++] = static_cast<uint8_t>(value | 0x80);
value >>= 7;
}
buf[len++] = static_cast<uint8_t>(value);
return len;
}
</uint8_t></uint8_t>
解码时容易崩溃的三个坑:buf 越界、负数误读、超长编码
解码比编码更容易出错,尤其在网络收包场景下,buf 可能被截断或含脏数据:
- 必须限制最多读 10 字节(对
uint64_t),否则恶意构造的全0xFF输入会导致无限循环或越界访问 - 不能假设输入是“合法 Protobuf 流”——真实网络包可能丢包、粘包,解码前要确认可用字节数 ≥ 当前已读长度
- 如果原始数是有符号的(如
int32_t),直接解成uint32_t再强转会出错;必须先用 ZigZag 解码:(n >> 1) ^ -(n & 1) - Protobuf 的
int32字段底层存的是 ZigZag 编码的uint32,不是原值;漏这步会导致负数全变成极大正数(比如-1变成0xFFFFFFFF)
安全解码示意(带长度检查):
.proto
- 必须静态链接
libprotobuf或确保运行时 ABI 版本匹配,否则std::bad_cast或静默解析失败很常见 - 生成的
SerializePartialToArray比完整版更快(跳过 required 字段检查),适合高性能场景 - 字段类型选
int32还是sint32很关键:int32用普通 Varint(负数编码极长),sint32才用 ZigZag —— 别凭名字猜,查文档 - 不推荐只为 Varint 去引入 Protobuf;纯轻量场景,手写 50 行编码/解码函数更可控
真正麻烦的从来不是算法本身,而是跨语言对接时对方用的是不同 ZigZag 实现、或把 signed/unsigned 混着传——这种问题往往在压测后期才暴露,且难以复现。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











