
简介RLERun-Length Encoding行程长度编码是一种典型的无损压缩算法在C语言中实现它能够帮助初学者理解字符串处理、动态内存管理和循环控制等核心编程技巧。这份资源面向计算机专业学生、嵌入式开发者及对数据压缩感兴趣的入门者包含完整可运行的C语言工程。压缩包为RAR格式共5个文件涵盖main.c源代码、Code::Blocks工程文件.cbp、编译依赖文件.depend、目标文件.o以及可直接运行的exe程序整体体积仅10KB方便快速下载与本地验证。已有2023人学习使用适合用来对照代码梳理RLE压缩与解压缩的完整流程尤其是连续重复字符的统计、压缩结果输出、动态数组分配与释放等关键环节。资源虽小但属于典型的算法教学示例可以作为后续研究LZ77或Huffman编码的入门基础。 做嵌入式或者搞文件存储的朋友多半遇到过这种尴尬一个文件或一段缓存里全是重复字节直接传输浪费带宽写进Flash又占空间。RLERun-Length Encoding游程长度编码正是为这种情况准备的经典压缩算法。上个月我在给一块STM32的日志模块做数据存储优化时需要把一条条重复度很高的传感器记录先压缩再写Flash对比了若干方案后选了RLE并且用C语言完整手写了一遍。这篇博文就把我的思考、代码和踩坑记录一起放出来给同样在纠结C语言压缩实现的朋友做个参考。内容从算法原理讲到工程化改进适合刚开始接触压缩算法的C语言学习者也适合在嵌入式或文件处理场景里需要轻量压缩的开发者。1. RLE算法原理与设计思路1.1 压缩原理把“连续重复”变成“数值计数”RLE的核心思想很简单一段数据里如果出现很多连续相同的字节我们不需要重复存储每一个字节只需要记录“这个字节连续出现了多少次”。比如字符串“AAAAABBBBCCCC”原始长度是13字节RLE可以编码成“5A4B4C”这种形式只看这些内容压缩后只有6字节。如果按字节而不是字符来看就是连续的计数和值交替排列。这里有个关键点RLE不是通用压缩算法它只对重复数据有效。数据越“有规律”压缩率越好看如果数据本身是随机的RLE不仅压不动还有可能让体积变大。所以拿到一段数据先看里面是不是有成片的重复字节再决定要不要上RLE。这个“预判”能力比代码本身更重要。1.2 为什么用C语言实现RLE选择C语言其实没有太多纠结。RLE操作的对象是“字节流”C语言里unsigned char*天然就是一个可以直接读写字节的缓冲区配合指针移动和内存操作整个算法可以做到几乎没有额外开销。尤其是我要放到STM32上跑像Java、Python这类带运行时环境的语言根本不合适C语言编译出来的代码运行期间没有垃圾回收内存占用可控非常契合这种轻量压缩场景。另外C语言的文件读写接口像fopen、fread、fwrite处理大型日志文件也很直接。RLE的编码和解码本质上都是“读一段字节处理写一段字节”这个流程用C语言描述很自然。如果你在刷题或做课程设计时遇到RLE用C语言实现也是最能体现底层逻辑的选择。2. C语言实现先写一个能跑的版本2.1 基础编码器单字节计数版我写第一个版本时没有考虑复杂情况目标就是先把流程跑通。编码逻辑很直白从源缓冲区第一个字节开始找出连续相同的长度然后把“长度”和“字节本身”依次写入目标缓冲区。为了保证计数不会超过一个字节能表示的范围我限制每个run最多255超过就拆成多段。#include stdio.h #include string.h int rle_encode(unsigned char *src, int src_len, unsigned char *dst, int dst_cap) { int d 0, i 0; while (i src_len) { int run 1; unsigned char ch src[i]; while (i run src_len src[i run] ch run 255) { run; } if (d 2 dst_cap) { return -1; } dst[d] (unsigned char)run; dst[d] ch; i run; } return d; }这里dst_cap是目标缓冲区容量用于防止写入越界这也是我在嵌入式上吃过亏之后加上的。编码函数最终返回写入目标缓冲区的字节数如果缓冲区不够大就返回-1。注意run被限制在255所以(unsigned char)run永远不会溢出。2.2 基础解码器反向还原解码是编码的逆过程每两个字节为一组第一个字节是重复次数第二个字节是值循环重复写入输出缓冲区。写解码器时特别要注意输入数据的格式合法性比如字节数必须是偶数否则直接返回-1。代码里我对输出缓冲也做了同样的边界检查。int rle_decode(unsigned char *src, int src_len, unsigned char *dst, int dst_cap) { int d 0, i 0; while (i src_len) { if (i 1 src_len) { return -1; } int run src[i]; unsigned char ch src[i 1]; if (d run dst_cap) { return -1; } for (int j 0; j run; j) { dst[d] ch; } i 2; } return d; }因为run是从unsigned char转成的int所以范围是0到255。实际编码时run至少为1不会出现0但如果别人给了个非法的压缩数据run0也只会写0个字节不影响安全性这是防御性编程的基本思路。2.3 用一段测试数据验证我用字符串“AAAAABBBBCCCC”做测试原始长度13调用编码器后返回6再调用解码器还原。能还原成原串基本就说明流程通了。测试代码我习惯单独放一个main函数方便直接编译运行int main(void) { unsigned char test[] AAAAABBBBCCCC; unsigned char comp[64]; unsigned char decomp[128]; int clen rle_encode(test, (int)strlen((char *)test), comp, sizeof(comp)); int dlen rle_decode(comp, clen, decomp, sizeof(decomp)); printf(orig%zu comp%d decomp%d\n, strlen((char *)test), clen, dlen); if (dlen (int)strlen((char *)test) memcmp(test, decomp, dlen) 0) { printf(round trip ok\n); } else { printf(round trip failed\n); } return 0; }这个版本已经把RLE的核心链路走通了。但如果你是拿真实数据来试很快会发现一个问题如果每个字节都不重复基础版会把“ABCDEF...”编码成“1A1B1C1D...”数据量直接翻倍。所以下一步就需要对算法做工程化改进。3. 工程化改进避免“越压越大”3.1 从源头控制膨胀PackBits变体真正能用在生产环境的RLE通常不会像我上面写的那么“单纯”。工业界更常见的是带“字面量打包”的变体比如TGA图像格式里用的RLE以及经典PackBits算法。核心思路是遇到连续重复的字节写一个“重复模式”标记遇到一连串不重复的字节打包成“字面量模式”一起写而不是每个字节都加计数。具体到控制字节我用一个字节的高位区分两种模式高位置1表示后面跟1个字节该字节需要重复(低7位1)次高位置0表示后面跟(低7位1)个字节这些字节原样复制。低7位能表示0-127所以单次最多处理128个字节。这个办法对结构规整的数据压缩率很好对随机数据也只是多了一个控制字节的开销不会再出现放大一倍的情况。3.2 PackBits编码器与解码器实现编码器遍历时先判断当前位置有没有长度大于等于2的连续块。如果有就用重复模式输出如果没有就尽量往后收集一个不包含相邻重复字节的字面量段最长128字节然后一次性输出。这里有一个容易踩的细节字面量段的停止条件是“发现下一个字节和当前段最后一个字节相同”因为一旦相同说明后面才是可以形成run的位置。int rle_packbits_encode(unsigned char *src, int src_len, unsigned char *dst, int dst_cap) { int d 0, i 0; while (i src_len) { int run 1; while (i run src_len src[i run] src[i] run 128) { run; } if (run 2) { if (d 2 dst_cap) return -1; dst[d] 0x80 | (run - 1); dst[d] src[i]; i run; } else { int start i; int len 0; while (i src_len len 128) { if (len 0 src[i - 1] src[i]) { break; } len; i; } if (d 1 len dst_cap) return -1; dst[d] len - 1; for (int j 0; j len; j) { dst[d] src[start j]; } } } return d; }解码器更简单。读一个控制字节判断最高位如果为1则取后面一个字节重复指定次数如果为0则取后面指定数量的字节逐个复制输出。int rle_packbits_decode(unsigned char *src, int src_len, unsigned char *dst, int dst_cap) { int d 0, i 0; while (i src_len) { unsigned char control src[i]; int count (control 0x7F) 1; if (control 0x80) { if (i src_len) return -1; if (d count dst_cap) return -1; for (int j 0; j count; j) { dst[d] src[i]; } i; } else { if (i count src_len) return -1; if (d count dst_cap) return -1; for (int j 0; j count; j) { dst[d] src[i]; } } } return d; }用这版编码器去压缩“ABCDEFGHIJK”这种全是单字节重复的数据输出是一段控制字节加11个原字符总长度12字节只比原来多1字节。压缩“AAAAABBBBCCCC”时输出约7字节左右依然有接近一半的压缩率。工程上这个方向基本够用。3.3 内存规划与缓冲区分配讲完编码逻辑不能忽略内存管理。C语言里给目标缓冲区分配多大一直是写RLE最容易翻车的地方。理论上最坏情况下PackBits版本编码后的数据可能比原始数据多出“每128字节多1字节控制头”的开销所以安全的上限是src_len src_len / 128 1。我一般会按这个公式分配目标缓冲区避免写入时出现越界。如果是在嵌入式环境里我建议直接用静态数组或预先分配好的内存池不要用动态分配。比如STM32上栈空间很小一个几KB的数组放在栈上可能导致HardFault改为全局静态缓冲区或者用malloc一次性申请风险会小很多。基础版的最坏情况是每字节都变成两个字节缓冲区需要src_len * 2所以“先算上限再分配”这步不能省。4. 测试与性能分析4.1 怎么构造能暴露问题的测试数据只测一句“AAAAABBBBCCCC”远远不够。我在调试PackBits版本时构造了四类数据来测全重复型如10000个0xFF完全随机型交替型如“ABABABAB”以及混合型大量重复块中间夹着几个随机字节。这样才能确认编码器在“连续重复”“无重复”“边缘切换”等各种分支下都能正确解码。全重复型验证run分支和128上限拆分完全随机型验证literal分支以及在随机数据中偶然出现的连续重复交替型验证“看似有规律但实际没有连续相同字节”的情况混合型验证从literal切换到run再切换回去的逻辑。测试时我写了一个helper函数对每种数据先编码再解码然后用memcmp逐字节比较打印出原始长度、压缩长度和解压结果。只要有一组不匹配就说明分支判断或者指针移动有bug。4.2 压缩率实测结果我拿一组模拟数据测了一下数组长度10000字节其中前6000字节全部是0x00后4000字节用伪随机数据填充。基础版RLE压缩后约8048字节PackBits版本压缩后约4126字节。可见PackBits对随机数据有显著改善。为了快速对比我列了一个表测试数据原始大小基础版压缩后PackBits压缩后10000个0x0010000801588000个随机字节8000约16000约80636000个0x00 4000随机字节10000约8048约4126表格里的随机数据是最不友好的场景基础版会膨胀到原始大小两倍PackBits版本只多出约0.8%的开销。这个差异直接决定了算法能不能放到真实项目里。当然PackBits对超长重复段的编码效率不如基础版因为单次run最多128字节需要更多的控制头但在混合数据上总体还是更稳。4.3 性能优化心得RLE的复杂度是O(n)编码和解码都只需要一遍扫描。这一点在需要压缩大数据块的时候很占优势。实际优化时我关注三个地方读写指针使用局部变量少从缓冲区里重复读取在连续字节匹配循环中让run累加而不是每次调用memcmp避免函数调用开销解码时用memset输出连续重复块比逐字节for循环快不少特别是在run长度接近128时。不过提前优化有时会带来代码可读性下降。我自己的原则是先保证正确性和边界安全再用profiler确认瓶颈最后才考虑循环展开之类的技巧。嵌入式编译器优化选项开到-O2之后上面的逐字节循环大多能被自动优化手写优化收益有限。5. 典型应用场景与集成建议5.1 STM32等嵌入式设备上的日志存储我现在最常用的场景就是STM32的Flash日志存储。传感器数据经过AD采样后如果在一段时间内变化不大原始数据里会有很多高位相同的字节甚至整段数据大部分是相同字节。把日志先做RLE再写入Flash能显著降低Flash擦写次数延长设备寿命。嵌入式上跑RLE要注意缓冲区尽量用静态数组避免栈溢出数据长度用uint32_t记录不要用int防止交叉编译时长度不一致因为Flash按扇区擦除压缩后的数据建议按固定块大小对齐方便索引。5.2 文件读写场景与C语言文件操作结合如果要做文件压缩可以把编码器和文件操作封装起来。读取源文件用fread按块读入调用RLE编码再用fwrite写压缩文件。解压时反向操作。因为RLE不依赖全局字典每一块数据都可以独立编解码天然适合分块处理这一点比Huffman方便很多。我快速写个示例思路FILE *in fopen(input.bin, rb); FILE *out fopen(output.rle, wb); unsigned char buf[4096], comp[8192]; size_t n; while ((n fread(buf, 1, sizeof(buf), in)) 0) { int clen rle_packbits_encode(buf, (int)n, comp, sizeof(comp)); fwrite(n, sizeof(n), 1, out); // 保存原块长度 fwrite(clen, sizeof(clen), 1, out); fwrite(comp, 1, (size_t)clen, out); } fclose(in); fclose(out);这里把每块原始长度和压缩长度都存下来解压时按块读取就能逐块还原。注意存储长度时用固定大小的类型像我这里size_t在不同平台可能长度不一样跨平台使用时要换成uint32_t。5.3 与其他压缩算法配合RLE很少单独压死所有场景但它非常适合作为其他算法的预处理步骤。比如一些图像数据先跑一遍RLE把连续重复的颜色值处理掉再喂本文还有配套的精品资源点击获取