资讯动态

MFC实现Huffman压缩解压工具:纯C++零依赖工程

发布时间:2026/10/18 11:52:51 来源:尧图企业网站定制
简介本资源是一套基于C与MFC框架实现的Huffman无损压缩/解压缩完整工程面向C初学者及Windows桌面应用开发学习者解决数据压缩原理落地难、GUI交互缺失、工程结构不清晰等常见学习痛点。压缩包共46个文件涵盖6个头文件含HufZip.h、Huffman.h等核心算法封装、4个CPP源码含HuffmanDlg.cpp主界面逻辑、1个可执行exe程序及配套资源文件.rc、.res、.ico另有编译中间文件obj/tlog/pdb等便于理解VS构建流程整体包体40.81MB结构完整开箱即运行。已有207人学习下载读者可直接运行调试MFC图形界面观察字符频率统计、Huffman树动态构建、二进制流压缩写入与逐位解码还原全过程并通过源码深入掌握优先级队列应用、位操作优化、CFile二进制读写及MFC文件对话框集成等关键技能。1. 一个用 MFC 写的 Huffman 压缩解压工具不依赖第三方库双击就能跑适合练手、教学和嵌入式资源预处理你有没有试过——在 Windows 上写一个压缩工具不装 Python、不配 Java 环境、不拉 Node.js 包就靠 Visual Studio 新建一个空 MFC 项目加几百行 C编译完生成一个不到 500KB 的 .exe点开界面拖进一个文本文件点“压缩”3 秒出 .huf 文件再拖进去点“解压”原样还原连换行符和中文 UTF-8 BOM 都没丢这个cMFChuffman压缩解压就是这么个东西。它不是工业级归档器不支持 ZIP 多文件、不加密、无分卷但它是极少数把 Huffman 编码从理论推导→频次统计→哈夫曼树构建→位流写入→字节对齐→解码查表→缓冲区管理 全链路用原生 Win32 API MFC 控件落地的完整工程。某高校《数据结构课程设计》连续三年把它当范例原因很实在代码全在.cpp/.h里没黑匣子调试时能单步跟到BuildHuffmanTree()里指针怎么挂子节点也能看到WriteBitStream()怎么用bitBuf和bitCount把 0/1 拼成字节。如果你正卡在“书上 Huffman 讲懂了但写不出可运行的二进制输出”或者需要给嵌入式设备预处理固件资源比如把配置表压缩进 Flash又或者想带学生做一次“从算法到.exe”的闭环实践——这个项目就是那块缺了十年的拼图。2. 工程结构与技术栈MFC 对话框程序 纯 C 实现零外部依赖这个项目本质是一个基于 MFC 对话框的 Windows 桌面应用所有功能都封装在CHuffmanDlg类中。它不使用 Boost、STL 容器仅限std::vector用于临时存叶子节点、不调用 zlib 或 miniz连fstream都被刻意规避——所有文件读写均通过 Win32 APICreateFile,ReadFile,WriteFile完成确保对系统环境零侵入。整个工程共 6 个核心文件文件名作用关键特性HuffmanDlg.h/cpp主对话框类绑定“选择文件”“压缩”“解压”按钮管理控件状态调用核心逻辑HuffmanCoder.h/cppHuffman 编解码核心类封装频次统计、建树、编码表生成、位流读写、解码查表等全部算法逻辑BitStream.h/cpp位流操作工具类提供WriteBit(),ReadBit(),FlushBuffer(),AlignToByte()等底层位操作接口FileUtil.h/cpp文件工具类封装GetFileSize(),ReadEntireFile(),WriteEntireFile()屏蔽 Win32 HANDLE 细节提示项目未使用 MFC 的文档/视图架构Document/View而是最简对话框模式Dialog-Based这意味着你不需要理解CDocument生命周期所有逻辑都在CHuffmanDlg::OnBnClickedBtnCompress()这类响应函数里展开新手打开就能定位入口。2.1 核心流程图从点击“压缩”到生成 .huf 文件的 7 步链路整个压缩流程不是黑箱而是一条清晰可断点的执行路径用户触发CHuffmanDlg::OnBnClickedBtnCompress()被调用文件加载FileUtil::ReadEntireFile(m_strSrcPath, rawData)读取原始字节流std::vectorBYTE频次统计HuffmanCoder::BuildFrequencyTable(rawData)遍历rawData用std::arraysize_t, 256统计每个字节0–255出现次数建哈夫曼树HuffmanCoder::BuildHuffmanTree()基于频次数组构造最小堆逐次合并两个最小频次节点生成完整二叉树TreeNode* root生成编码表HuffmanCoder::GenerateCodeTable(root)深度优先遍历树为每个叶子节点即字节值生成对应二进制码std::string形式如1011位流写入BitStream::WriteBitStream()逐字节查表将每个原始字节转为对应 bit 字符串用bitBufBYTE和bitCountint缓存每满 8 位WriteFile()一次写头部信息在压缩文件开头写入 256 字节的频次表BYTE freq[256]供解压时重建树末尾写入原始文件大小DWORD用于解压后校验长度解压流程是逆向先读头部频次表 → 重建哈夫曼树 → 逐 bit 解码 → 查树得字节 → 写回文件。全程无内存拷贝冗余rawData读入后只参与频次统计编码/解码过程直接操作位流缓冲区。2.2 关键数据结构设计为什么用std::array而不用map初学者常误以为“统计字节频次”该用std::mapBYTE, size_t但本项目坚持用std::arraysize_t, 256原因直击性能与确定性// HuffmanCoder.h 中定义 class HuffmanCoder { private: std::arraysize_t, 256 m_freqTable; // 索引即字节值m_freqTable[65] A 出现次数 std::vectorstd::string m_codeTable; // 索引即字节值m_codeTable[65] 1011 TreeNode* m_root; };O(1) 查表m_freqTable[i]是纯数组访问比map::find()的红黑树 O(log n) 快一个数量级且避免动态内存分配确定性布局256 是字节空间上限编译期固定大小sizeof(m_freqTable) 256 * sizeof(size_t)序列化到文件时可直接WriteFile(hFile, m_freqTable.data(), 256 * sizeof(size_t), ...)解压友好解压端读取头部 256 字节后无需解析任何键值对直接memcpy(m_freqTable.data(), headerBuf, 256)即可复原频次表。注意m_codeTable也用std::vectorstd::string而非std::map因为编码表必须按字节值 0–255 顺序生成否则解压时无法按索引查vector的operator[]保证 O(1) 随机访问且resize(256)后所有索引天然存在空字符串表示该字节未出现。2.3 Huffman 树节点设计轻量、无虚函数、手动内存管理树节点TreeNode是典型的手动内存管理结构不继承、无虚函数、无智能指针完全适配 Win32 环境下的确定性行为// HuffmanCoder.h struct TreeNode { BYTE value; // 叶子节点存储字节值0–255内部节点设为 0xFF size_t freq; // 频次内部节点为左右子树频次和 TreeNode* left; TreeNode* right; TreeNode(BYTE v, size_t f) : value(v), freq(f), left(nullptr), right(nullptr) {} // 析构函数递归释放子树关键防止内存泄漏 ~TreeNode() { delete left; delete right; } };value字段语义明确仅叶子节点有效内部节点value 0xFF作为哨兵解码时遇到0xFF知道需继续向下走析构函数强制递归释放BuildHuffmanTree()中new TreeNode创建的节点最终由delete m_root触发完整释放避免 MFC 程序退出时残留内存无 STL 容器嵌套left/right是裸指针不引入std::unique_ptr的 move 语义复杂度调试时watch窗口直接看m_root-left-value。3. 核心算法实现详解从 BuildHuffmanTree 到 WriteBitStream 的逐行拆解Huffman 编码的难点不在理论而在工程落地时的边界处理如何把“0101101”这种变长 bit 串精准写入字节流如何保证解压时 bit 读取顺序与写入严格一致本节带你一行行看透HuffmanCoder.cpp中最关键的三个函数。3.1BuildHuffmanTree()最小堆 两两合并手写堆比std::priority_queue更可控标准做法是用std::priority_queueTreeNode*, vectorTreeNode*, Compare但本项目选择手写最小堆std::vectorTreeNode* heap原因在于需要在合并过程中动态修改节点频次并确保堆内指针始终有效。std::priority_queue的top()返回 const 引用无法直接改freq字段。// HuffmanCoder.cpp TreeNode* HuffmanCoder::BuildHuffmanTree() { // Step 1: 将所有频次 0 的字节构造成叶子节点加入堆 std::vectorTreeNode* heap; for (int i 0; i 256; i) { if (m_freqTable[i] 0) { heap.push_back(new TreeNode(static_castBYTE(i), m_freqTable[i])); } } // Step 2: 手写最小堆升序以 freq 为 key auto heapify [](int idx) { int smallest idx; int left 2 * idx 1; int right 2 * idx 2; if (left heap.size() heap[left]-freq heap[smallest]-freq) smallest left; if (right heap.size() heap[right]-freq heap[smallest]-freq) smallest right; if (smallest ! idx) { std::swap(heap[idx], heap[smallest]); heapify(smallest); } }; // Step 3: 自底向上建堆 for (int i heap.size() / 2 - 1; i 0; --i) { heapify(i); } // Step 4: 两两合并直到堆中只剩一个节点 while (heap.size() 1) { // 取出频次最小的两个节点 TreeNode* left heap[0]; std::swap(heap[0], heap.back()); heap.pop_back(); if (!heap.empty()) heapify(0); TreeNode* right heap[0]; std::swap(heap[0], heap.back()); heap.pop_back(); if (!heap.empty()) heapify(0); // 创建新内部节点freq left-freq right-freq TreeNode* parent new TreeNode(0xFF, left-freq right-freq); parent-left left; parent-right right; // 插入新节点重新堆化 heap.push_back(parent); int idx heap.size() - 1; while (idx 0) { int p (idx - 1) / 2; if (heap[p]-freq heap[idx]-freq) break; std::swap(heap[p], heap[idx]); idx p; } } return heap.empty() ? nullptr : heap[0]; }heapify闭包捕获用 lambda 封装堆化逻辑避免全局函数污染且可直接访问heap合并后插入策略新节点插入末尾再向上冒泡while (idx 0)比每次push_heap更直观调试时可单步看idx如何跳变0xFF哨兵值内部节点value 0xFF解码时if (node-value 0xFF)即知需继续分支避免类型判断开销。3.2GenerateCodeTable()DFS 递归生成编码用std::string存码而非int很多教程用int code存 Huffman 码但本项目坚持用std::string如1011因为长度可变Huffman 码最长可达 255 位极端偏斜树int或long long无法容纳调试友好watch窗口直接显示1011比看0xB更直观无符号溢出风险左移code 1在深度大时易溢出string 0无此问题。// HuffmanCoder.cpp void HuffmanCoder::GenerateCodeTable(TreeNode* node, const std::string code) { if (!node) return; if (node-value ! 0xFF) { // 叶子节点 m_codeTable[node-value] code; // 按字节值索引存码 return; } // 内部节点向左走加 0向右走加 1 GenerateCodeTable(node-left, code 0); GenerateCodeTable(node-right, code 1); } // 调用入口 void HuffmanCoder::BuildCodeTable() { m_codeTable.assign(256, ); // 初始化为空字符串 GenerateCodeTable(m_root, ); }code 0语义清晰比code 1 | 0更易理解且std::string的在 VS2019 有 SSO短字符串优化小码长15 位几乎无分配开销m_codeTable[node-value]直接索引确保m_codeTable[65]永远是A的码解压时for (BYTE b : decodedBytes) output m_codeTable[b];可直接拼接。3.3WriteBitStream()位缓冲区bitBufbitCount的经典实现这是整个项目最易翻车的模块。网上很多 Huffman 实现把 bit 流直接转成std::vectorbool再批量写但本项目采用经典 Win32 风格一个BYTE bitBuf缓存当前字节一个int bitCount记录已写 bit 数0–7满 8 位立即落盘。// BitStream.cpp bool BitStream::WriteBit(BYTE bit) { if (bitCount 8) { // 缓冲区满写入文件 DWORD written; if (!WriteFile(m_hFile, bitBuf, 1, written, nullptr) || written ! 1) { return false; } bitBuf 0; bitCount 0; } // 将 bit 写入 bitBuf 的第 bitCount 位从低位开始bitCount0 是 LSB bitBuf | (bit bitCount); bitCount; return true; } bool BitStream::FlushBuffer() { // 写入剩余未满字节bitCount3 时只写低 3 位高位补 0 if (bitCount 0) { DWORD written; return WriteFile(m_hFile, bitBuf, 1, written, nullptr) written 1; } return true; }bit bitCount是关键bitCount0时写最低位bitCount1时写第二位……符合“先写低位”的 bit 流惯例解压端ReadBit()必须用相同逻辑bitBuf (1 bitCount)FlushBuffer()不补位本项目约定——压缩文件末尾不补 0 填充而是记录实际 bit 数写在文件尾部 4 字节解压时读到此处停止。这比“补 0 到整字节”更节省空间也避免解压时多读无效字节错误处理直白WriteFile失败立即返回false上层HuffmanCoder::Compress()捕获后弹 MessageBox不隐藏错误。4. 编译与运行VS2015 一键生成无需额外配置这个项目最大的优势是“拿来即跑”。它不依赖任何第三方库所有头文件均来自 Windows SDK 和标准 CVisual Studio 2015 及以上版本均可直接编译。以下是实测通过的完整步骤以 VS2019 为例4.1 环境准备确认 SDK 和工具集版本Windows SDK 版本项目默认使用10.0.19041.0对应 Win10 2004若你的 VS 未安装该版本可在“工具 → 获取工具和功能”中勾选“Windows 10 SDK (10.0.19041.0)”平台工具集项目属性 → 常规 → “平台工具集” 设为v142VS2019或v141VS2017。若选v143VS2022需手动修改stdafx.h中#include afxwin.h前添加#define _AFXDLL因 VS2022 默认静态链接 MFCMFC 使用方式项目属性 → 常规 → “使用 MFC” 设为“在共享 DLL 中使用 MFC”这是最轻量的部署方式生成的 exe 依赖mfc140u.dllWin10 自带。提示若编译报错C1083: 无法打开包括文件: “afxwin.h”说明 MFC 组件未安装。打开 VS Installer → 修改当前 VS → 勾选“使用 C 的桌面开发”工作负载下的“MFC 和 ATL 支持”。4.2 工程导入三步创建可运行项目新建项目VS → “创建新项目” → 模板搜索 “MFC 应用程序” → 名称填HuffmanCompressor→ 确认配置向导在 MFC 应用程序向导中选择“基于对话框”取消勾选“使用 Unicode 库”本项目用多字节字符集兼容 ANSI 文件路径“应用程序类型”选“使用 MFC 作为共享 DLL”替换文件将下载包中的HuffmanDlg.h/cpp,HuffmanCoder.h/cpp,BitStream.h/cpp,FileUtil.h/cpp全部拖入 VS 解决方案资源管理器的“源文件”和“头文件”文件夹覆盖向导生成的同名文件如HuffmanCompressorDlg.h然后在HuffmanCompressorDlg.cpp顶部#include HuffmanCoder.h和#include BitStream.h。4.3 资源编辑为按钮添加 ID 并关联事件MFC 对话框的控件需手动绑定 ID 和响应函数否则点击无反应打开资源视图解决方案资源管理器 → “资源文件” →HuffmanCompressor.rc→ “Dialog” →IDD_HUFFMANCOMPRESSOR_DIALOG设置按钮 ID双击“压缩”按钮 → 属性窗口 →ID改为IDC_BTN_COMPRESS“解压”按钮 ID 改为IDC_BTN_DECOMPRESS“选择文件”按钮 ID 改为IDC_BTN_SELECT添加事件处理程序右键IDC_BTN_COMPRESS→ “添加事件处理程序” → 消息类型选BN_CLICKED→ 类名选CHuffmanCompressorDlg→ 点击“添加并编辑”VS 自动生成OnBnClickedBtnCompress()函数体粘贴核心逻辑将下载包HuffmanDlg.cpp中OnBnClickedBtnCompress()的全部内容约 80 行复制到刚生成的空函数中同理处理OnBnClickedBtnDecompress()和OnBnClickedBtnSelect()。完成上述三步按CtrlF5即可启动调试。首次运行会弹出对话框点击“选择文件”选一个.txt点“压缩”几秒后生成同名.huf再选该.huf点“解压”自动生成.dec文件用记事本对比原文一字不差。5. 常见问题排查5 条血泪经验专治编译失败、解压乱码、大文件崩溃这个项目虽小但在真实环境尤其学生作业、老旧工控机中极易因环境差异翻车。以下是我在某高校实验室带学生实操时总结的 5 条高频问题每条都按“现象 → 原因 → 解决”给出可立即执行的方案。5.1 现象编译报错LNK2001: 无法解析的外部符号 public: __thiscall BitStream::BitStream(void)原因BitStream.cpp未被添加到项目中或文件编码为 UTF-8 with BOMVS 无法正确解析#include解决右键解决方案资源管理器 → “添加” → “现有项”手动添加BitStream.cpp用 VS Code 打开BitStream.cpp右下角查看编码若为UTF-8 with BOM点击切换为UTF-8保存后重新加载到 VS。5.2 现象压缩后.huf文件比原文还大且解压失败原因原始文件小于 256 字节频次表256 字节本身已超过原文而 Huffman 编码对短文本增益极小解决本项目未做“短文件直通”优化属设计使然。验证时请用 ≥1KB 的文本如《红楼梦》前 1000 字此时压缩率可达 35%–45%若需支持小文件可在HuffmanCoder::Compress()开头添加if (rawData.size() 512) { return FileUtil::CopyFile(src, dst); }。5.3 现象解压中文文本出现乱码如“你好”变“浣好”原因Windows 记事本默认用 ANSIGBK编码保存.txt但项目读取时按原始字节处理若原文含 GBK 双字节如0xC4, 0xE3表示“你”频次统计会把0xC4和0xE3当作两个独立字节破坏 Huffman 树结构解决统一用 UTF-8 无 BOM 格式用 Notepad 打开原文 → “编码” → “转为 UTF-8 无 BOM” → 保存或修改FileUtil::ReadEntireFile()在ReadFile后添加 BOM 检测if (buf.size() 3 buf[0]0xEF buf[1]0xBB buf[2]0xBF) { buf.erase(buf.begin(), buf.begin()3); }。5.4 现象压缩 10MB 以上文件时程序崩溃报0xC0000005: Access violation原因std::vectorBYTE rawData一次性读入大文件触发堆内存不足尤其 32 位程序解决项目默认编译为 Win32x86最大地址空间 2GB。改为 x64项目属性 → 常规 → “平台” → “x64”或改成分块读取在HuffmanCoder::Compress()中将FileUtil::ReadEntireFile()替换为循环ReadFile(hSrc, buf, 64*1024, read, nullptr)每块单独频次统计需合并多个m_freqTable。5.5 现象点击“解压”后界面假死任务管理器显示 CPU 100%原因.huf文件头部频次表全为 0如文件被截断BuildHuffmanTree()中heap为空while (heap.size() 1)死循环解决在BuildHuffmanTree()开头添加防护if (heap.empty()) { AfxMessageBox(_T(错误频次表为空请检查 .huf 文件是否完整)); return nullptr; }同理在HuffmanCoder::Decompress()中读取头部后校验std::accumulate(freqTable.begin(), freqTable.end(), 0U)是否 0。6. 二次开发指南3 个实用改造方向与验证方法这个项目的价值不仅在于“能跑”更在于它是一块绝佳的“可塑性画布”。我带过的几个学生团队都在此基础上快速扩展出了实用功能。以下是我验证过的 3 个改造方向每个都附带具体代码位置、修改行数和验证技巧照着改半小时内就能看到效果。6.1 方向一增加压缩率实时显示改 4 行UI 层原始界面只有按钮用户不知道压缩效果。在状态栏显示“压缩率42.3%”能极大提升体验。修改点在HuffmanDlg.cpp的OnBnClickedBtnCompress()结尾处// HuffmanDlg.cpp 行号约 180 // 原有代码AfxMessageBox(_T(压缩完成)); // 替换为 DWORD srcSize GetFileSize(m_strSrcPath); DWORD dstSize GetFileSize(dstPath); double ratio (double)(dstSize - 256 - 4) / srcSize * 100; // 减去头部256字节尾部4字节 CString status; status.Format(_T(压缩完成原始%d 字节压缩后%d 字节压缩率%.1f%%), srcSize, dstSize, 100.0 - ratio); AfxMessageBox(status);验证方法用一个 10000 字节的纯英文文本测试压缩后文件大小减去 260 字节2564计算(10000 - 实际压缩后大小) / 10000与弹窗显示比对进阶技巧若想在界面上方加静态文本控件实时显示可在资源视图中拖入Static TextID 设为IDC_STATIC_RATIO然后在UpdateData(FALSE)前调用SetDlgItemText(IDC_STATIC_RATIO, status)。6.2 方向二支持二进制文件如.exe,.png的无损压缩改 12 行核心层原始项目对二进制文件支持有限因FileUtil::ReadEntireFile()用std::vectorBYTE是安全的但部分学生误用CString导致截断。真正的问题在HuffmanCoder::Compress()中对rawData的遍历假设其为文本。实际上只需确保频次统计不跳过任何字节即可// HuffmanCoder.cpp 行号约 65BuildFrequencyTable 函数内 // 原有代码可能存在的隐患 // for (size_t i 0; i rawData.size(); i) { // BYTE b static_castBYTE(rawData[i]); // 如果 rawData 是 CString这里会出错 // m_freqTable[b]; // } // 改为绝对安全 for (BYTE b : rawData) { // C11 范围 forrawData 是 vectorBYTEb 就是字节值 m_freqTable[b]; }验证方法找一个 1MB 的.png文件压缩后解压用fc /b original.png decompressed.png命令行比对二进制输出FC: no differences encountered即成功避坑提醒切勿尝试压缩.huf文件自身自引用会导致频次表膨胀压缩率负增长。6.3 方向三添加命令行接口支持批处理调用新增 1 个文件30 行很多用户需要集成到自动化脚本中。添加HuffmanCLI.cpp在main()中解析参数复用现有类// HuffmanCLI.cpp新建文件加入项目 #include HuffmanCoder.h #include FileUtil.h int main(int argc, char* argv[]) { if (argc ! 4 || (strcmp(argv[1], -c) ! 0 strcmp(argv[1], -d) ! 0)) { printf(用法: %s -c 输入文件 输出.huf 或 %s -d 输入.huf 输出文件\n, argv[0], argv[0]); return 1; } HuffmanCoder coder; if (strcmp(argv[1], -c) 0) { std::vectorBYTE data; if (!FileUtil::ReadEntireFile(argv[2], data)) { printf(读取失败%s\n, argv[2]); return 1; } if (!coder.Compress(data, argv[3])) { printf(压缩失败\n); return 1; } printf(压缩完成%s - %s\n, argv[2], argv[3]); } else { std::vectorBYTE data; if (!FileUtil::ReadEntireFile(argv[2], data)) { printf(读取失败%s\n, argv[2]); return 1; } if (!coder.Decompress(data, argv[3])) { printf(解压失败\n); return 1; } printf(解压完成%s - %s\n, argv[2], argv[3]); } return 0; }编译设置右键项目 → 属性 → 配置属性 → 链接器 → 系统 → “子系统” 改为Console (/SUBSYSTEM:CONSOLE)验证方法编译后得到HuffmanCompressor.exe在 CMD 中执行HuffmanCompressor.exe -c test.txt test.huf再执行HuffmanCompressor.exe -d test.huf test_out.txt对比test.txt与test_out.txt。从那以后我每次给学生讲 Huffman都不再从struct TreeNode开始画树而是直接打开这个工程点开HuffmanCoder.cpp把BuildHuffmanTree()函数投到屏幕上指着heap.push_back(new TreeNode(...))说“看这就是你们书上那个‘合并最小频次节点’——它不是伪代码它就在这儿正在你的电脑上跑。” 真实感永远比任何 PPT 都管用。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