概述
前言
这是大一写过的一个小项目,现在大三,重新实现了一下。这是原来的链接,可以看一下效果,思路和现在的一样。
扩展性、健壮性比原来更好,思路也更清晰了。当时只想花里胡哨,这次重心放在质量、功能上。
后续会不断改进它,直到它贴近实际。
项目分析
项目围绕实现压缩、解压缩。
模块划分
-
压缩/解压缩均通过哈夫曼编码算法来实现,所以我们的第一个模块为算法模块。
-
实际的任务流程需要一个模块来控制,负责流程的控制,提供简单可用的接口,划分为接口模块。
-
在进行编码时,我们需要进行字节和位串的转换,这就需要位的操作,C语言没有提供这样的函数,需要自己实现,所以划分为位操作模块。
-
对于任一个模块,我们希望给它一个输入,产生一个结果,而不用它考虑输入来自哪里、输出到了哪里。所以我们需要一个流处理模块来集中解决这个问题,这样也可以降低各模块的耦合程度。
-
为了方便, 我们需要一个打印错误信息的模块,就把它作为错误处理模块。
-
为了出现错误时更方便排查,我们增加一个测试模块, 实际上是将各个模块节点的IO进行记录,方便对比排查。
将项目划分为以下几个模块:
- 算法模块
- 接口模块
- 流处理模块
- 错误处理模块
- 位操作模块
- 测试模块
实测中需要频繁地使用缓冲区,包括字符串缓冲区、字节缓冲区、位缓冲区 ,每次都要实现一遍非常不方便,好在数量不多,后续会增加缓冲区模块。
模块功能分析
1.算法模块
算法模块的任务是通过哈夫曼编码得到编码表,输入和输出进行功能分析。这个模块聚合程度很高,我们尽可能不去动它。
- 统计字节频率
- 编码需要构造哈夫曼树, 而构造树需要有weight(权值),而权值需要扫描输入流来进行统计,显然这一工作与编码独立,所以我们将它作为单独功能。
- 构造哈夫曼树
- 不用说,为编码做准备。
- 哈夫曼编码
- 得到哈夫曼编码表。
- 这里的表是抽象类型,实际上用二级指针数组存储编码字符串指针。
- 编码的结果写入输出流,流均由流处理模块指定。
2.接口模块
这个模块负责将各个模块连接,提供压缩、解压缩的接口。
3.流处理模块
这个模块负责压缩文件写入的格式、解析压缩文件、错误打印格式等。
4.错误处理模块
较为简单,但注意打印的消息要写入标准错误流(STDERR
)而非标准输出流(STDOUT
)。
5. 位操作模块
提供位操作。
6.测试模块
这个模块虽然名义上为模块,但实际上却渗透到各个模块中,我们通过宏定义开关来降低它与其他模块的耦合度。
流程图
这里主要描述压缩、解压、压缩文件格式。
压缩逻辑流程图
解压逻辑流程图
名词/行为解释
- 压缩
- 实际上是将原字节替换为哈夫曼编码。
- 解压
- 压缩的逆操作,即将哈夫曼编码替换为原字节码。
- 余码
- 由于编码总长度不是8的整数倍而多余出来的码。
压缩文件格式
压缩文件包括:
- 文件头
- 余码
- 原文件大小
- 编码表
- 压缩数据
例子
假设有个压缩文件1.hfm,那么在文件中的数据是这样的:
10000
1824
00 11111111011010110000
01 11111111011010110001
02 11111111011010110010
03 11111111011010110011
04 11111111011010110100
05 11111111011010110101
......
FE 1111111101101010110
FF 1111111101101010111
�fn�ղ�R_p�[/�n��`_�����]W�CI{z��<���P����kK��O������!&��,t䛮���p������
��a`nP
......
第一行是余码;第二行是原文件字节数;接下来的256行([0, 255])是编码表;最后的N行是压缩后的数据,没错,它是乱七八糟的。
代码解读
实际实现时,我们按照由易到难、由具体到抽象的顺序来实现。
很明显,错误处理模块、位操作模块、算法就很具体,而流格式化的模块就相对抽象。
错误处理模块
用到了变参函数。
err.c
#include "err.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
void errPrint(const char *format, va_list arg_list)
{
char buf[ERR_BUF_SIZE];
vsnprintf(buf, sizeof(buf), format, arg_list);
fprintf(stderr, "%s", buf);
}
void errExit(const char *format, va_list arg_list)
{
errPrint(format, arg_list);
exit(EXIT_FAILURE);
}
void errCaller(void (*errFunc)(const char *, va_list), const char *format, ...)
{
va_list arg_list;
va_start(arg_list, format);
errFunc(format, arg_list);
va_end(arg_list);
}
err.h
#ifndef ERR_H
#define ERR_H
#define ERR_BUF_SIZE 4096
#include <stdarg.h>
void errPrint(const char *format, va_list arg_list);
void errExit(const char *format, va_list arg_list);
void errCaller(void (*errFunc)(const char *, va_list), const char *format, ...);
#endif
位操作模块
bit.c
#include "bit.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include "err.h"
void setbit(Byte *pbyte, size_t ordinal, bool value)
{
// ordinal is [0,7], oridinal is from RIGHT to LEFT.
// bit&1 或者 bit|0,不改变值。bit&0 = 0, bit|1 = 1
Byte mask_AND_to_0[8] = {
0xFE, // 0b11111110,
0xFD, // 0b11111101,
0xFB, // 0b11111011,
0xF7, // 0b11110111,
0xEF, // 0b11101111,
0xDF, // 0b11011111,
0xBF, // 0b10111111,
0x7F // 0b01111111
};
Byte mask_OR_to_1[8] = {
0x01, // 0b00000001,
0x02, // 0b00000010,
0x04, // 0b00000100,
0x08, // 0b00001000,
0x10, // 0b00010000,
0x20, // 0b00100000,
0x40, // 0b01000000,
0x80 // 0b10000000
};
if (value == 0) // set to 0
(*pbyte) &= mask_AND_to_0[ordinal];
else if (value == 1)
(*pbyte) |= mask_OR_to_1[ordinal];
}
int getbit(Byte byte, size_t oridinal)
{
return (byte >> oridinal) & 0x00000001;
}
static size_t rest_bit_quantity(Byte *bytebit_buf, size_t buf_size, size_t byte_end, size_t bit_end)
{ //辅助函数,return the rest quantity of bits of bytebit_buf.
return (buf_size - byte_end) * 8 - bit_end;
}
// byte_end is the index of the last byte not full to 8 bits
// bit_end is the index of the end of bytebit_buf[byte_end], not saving data, just like '