当前位置: 首页 > news >正文

VC6/MFC老项目集成SQLite实战:编译、编码转换与升级管理

简介:这份VC++环境下SQLite集成示例工程,面向需要在Visual C++项目中嵌入轻量级数据库的开发者,重点解决大数据量快速写入和通过ListCtrl控件展示查询结果的问题。包内共61个文件,以C++源码(cpp/h)、SQLite数据库文件(db)、Visual Studio工程配置(sln/vcxproj)为主,同时包含编译生成的exe、dll、lib及调试信息,总计32.07MB。已有199人学习下载,可直接运行体验。资源提供完整可编译的MFC工程,包含数据库连接、批量INSERT语句执行、sqlite3_prepare_v2预处理查询并填充ListCtrl等关键代码,同时附带多个示例数据库,便于观察数据存储与界面联动的实际效果。代码结构简洁、注释清晰,适合具备一定VC基础的开发者作为参考并快速移植到自有项目中。 这两天帮朋友处理一个跑在工控机上的老项目,VC6 + MFC,界面还是朴素得不能再朴素的灰色对话框。新需求是把设备上报的温度、湿度历史数据落在本地,支持按时间段查询。朋友第一反应是写CSV文件,第二反应是Access,我让他直接用SQLite,并且把编译、调用、编码转换、表结构升级这些东西都梳理了一遍。这篇就当作一次完整记录:在VC环境下把SQLite真正用起来,到底要走哪些路。文章不搞花活,讲的是把sqlite3.c编进MFC工程、CString和UTF-8的互相转换、字节数组存BLOB、老版本SQLite怎么写“存在就更新不存在就新增”,以及数据库升级怎么管理。适合正在维护VC6、VS2008或者更老MFC程序,又不想引入重量级数据库的朋友参考。

1. 老方案对比:为什么我劝他用SQLite而不是Access或CSV

1.1 Access、CSV与SQLite的实际差距

先放一张我自己整理过的对比表,覆盖了我在工控项目里最关心的几项:

对比项AccessCSV文件SQLite
部署依赖需要ODBC驱动或OLE DB,工控机上经常没装无依赖无依赖,可静态编译进exe
SQL支持支持不支持,只能自己遍历支持,标准SQL
并发写入文件共享方式弱,容易锁库基本不适用多进程可读,写串行化
数据完整性中等差,断电易丢事务+崩溃恢复
调试便利性要装Access软件Excel可打开DB Browser for SQLite免费开箱

CSV文件的坑不用多说:数据量一上来,按时间区间查数据就是遍历整个文件,设备一天上报几千条数据,一个月下来文件几十MB,UI线程直接卡死。Access的问题是部署:很多工控机是几十年的老机器,装个Office或者ODBC驱动本身就是一场灾难,而且Access文件在网络共享下并发写入时经常出现“无法更新数据库”的诡异问题。SQLite是嵌入式的,一个文件就是整个数据库,查询、事务、崩溃恢复都内置,不用装任何东西。

1.2 集成方式怎么选:源码、DLL还是封装库

在VC里用SQLite,常见三种路线:

  1. 源码编译:从sqlite.org下载sqlite-amalgamation压缩包,把sqlite3.c、sqlite3.h直接加进工程。
  2. 动态库DLL:用别人编好的sqlite3.dll,或者自己编一个DLL,再通过LoadLibrary调用。
  3. 第三方封装库:比如CppSQLite3,把C API包了一层,写起来像用类。

我对长期维护项目的建议是第一种,源码编译。理由很实在:exe自带数据库引擎,不存在目标机器缺DLL的问题;SQLite的C代码开源,出问题时可以直接打到源码里跟踪;而且sqlite3.c是编译期优化,性能上通常比自己动态加载还稳定一点。DLL方式适合要升级SQLite版本、又不想重编主程序的场景,但老VC工程里做DLL的导出函数签名维护也比较烦。CppSQLite3这种封装我只在快速原型里用,正式项目还是推荐自己封一层,因为封装库对中文编码、错误日志这些没法完全贴合项目需求。

