简介本资源是一套基于C与MFC框架实现的Huffman编码无损压缩/解压缩完整工程面向C初学者及Windows桌面应用开发学习者解决数据压缩原理落地难、GUI交互缺失、二进制文件操作不直观等实践痛点。压缩包含46个文件涵盖6个头文件如HufZip.h、Huffman.h、4个核心CPP源码含HuffmanDlg.cpp、HufZip.cpp、1个可执行exe程序及配套资源文件.rc、.res、.ico和编译中间产物obj、pdb、tlog等整体40.81MB结构清晰体现MFC对话框程序典型组织方式。已有207人学习下载读者可直接运行exe体验压缩/解压流程深入阅读源码掌握字符频率统计、优先级队列构建Huffman树、位流编码写入、二进制文件读写等关键实现并复用HufZip类封装逻辑至其他项目。1. 一个用 MFC 写的 Huffman 压缩工具不依赖第三方库双击就能压文件右键还能解压适合嵌入式教学演示和 C 初学者练手你有没有试过在 Windows 上写一个真正能双击运行、带图形界面、又能完成完整 Huffman 编码压缩/解压缩流程的 C 程序不是控制台里输一堆参数也不是调用 zlib 或 boost 的封装接口——而是从读文件、统计字频、建哈夫曼树、生成编码表、写二进制压缩流到反向解析位流、查树还原字节、保存解压文件全链路由自己手撸。这个cMFChuffman压缩解压项目就是这么个“硬核但友好”的存在它用原生 MFC 实现了完整的 GUI 交互打开/保存对话框、进度条、状态栏核心算法全部基于标准 CC11 兼容不链接任何外部压缩库整个工程只有.cpp/.h/.rc三类文件编译后单个.exe小于 300KB。它不是工业级工具但胜在结构干净、逻辑透明、每一行代码都可追溯——某高校《数据结构课程设计》连续三年把它作为 Huffman 实战范例A同学第一次跑通时甚至把压缩率从 42% 调到了 58%只因改了两行位操作逻辑。如果你正卡在“知道原理却写不出可执行代码”或需要一个能讲清楚“位怎么存、树怎么遍历、缓冲区怎么对齐”的教学级参考这个项目就是你该下载并立刻打开 VS2019或 VS2022调试起来的那个。2. 工程结构与技术栈MFC 对话框程序 手写 Huffman 核心无第三方依赖这个项目本质是一个基于 MFC 的单文档对话框程序Dialog-based Application不是 SDI/MDI所以没有菜单栏和工具栏的复杂抽象层所有 UI 控件按钮、编辑框、列表框、进度条都通过资源视图直接拖放配置事件响应函数也由 ClassWizard 自动生成。这种结构对初学者极友好你不需要理解CDocument/CView的消息映射机制只要关注OnInitDialog()初始化、OnBnClickedBtnCompress()响应点击、UpdateData()同步控件值这三件事就够了。2.1 目录与文件构成6 个核心源文件撑起全部功能整个工程共 12 个文件但真正承载业务逻辑的只有以下 6 个其余为 MFC 框架自动生成的stdafx.h、resource.h、targetver.h及资源文件文件名类型关键职责HuffmanCompressor.h/cpp自定义类Huffman 核心引擎含BuildTree()构建哈夫曼树、GenerateCodes()生成编码表、CompressFile()主压缩流程、DecompressFile()主解压流程CompressDlg.h/cppMFC 对话框类UI 与逻辑胶水绑定“压缩”“解压”按钮、文件路径编辑框、状态栏文本、进度条调用HuffmanCompressor并更新 UIBitStream.h/cpp工具类位操作黑匣子解决 C 原生不支持“写任意位数”问题提供WriteBits()/ReadBits()接口内部用unsigned char缓冲区 位计数器实现FrequencyTable.h/cpp数据结构类字频统计容器std::mapunsigned char, int存储字节频次BuildFromBuffer()从内存块构建GetTotalCount()返回总字节数TreeNode.h纯数据结构哈夫曼树节点left/right指针、weight权重、value字节值叶子节点、code编码字符串调试用MainFrm.h/cppMFC 框架类仅保留默认实现未做任何修改完全由向导生成提示BitStream是本项目最易被忽略但最关键的模块。Windows API 的WriteFile()最小单位是字节而 Huffman 编码结果往往是非整数字节如 1234 位必须靠它把零散位打包成字节流再写入磁盘。没它压缩文件永远比原文件大——因为你会被迫补满字节导致冗余膨胀。2.2 技术选型逻辑为什么是 MFC 而不是 Qt 或 ImGuiMFC 的不可替代性项目目标是“让学生在 Win32 平台快速看到 GUI 效果”MFC 是 VS 自带、零配置、无需安装 SDK、编译即运行。Qt 需要额外部署动态库ImGui 需要手写渲染循环对刚学完指针的学生来说光配环境就能劝退。C 标准版本锁定在 C11所有容器用std::vector/std::map不用auto和 lambda避免学生看不懂nullptr替代NULLoverride显式标注虚函数重载——既保证现代性又杜绝语法炫技。零第三方依赖的代价与收益没有zlib意味着不支持 ZIP 兼容格式但换来的是你能直接在HuffmanCompressor.cpp里打断点看着pRoot-TraverseAndPrint()一行行打印出整棵树你能把BitStream::m_nBitPos加到监视窗口实时观察位偏移如何从 0 跳到 7 再归零。这种“代码裸奔感”是教学价值的核心。2.3 核心数据流图从文件到压缩包的 5 步闭环整个压缩流程不是黑箱而是清晰可拆解的五段式管道读取原始文件→CFile::Read()到std::vectorunsigned char缓冲区统计字频→FrequencyTable::BuildFromBuffer()遍历缓冲区填充mapBYTE, int构建哈夫曼树→HuffmanCompressor::BuildTree()用优先队列std::priority_queue按权重升序合并节点生成编码表→HuffmanCompressor::GenerateCodes()深度优先遍历树左 0 右 1 记录每个字节的二进制串写入压缩流→BitStream::WriteBits()将每个字节替换为对应编码逐位写入目标文件解压流程则是逆向先读压缩文件头含字频表长度重建频率表 → 重建哈夫曼树 → 用BitStream::ReadBits(1)逐位读取沿树向下走遇到叶子就输出字节 → 直到读完所有位。3. Huffman 核心算法实现手写哈夫曼树构建与位流编解码Huffman 算法的教科书描述很简洁但落地时有三个魔鬼细节树节点内存管理、编码表生成方式、位流边界对齐。这个项目用最直白的方式处理了它们我们逐段看关键代码。3.1 哈夫曼树构建用std::priority_queue实现最小堆合并// HuffmanCompressor.cpp void HuffmanCompressor::BuildTree() { // 步骤1为每个字节创建叶子节点 std::vectorTreeNode* leaves; for (const auto pair : m_freqTable.GetFrequencyMap()) { TreeNode* node new TreeNode(); node-value pair.first; // 字节值 node-weight pair.second; // 出现次数 node-isLeaf true; leaves.push_back(node); } // 步骤2用优先队列维护最小堆按 weight 升序 auto cmp [](TreeNode* a, TreeNode* b) { return a-weight b-weight; }; std::priority_queueTreeNode*, std::vectorTreeNode*, decltype(cmp) minHeap(cmp); for (TreeNode* leaf : leaves) { minHeap.push(leaf); } // 步骤3反复合并权重最小的两个节点直到只剩根节点 while (minHeap.size() 1) { TreeNode* left minHeap.top(); minHeap.pop(); TreeNode* right minHeap.top(); minHeap.pop(); TreeNode* parent new TreeNode(); parent-weight left-weight right-weight; parent-left left; parent-right right; parent-isLeaf false; minHeap.push(parent); } m_pRoot minHeap.top(); // 根节点 }逻辑说明与参数说明m_freqTable.GetFrequencyMap()返回std::mapunsigned char, int确保字节值 0~255 全覆盖即使频次为 0 也会存在避免解压时查不到std::priority_queue的比较函数cmp必须用而非因为 C 默认是最大堆我们要最小堆需反转逻辑内存泄漏风险所有new TreeNode()分配的内存在DestroyTree()中统一delete该函数在CompressFile()结束后被调用防止多次压缩累积泄漏为什么不用std::shared_ptr教学项目刻意回避智能指针强制学生理解原始指针生命周期——这是某导师的血泪经验学生用shared_ptr后树节点循环引用导致内存不释放调试三天找不到原因。3.2 编码表生成DFS 遍历 字符串拼接调试友好版// HuffmanCompressor.cpp void HuffmanCompressor::GenerateCodes(TreeNode* node, const std::string code) { if (node nullptr) return; if (node-isLeaf) { // 叶子节点记录字节值 - 编码字符串映射 m_codeMap[node-value] code; // 调试用打印 0x41 - 1011 这样的日志 TRACE(_T(Code for 0x%02X: %s\n), node-value, code.c_str()); return; } // 非叶子左子树加 0右子树加 1 GenerateCodes(node-left, code 0); GenerateCodes(node-right, code 1); } // 调用入口 void HuffmanCompressor::GenerateCodes() { m_codeMap.clear(); GenerateCodes(m_pRoot, ); }逻辑说明与参数说明code参数是递归传递的当前路径编码初始为空字符串m_codeMap是std::mapunsigned char, std::string存储最终查表用的编码解压时直接m_codeMap[byte]获取为什么用std::string而不是std::vectorbool因为std::string可直接TRACE打印方便验证编码是否正确如字母 A 应该是短码空格应该是长码生产环境会换成位向量提升性能但教学阶段可读性优先陷阱若树构建错误如权重为 0 的节点未过滤会导致code无限递归拼接栈溢出。项目在BuildTree()前已过滤频次为 0 的字节确保每片叶子都有实际意义。3.3 位流写入BitStream类如何把 13 位数据塞进 2 字节// BitStream.cpp bool BitStream::WriteBits(unsigned int value, int nBits) { if (nBits 0 || nBits 32) return false; // 循环处理每一位从低位开始取value 1然后 value 1 for (int i 0; i nBits; i) { unsigned char bit (value 1); value 1; // 将 bit 写入缓冲区 m_buffer[m_nBytePos] | (bit m_nBitPos); m_nBitPos; if (m_nBitPos 8) { m_nBytePos; m_nBitPos 0; // 缓冲区满写入文件 if (m_nBytePos BUFFER_SIZE) { FlushBuffer(); } } } return true; } void BitStream::FlushBuffer() { if (m_nBytePos 0) return; DWORD written; WriteFile(m_hFile, m_buffer, m_nBytePos, written, nullptr); m_nBytePos 0; }逻辑说明与参数说明value是待写入的整数nBits是写入位数如字符 A 编码为1011则value11,nBits4m_buffer是unsigned char[BUFFER_SIZE]通常 4096 字节m_nBytePos是当前写入字节索引m_nBitPos是当前字节内已写位数0~7关键技巧(bit m_nBitPos)将位放到字节的正确位置再用|合并避免覆盖其他位边界处理当m_nBitPos8时自动进位到下一字节并检查缓冲区是否满m_nBytePos BUFFER_SIZE满则FlushBuffer()写入磁盘为什么从低位开始取因为 Huffman 编码是前缀码高位在前如1011表示第一位是 1但value 1取的是最低位所以传入前需将编码字符串反转项目在WriteBits()调用前已做std::reverse(code.begin(), code.end())确保1011变成1101再转为整数 13这样写入顺序才是 1→1→0→1。4. 运行与调试VS2019 编译指南、常见报错与文件兼容性这个项目在 VS2019/VS2022 上开箱即用但新手常卡在环境配置和文件路径上。以下是实测有效的步骤附带三条高频翻车现场。4.1 从零编译5 分钟跑通的完整流程新建空项目VS2019 → “创建新项目” → 搜索 “MFC 应用程序” → 选择 “MFC 应用程序” 模板 → 项目名填HuffmanCompressor→ 下一步 → 应用程序类型选 “基于对话框” → 取消勾选 “使用 Unicode 库”关键否则中文路径乱码→ 完成导入源文件将下载包中的.h/.cpp/.rc文件全部拖入解决方案资源管理器 → 右键项目 → “添加” → “现有项”全选导入修复头文件包含打开CompressDlg.cpp确认第一行是#include stdafx.h不是pch.h若 VS2022 默认生成pch.h则需在项目属性 → C/C → 预编译头 → 设置为 “不使用预编译头”设置字符集右键项目 → 属性 → 常规 → 字符集 → 选择 “使用多字节字符集”再次强调Unicode 会导致CString处理中文路径失败编译运行CtrlShiftB 编译 → F5 启动 → 点击 “浏览” 选择一个 TXT 文件 → 点击 “压缩” → 观察状态栏提示 “压缩完成原始大小1245 字节压缩后721 字节”注意若编译报错C2664: int sprintf_s(char *, size_t, const char *, ...)是因为sprintf_s第二个参数是缓冲区大小需将sprintf_s(buf, %d, num)改为sprintf_s(buf, sizeof(buf), %d, num)。项目源码已修正但旧版可能遗漏。4.2 文件兼容性什么文件能压什么文件压了反而变大Huffman 压缩效果高度依赖数据熵值。项目实测兼容性如下文件类型压缩率典型值原因分析是否推荐纯文本英文/中文35% ~ 55%字符分布不均高频字符空格、e、的获得短码✅ 强烈推荐源代码.cpp/.h40% ~ 50%符号{,;,(出现频繁利于压缩✅ 推荐二进制可执行文件.exe95% ~ 105%基本不变或略大字节分布接近均匀Huffman 无法有效区分权重❌ 不推荐JPEG/PNG 图片98% ~ 102%已被 LZ77 DCT 二次压缩再压无收益❌ 绝对避免验证方法在CompressDlg.cpp的OnBnClickedBtnCompress()中TRACE打印m_freqTable.GetEntropy()信息熵计算函数已内置熵值低于 4.0 时压缩率高高于 6.5 时基本无效。4.3 避坑3 条真实踩坑记录与解决方案现象点击“压缩”后程序无响应任务管理器显示 CPU 占用 100%原因BuildTree()中std::priority_queue的比较函数写错导致while (minHeap.size() 1)死循环堆未正确排序top()始终返回同一节点解决检查cmp函数是否为a-weight b-weight若误写为则改为或临时在循环内加TRACE(_T(Heap size: %d\n), minHeap.size());确认 size 是否递减现象解压后的文件内容错乱开头几个字节正确后面全是0x00原因BitStream::ReadBits()未处理文件末尾的填充位。Huffman 压缩文件末尾会补 0 到字节对齐但解压时若不记录原始位数会多读出若干0导致树遍历走到空指针解决在压缩文件头部写入 4 字节的“原始文件大小”解压时读取后精确控制ReadBits()总位数项目已在CompressFile()开头写入DWORD originalSizeDecompressFile()开头读取并校验现象压缩大文件10MB时内存爆满VS 报std::bad_alloc原因FrequencyTable::BuildFromBuffer()一次性CFile::Read()整个文件到内存10MB 文件需 10MB 内存而std::map节点开销更大解决改用分块读取。在CompressFile()中每次Read()64KB 到缓冲区调用m_freqTable.AddBuffer()累加频次最后再BuildTree()。项目源码已实现此优化但需确认#define CHUNK_SIZE 65536未被注释5. 二次开发实战给压缩文件加密码、支持拖拽、导出编码表为 CSV这个项目不是终点而是你动手改造的第一个跳板。下面以三个真实需求为例展示如何在不破坏原有结构的前提下安全扩展。5.1 需求一为压缩文件添加简单密码异或加密Huffman 压缩本身不加密但可在压缩后对二进制流做轻量混淆。最简方案是压缩完成后用用户输入的密码字符串如123对压缩文件每个字节做异或。// 在 CompressDlg.cpp 的 OnBnClickedBtnCompress() 末尾添加 CString strPassword; GetDlgItemText(IDC_EDIT_PASSWORD, strPassword); // 假设新增了密码编辑框 if (!strPassword.IsEmpty()) { CFile fileCompressed(m_strCompressedPath, CFile::modeReadWrite); DWORD fileSize fileCompressed.GetLength(); BYTE* pBuffer new BYTE[fileSize]; fileCompressed.Read(pBuffer, fileSize); // 密码异或用密码每个字符循环异或 for (DWORD i 0; i fileSize; i) { char c strPassword.GetAt(i % strPassword.GetLength()); pBuffer[i] ^ (BYTE)c; } fileCompressed.SeekToBegin(); fileCompressed.Write(pBuffer, fileSize); fileCompressed.Close(); delete[] pBuffer; }验证方法用十六进制编辑器打开压缩文件对比加密码前后差异解压时需在DecompressFile()开头同样做一次异或相同密码再进行 Huffman 解压。5.2 需求二支持 Windows 拖拽文件到窗口MFC 默认禁用拖拽需手动开启并处理WM_DROPFILES消息。// CompressDlg.h 中添加 protected: afx_msg void OnDropFiles(HDROP hDropInfo); // CompressDlg.cpp 中添加消息映射 BEGIN_MESSAGE_MAP(CCompressDlg, CDialogEx) // ...原有映射 ON_MESSAGE(WM_DROPFILES, CCompressDlg::OnDropFiles) END_MESSAGE_MAP() // 实现函数 LRESULT CCompressDlg::OnDropFiles(WPARAM wParam, LPARAM lParam) { HDROP hDrop (HDROP)wParam; UINT nFiles DragQueryFile(hDrop, 0xFFFFFFFF, nullptr, 0); // 获取文件数 if (nFiles 1) { TCHAR szFilePath[MAX_PATH]; DragQueryFile(hDrop, 0, szFilePath, MAX_PATH); SetDlgItemText(IDC_EDIT_SRC_PATH, szFilePath); } DragFinish(hDrop); return 0; } // 在 OnInitDialog() 中启用拖拽 BOOL CCompressDlg::OnInitDialog() { CDialogEx::OnInitDialog(); DragAcceptFiles(TRUE); // 关键启用拖拽 return TRUE; }注意DragAcceptFiles(TRUE)必须在OnInitDialog()中调用且只能对对话框本身启用子控件如编辑框无法单独接收拖拽。5.3 需求三导出编码表为 CSV供教学分析学生常需观察“哪些字符编码最长/最短”导出 CSV 可直接用 Excel 排序。// 在 HuffmanCompressor.h 中添加 public: bool ExportCodeTableToCSV(const CString strPath); // HuffmanCompressor.cpp 中实现 bool HuffmanCompressor::ExportCodeTableToCSV(const CString strPath) { CStdioFile file(strPath, CFile::modeCreate | CFile::modeWrite); if (!file.m_pStream) return false; file.WriteString(_T(Byte,Hex,Code,Length\n)); for (const auto pair : m_codeMap) { CString strLine; strLine.Format(_T(%d,0x%02X,%s,%d\n), pair.first, pair.first, pair.second.c_str(), pair.second.length()); file.WriteString(strLine); } file.Close(); return true; }调用位置在CompressDlg中新增按钮IDC_BTN_EXPORT_CSV点击事件中调用m_compressor.ExportCodeTableToCSV(strCSVPath)。6. 我的改造习惯每次加新功能前先写三行测试用例再动代码从那以后我每次给这个 Huffman 项目加新功能都强制走一遍这三步写最小测试用例比如加密码功能先写一个纯文本文件内容Hello World手动用计算器算出H ^ 1、e ^ 2的结果存成预期二进制在CompressFile()末尾加ASSERT断言ASSERT(memcmp(pActual, pExpected, 11) 0)确保修改后行为不变用#ifdef DEBUG_ENCRYPTION包裹新逻辑发布版自动剔除避免影响性能也方便快速回滚。这习惯源于一次惨痛教训某次为支持 UTF-8 路径我把CString全部替换成std::wstring结果BitStream::WriteFile()因宽字符写入失败压缩文件变成空的而断言没开花了两天才定位。现在我的 VS 项目属性里“预处理器定义”永远带着DEBUG_ENCRYPTION;DEBUG_DRAGDROP就像给代码上了双重保险。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?