2. 环境准备:把sqlite3.c编进VC工程这一步容易踩坑

2.1 下载amalgamation源码,版本怎么选

到sqlite.org下载页拿sqlite-amalgamation压缩包,解压出来有三个文件:sqlite3.c、sqlite3.h、sqlite3ext.h。sqlite3ext.h主要是给扩展模块用的,一般用不到。

版本选择是个容易忽略的点。如果你还在用VC6,那编译器还是上个世纪的C89风格,新版sqlite3.c编译时很可能在类型定义、内部函数声明上报错。我的经验是用3.8.x或者3.7.x的amalgamation,功能对老项目完全够用。如果你用的是VS2008以上的工程,直接上最新版问题不大。所以别一上来就下最新源码,先确认编译器版本。SQLite官方其实早就停止对老编译器的兼容测试了,但老版本源码仍然稳定可用,这是老项目最务实的解。

2.2 把源码加进工程,以及必要的预处理器宏

操作上很笨但很有效:在VC工程的FileView里选中Source Files,右键Add Files,把sqlite3.c加进去。在要用SQLite的.cpp文件顶部包含sqlite3.h。sqlite3.h内部已经处理了extern "C"判断,所以即使在C++文件里直接include也没问题。

建议在预处理器定义里加上这几个宏,按需选择:

作用
SQLITE_ENABLE_COLUMN_METADATA提供sqlite3_column_table_name等元数据函数
SQLITE_ENABLE_FTS3 / FTS5启用全文搜索,老版本工程可用FTS3
SQLITE_ENABLE_JSON1启用JSON函数,只有新版源码支持
SQLITE_THREADSAFE=1启用多线程安全模式,默认就是1

这些宏要在编译sqlite3.c之前定义,最可靠的办法是在预处理定义里写全。注意FTS5在VC6上可能编不过,老工程用FTS3就好。

2.3 VC6编译报错的应急处理

真遇到VC6编译sqlite3.c报错,先别慌。常见的是缺少stdint.h或者某个类型没有定义。两个路子:一是换更老的SQLite源码,比如3.7.x,我实测VC6能编过;二是放弃纠缠,直接改用编译好的sqlite3.dll,用LoadLibrary + GetProcAddress动态调用。第二种方式虽然麻烦一点,但对维护老系统的朋友来说,比升级整个编译器要靠谱得多。另外,如果哪天项目决定彻底升级到VS2015+,sqlite3.c基本是直接拖进去就能编译的,不会制造额外困难。

3. 核心调用模型:prepare/step/finalize三步循环

3.1 打开连接,第一步先把路径问题想清楚

SQLite的C API不算复杂,一共就几十个函数,但核心流程高度统一。先说打开数据库:

sqlite3* pDb = NULL; int rc = sqlite3_open("data.db", &pDb); if (rc != SQLITE_OK) { CString strErr = sqlite3_errmsg(pDb); TRACE("open failed: %s\n", strErr); sqlite3_close(pDb); return; }

这里最容易踩的坑是路径。sqlite3_open("data.db")这个相对路径是相对于进程当前工作目录的,在VC调试环境下当前目录往往是工程目录,和exe所在目录不一样。所以发布后经常出现“在开发机跑得好好的,拷到目标机器就no such table”这种情况。稳妥做法是启动时拿到exe所在目录,拼上数据库文件名,再转成UTF-8传给sqlite3_open。注意中文路径也要转,SQLite内部对文件名按UTF-8处理,直接传GBK编码的中文路径在中文Windows上也可能打不开。

3.2 prepare/step/finalize是查询的唯一正确姿势

SQLite没有像ODBC那种直接返回结果集的接口,一切查询都走sqlite3_prepare_v2 -> sqlite3_step -> sqlite3_finalize这条链路。我写个查询示例:

sqlite3_stmt* pStmt = NULL; const char* sql = "SELECT id, name, value FROM sensor_data " "WHERE ts >= ?1 AND ts <= ?2 ORDER BY ts"; if (sqlite3_prepare_v2(pDb, sql, -1, &pStmt, NULL) != SQLITE_OK) { CString strErr = sqlite3_errmsg(pDb); TRACE("prepare failed: %s\n", strErr); return; } sqlite3_bind_int64(pStmt, 1, startTime); sqlite3_bind_int64(pStmt, 2, endTime); while (sqlite3_step(pStmt) == SQLITE_ROW) { int id = sqlite3_column_int(pStmt, 0); const unsigned char* name = sqlite3_column_text(pStmt, 1); double val = sqlite3_column_double(pStmt, 2); // 每行数据的业务处理 } sqlite3_finalize(pStmt);

为什么不用sqlite3_exec去拼SQL字符串?两个原因:一是拼接用户输入会导致SQL注入和单引号转义问题,二是每次执行都要重新解析SQL。而prepare解析一次之后,可以反复绑定不同参数执行,性能差异在大批量操作时非常明显。SQLite的占位符支持?、?NNN、:AAA、@AAA和$AAA,我习惯用?1、?2这种带编号的写法,语句一长也不会搞混。

写入操作套路完全一样,只是把sqlite3_step换成了sqlite3_reset重用,最后也要finalize:

const char* sql = "INSERT INTO sensor_data (ts, name, value) VALUES (?1, ?2, ?3)"; sqlite3_prepare_v2(pDb, sql, -1, &pStmt, NULL); sqlite3_bind_int64(pStmt, 1, ts); sqlite3_bind_text(pStmt, 2, strName, -1, SQLITE_TRANSIENT); sqlite3_bind_double(pStmt, 3, val); if (sqlite3_step(pStmt) != SQLITE_DONE) { // 处理约束冲突等错误 } sqlite3_reset(pStmt); // 如果循环复用 sqlite3_finalize(pStmt);

注意bind_text最后一个参数。SQLITE_TRANSIENT告诉SQLite:数据在语句执行期间要拷贝走。如果为了省一次拷贝传SQLITE_STATIC,那就必须保证字符串指针在statement生命周期内一直有效,很多人在这里踩了内存释放后悬垂指针的坑。

3.3 批量写入必须开事务,数据量差一个量级

如果一条一条INSERT,在SQLite里是极慢的。我拿1万条数据实测过:不开事务,稳定十几秒;开事务,1秒以内。原因很简单,每条INSERT默认都触发一次磁盘提交,而事务把一万次写合并成一次提交。

sqlite3_exec(pDb, "BEGIN", NULL, NULL, NULL); // 循环里执行INSERT sqlite3_exec(pDb, "COMMIT", NULL, NULL, NULL); // 如果中间出错,执行 sqlite3_exec(pDb, "ROLLBACK", NULL, NULL, NULL);

这里建议用BEGIN IMMEDIATE而不是BEGIN。BEGIN IMMEDIATE在事务一开始就获取写锁,避免两个连接同时BEGIN后再互相等待升级写锁导致SQLITE_BUSY。老项目里如果开了多线程访问同一个数据库,这个细节能少掉很多灵异报错。

4. 高频场景:CString转UTF-8、BLOB互转、存在就更新

4.1 中文乱码的根因,以及一套通用转换函数

VC6的CString默认是ANSI编码,中文环境就是GBK。而SQLite存储文本的惯例是UTF-8。如果你直接把GBK字符串bind_text进去,SQLite不会拦你,照样存得进去,也能读出来,因为字节没有变。但用DB Browser打开看就是一堆乱码;更麻烦的是换一台代码页不同的机器,数据可能完全不可读。所以规范做法是写入前转成UTF-8,读出来后转回GBK。

我写了一个小函数,在VC6和VS2008+的ANSI工程里都能用:

std::string CStringToUtf8(const CString& str) { if (str.IsEmpty()) return ""; // ANSI/GBK -> 宽字符 int nWideLen = ::MultiByteToWideChar(CP_ACP, 0, str, -1, NULL, 0); if (nWideLen <= 0) return ""; wchar_t* pwBuf = new wchar_t[nWideLen]; ::MultiByteToWideChar(CP_ACP, 0, str, -1, pwBuf, nWideLen); // 宽字符 -> UTF-8 int nUtf8Len = ::WideCharToMultiByte(CP_UTF8, 0, pwBuf, -1, NULL, 0, NULL, NULL); std::string strOut; if (nUtf8Len > 0) { strOut.resize(nUtf8Len - 1); ::WideCharToMultiByte(CP_UTF8, 0, pwBuf, -1, &strOut[0], nUtf8Len, NULL, NULL); } delete[] pwBuf; return strOut; }

反过来读取的时候,用sqlite3_column_text拿到UTF-8字节,再走一遍宽字符中转回CString。如果你用的是Unicode工程(CStringW),可以省掉第一步,直接从宽字符转到UTF-8。千万不要图省事直接用CString的+=拼接,然后当char*传给SQLite,那基本等于在

本文还有配套的精品资源,点击获取

http://www.cnnetsun.cn/news/4354285.html

相关文章:

  • 基于SpringBoot的云与糖蛋糕购物平台系统(毕设源码+文档)
  • 5090看直播还卡?解码链路与硬件加速排查指南
  • 自制象棋打谱与AI分析工具:python-chess+Stockfish实战教程
  • Claude Code启动提速:终端开发效率与配置指南
  • 从MPX到步枪:射击游戏武器选择的数据化评测与换枪指南
  • Upscayl 免费AI图片放大:完整安装与上手指南
  • 基于SpringBoot+Vue的大学城就餐推荐系统:偏好建模与实时推荐
  • 老笔记本驱动安装指南:以硬件ID识别为核心的声卡显卡驱动解决方案
  • DeepSeek Harness:一切皆插件的AI工作流编排层
  • 微信小程序本科毕业设计选题
  • GigaBrain-0.7:System-3双塔架构如何统一视觉语言理解与细粒度感知
  • 贝壳找房Java春招笔试卷深度解析:核心考点与工程实践
  • MKVToolNix 80.0 实战指南:无损封装视频、音频与字幕
  • Win11下USB Blaster驱动失效?从原理到解决全套指南
  • 联想22校招数据挖掘岗全解析:技术栈、项目与面试策略
  • 动态线程池实战:扩展ThreadPoolExecutor实现参数动态调整与监控
  • 企业级技术方案决策逻辑:从风险规避到战略匹配的六类应对策略
  • 时域与频域特征提取全解析:从FFT到故障诊断的工程实践
  • LLM概率输出并非贝叶斯?量化内部一致性的方法与工程实践
  • CNC程序传输实战:从RS-232到以太网,新手必学的机床通信指南
  • Python自动抢券脚本:精准卡点与并发请求实战
  • Obsidian+Codex:用AI打造自动化个人知识库工作流
  • 联想技术服务与开发质量类笔试复盘:题型拆解与备考策略
  • Chatbox 快速指南:桌面AI客户端的3个实战场景
  • 音游社区高难度谱面LTX2.5大乱跳:从文件导入到实战进阶全解析
  • HMC1119数控衰减器C++编程实战:SPI控制与驱动实现解析
  • DSH Desktop完全指南:把DeepSeek Harness变成一键安装、开箱即用的桌面AI智能体客户端
  • Ollama + BGE-M3:构建本地RAG的检索与生成分离实践
  • RevokeMsgPatcher|Windows 微信QQ防撤回补丁:三步上手的完整指南
  • PDFMathTranslate:3 分钟完成 PDF 文献翻译,公式图表一个不丢