简介:针对MFC开发者,这份示例工程演示了在VS2010对话框应用中集成SQLite3数据库的完整流程,涵盖添加、删除、修改与查询操作,其中特别展示了基于回调函数的查询方式及同步/异步处理思路,适合初学者快速上手。压缩包共38个文件,总大小7.01MB,包含C++源码(h/cpp)、可执行程序(exe)、SQLite3运行库(dll/lib)、示例数据库(db)及工程配置(sln/vcxproj)等,源码与运行库分离,便于直接编译;同时附有可直接运行的程序及配套dll,无需安装即可运行验证,db文件提供预置测试数据,方便对比增删改查效果。目前已有1337人学习下载。通过该示例,读者可直观理解sqlite3_open、sqlite3_exec等API调用流程,掌握CRUD操作、错误处理与资源释放等关键细节,同时学习如何用回调函数逐行处理查询结果,为在Windows桌面端开发轻量级数据库应用打下基础,并能有效规避常见集成陷阱。
1. 把 SQLite 塞进 MFC 对话框程序:sqlite3 的引入比想象中多两道坎
MFC 程序需要本地存储时,我第一反应基本就是 sqlite3。它不用安装数据库服务,一个 DLL 加一个文件就能跑,特别适合对话框程序里的配置、记录、缓存这类轻量数据。不过,把 sqlite3 接进 MFC 工程并不是“下载一个 dll,写两句 SQL”这么简单。平台匹配、链接配置、字符编码、句柄释放,每一步都可能让程序在运行时突然翻车。这篇笔记按一个能跑的 MFC 对话框程序串下来:先给出最小引入路径,再讲列表展示、参数调优和常见坑,最后教你怎么自检。适合正在用 MFC 写工具类程序、又不想引入重量级数据库的工程师照着重现。
2. 引入 sqlite3 与项目配置:三条决定成败的路径
2.1 选型:预编译 DLL 比源码编译省事
sqlite3 官方在 sqlite.org 下载页同时提供源码包和 Windows 预编译二进制包。对绝大多数 MFC 工程,我建议用预编译 DLL:下载解压后一般能得到 sqlite3.dll、sqlite3.lib、sqlite3.h 三个文件,分别负责运行时、链接导入、头文件声明。把 dll 放到 exe 输出目录,在工程里链接 sqlite3.lib,就能开始写代码。
有人会问为什么不把 sqlite3.c 直接编译进工程。常见做法是它确实可行,但代价是你要自己处理 C 文件与 MFC 工程的运行库配置。预编译 DLL 的 sqlite3 已经按标准 Windows 运行库编好,和 MFC 的 /MD 模式兼容性最好。自己编源码时,一旦工程是静态链接 MFC,又要调整 SQLITE_THREADSAFE、预处理器定义,很容易在一个本来不用改的项目配置里折腾半天。对起步阶段,直接上官方 DLL 是最可靠的选择。
如果你打算给 sqlite3 加定制编译选项,比如启用某些扩展、调整页面大小默认值,再考虑换成源码编译。MFC 项目里这不是高频需求,等真需要时再迁移也不算晚。选型的关键是:目标是把业务跑起来,不要在引入环节埋雷。
2.2 包含目录、库目录与平台一致性
拿到 sqlite3 三件套后,最简单的方式是在 stdafx.h 或使用它的 cpp 文件顶部写两行:
#include <sqlite3.h> #pragma comment(lib, "sqlite3.lib")pragma comment 比到“链接器->输入->附加依赖项”里点鼠标更直观,也不会因为工程换机器后丢失配置。头文件所在目录如果不在工程默认包含路径里,需要到项目属性 -> VC++ 目录 -> 包含目录里追加,或者直接把 sqlite3.h 复制到工程目录。我一般倾向于把 sqlite3.h 放一个公共的 third_party 目录,然后只配包含目录,避免头文件散落。
真正容易踩坑的是平台一致性。Visual Studio 打开一个老 MFC 工程时,解决方案平台经常默认是 Win32,而官网下载的 sqlite 预编译包通常是 x64 目录下。你把 x64 的 sqlite3.lib 给 32 位工程链接,编译阶段会报一堆 LNK2019:无法解析的外部符号 sqlite3_open。如果你强行忽略 lib 版别,dllexe 运行时会直接提示加载失败。反过来,64 位工程链 32 位 lib 也会同样翻车。
所以配置目录前,先看两处:项目属性 -> 链接器 -> 常规里的“目标平台”,以及 Visual Studio 工具栏上的解决方案平台。再对照下载的 sqlite 包是 x86 还是 x64。官方包文件名一般写得很清楚,比如 sqlite-dll-win64-x64 之类的命名,按需取用。复制 dll 时也要复制到对应平台的输出目录;Debug 和 Release 的 OutDir 不同,不要只复制一次就觉得完事。
2.3 最小例子:建表、插入、查询、释放
接好库之后,先写一个最小的自检函数,能跑通就说明整条链路没问题。下面这个例子是一个控制台或对话框按钮里都能直接调用的完整流程:
#include <sqlite3.h> #pragma comment(lib, "sqlite3.lib") void RunSqlite3Demo() { sqlite3* db = nullptr; // 打开或创建数据库文件 int rc = sqlite3_open_v2("demo.db", &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr); if (rc != SQLITE_OK) { if (db) sqlite3_close(db); AfxMessageBox(_T("无法打开数据库")); return; } // 建表,IF NOT EXISTS 防止重复执行 rc = sqlite3_exec(db, "CREATE TABLE IF NOT EXISTS t_user(" "id INTEGER PRIMARY KEY AUTOINCREMENT," "name TEXT NOT NULL," "score REAL DEFAULT 0);", nullptr, nullptr, nullptr); if (rc != SQLITE_OK) { AfxMessageBox(CString(sqlite3_errmsg(db))); sqlite3_close(db); return; } // 插入:准备语句 + 绑定参数 sqlite3_stmt* stmt = nullptr; rc = sqlite3_prepare_v2(db, "INSERT INTO t_user(name, score) VALUES(?, ?);", -1, &stmt, nullptr); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, "zhangsan", -1, SQLITE_TRANSIENT); sqlite3_bind_double(stmt, 2, 98.5); if (sqlite3_step(stmt) != SQLITE_DONE) { AfxMessageBox(CString(sqlite3_errmsg(db))); } sqlite3_finalize(stmt); } // 查询 rc = sqlite3_prepare_v2(db, "SELECT id, name, score FROM t_user ORDER BY id;", -1, &stmt, nullptr); if (rc == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { int id = sqlite3_column_int(stmt, 0); const char* name = (const char*)sqlite3_column_text(stmt, 1); double score = sqlite3_column_double(stmt, 2); TRACE("id=%d name=%s score=%.1f\n", id, name, score); } sqlite3_finalize(stmt); } sqlite3_close(db); }这段代码覆盖了 sqlite3 基本操作里最核心的五个动作:打开、建表、插入、查询、关闭。sqlite3_open_v2 比老接口 sqlite3_open 多一个 flags 参数,推荐新代码都用它。SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE 表示文件不存在时自动创建;如果只读现有库,去掉 CREATE 即可。prepare_v2 里的 -1 表示按字符串结尾判断 SQL 长度,常规 SQL 这么写没问题。绑定参数的下标从 1 开始,不是 0,这是新手经常写错的地方。SQLITE_TRANSIENT 告诉 sqlite3 内部拷贝一份文本,这样即使传入的字符串在后续被释放也不会影响语句执行。每次 prepare 出来的 stmt 必须 finalize,否则会占用连接资源。TRACE 只在调试输出里打印,Release 下看不到,真要看数据需要配合界面展示。
2.4 打开与关闭的生命周期
数据库连接的打开和关闭,不要由某一个按钮函数单独负责。常见做法是在对话框类里放一个成员sqlite3* m_db;,在 OnInitDialog 里打开并 busy_timeout,在 OnDestroy 里关闭。这样能保证整个窗口存活期间只有一个连接,避免每个按钮函数临时开库、忘关句柄。
有人在 sqlite3_exec 里执行完一条 SQL 后没有关闭 stmt,然后反复打开新连接,程序在 Debug 下看起来正常,实际数据库文件一直被占用。这个问题在 MFC 程序里尤其常见,因为对话框关闭后进程不一定退出,dll 里残留的句柄让数据库文件无法删除。先养成一个习惯:所有打开路径必须有配套的关闭,所有 prepare 必须有配套的 finalize。想再稳妥一点,可以用一个简单的 RAII 包装,后面第 6 章会给骨架。
3. MFC 对话框查询与界面刷新:把 sqlite3 结果装进 CListCtrl
3.1 先解决显示:CListCtrl 从查询结果填充
sqlite3 查询结果是一行行读出来的,MFC 里最自然的展示控件是 CListCtrl。先给对话框加一个 List Control,把 View 设为 Report,在 OnInitDialog 里设置列头:
// 设置整行选中、显示网格线 m_list.SetExtendedStyle(m_list.GetExtendedStyle() | LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES); m_list.InsertColumn(0, _T("ID"), LVCFMT_RIGHT, 60); m_list.InsertColumn(1, _T("姓名"), LVCFMT_LEFT, 120); m_list.InsertColumn(2, _T("成绩"), LVCFMT_RIGHT, 80);之后写一个统一的加载函数。每次刷新前先 DeleteAllItems,再遍历查询结果插入数据。注意 sqlite3_column_text 返回的是 const unsigned char*,需要转成 const char* 处理,如果直接用 CString 接收,非 Unicode 工程还好,Unicode 工程下会出现乱码,后面专门讲转换。
void CMfcSqliteDlg::ReloadUserList() { m_list.DeleteAllItems(); sqlite3_stmt* stmt = nullptr; const char* sql = "SELECT id, name, score FROM t_user ORDER BY id;"; if (sqlite3_prepare_v2(m_db, sql, -1, &stmt, nullptr) != SQLITE_OK) { AfxMessageBox(CString(sqlite3_errmsg(m_db))); return; } int row = 0; while (sqlite3_step(stmt) == SQLITE_ROW) { CString idText; idText.Format(_T("%d"), sqlite3_column_int(stmt, 0)); const char* nameUtf8 = (const char*)sqlite3_column_text(stmt, 1); CString nameText = Utf8ToTchar(nameUtf8); CString scoreText; scoreText.Format(_T("%.1f"), sqlite3_column_double(stmt, 2)); m_list.InsertItem(row, idText); m_list.SetItemText(row, 1, nameText); m_list.SetItemText(row, 2, scoreText); ++row; } sqlite3_finalize(stmt); }这里每插入一行要调用一次 SetItemText,行的索引从 0 开始递增,和数据库里的 id 无关。如果表里有被删除的历史数据,id 会有空洞,不要用行号去映射 id,查询语句里带上主键即可。数据量不大时这种全量刷新完全够用,等上万行之后再考虑增量更新。
3.2 中文不乱码:TcharToUtf8 与 Utf8ToTchar
SQLite 的 TEXT 字段按 UTF-8 存储,而 MFC 的 CString 在 Unicode 工程里是 UTF-16。直接把 sqlite3_column_text 返回的字节串塞给 CString,等于把 UTF-8 当 UTF-16 用,结果必然乱码。正确的做法是:写入前把宽字符转成 UTF-8,读出后把 UTF-8 转回宽字符。下面两个函数是整个 MFC+sqlite3 方案里最值得复用的部分:
std::string TcharToUtf8(LPCTSTR text) { if (!text) return std::string(); int len = WideCharToMultiByte(CP_UTF8, 0, text, -1, nullptr, 0, nullptr, nullptr); if (len <= 0) return std::string(); std::string out(len - 1, '\0'); WideCharToMultiByte(CP_UTF8, 0, text, -1, &out[0], len, nullptr, nullptr); return out; } CString Utf8ToTchar(const char* text) { if (!text) return CString(); int len = MultiByteToWideChar(CP_UTF8, 0, text, -1, nullptr, 0); if (len <= 0) return CString(); CString out; wchar_t* buf = out.GetBuffer(len); MultiByteToWideChar(CP_UTF8, 0, text, -1, buf, len); out.ReleaseBuffer(); return out; }这段代码按 Unicode 工程处理,适用于 Visual Studio 默认的 MFC 项目设置。如果你的工程还停留在多字节字符集,TcharToUtf8 要改成先 GBK 转 Unicode,再由 Unicode 转 UTF-8,多一道中间步骤。转换函数的入参尽量用 LPCTSTR 而不是 CString,这样能直接接受字符串字面量和控件内容,调用处不用写一堆强制转换。
3.3 增删改查按钮的标准写法
在对话框上放两个编辑框和一个“添加”按钮,点击后读取输入,绑定参数插入数据库。这个流程是 MFC 对话框程序工程代码里最常见的形态:
void CMfcSqliteDlg::OnBnClickedBtnAdd() { CString name, scoreText; GetDlgItemText(IDC_EDIT_NAME, name); GetDlgItemText(IDC_EDIT_SCORE, scoreText); if (name.IsEmpty()) { AfxMessageBox(_T("姓名不能为空")); return; } double score = _ttof(scoreText); std::string utf8Name = TcharToUtf8(name); sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO t_user(name, score) VALUES(?, ?);"; if (sqlite3_prepare_v2(m_db, sql, -1, &stmt, nullptr) == SQLITE_OK) { sqlite3_bind_text(stmt, 1, utf8Name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_double(stmt, 2, score); if (sqlite3_step(stmt) != SQLITE_DONE) { AfxMessageBox(CString(sqlite3_errmsg(m_db))); } sqlite3_finalize(stmt); } ReloadUserList(); }要点是 SQL 里的值一律用 ? 占位,通过 bind 函数传进去。不要用 CString::Format 拼 SQL 字符串,MFC 的 CString 拼 SQL 时,单引号转义、百分号处理都是麻烦事,而且拼出来的语句容易被特殊字符注入。删除和修改同理,WHERE 条件走绑定参数。
更新语句绑定了一个 id 和一个 score,常见错误是绑定的顺序和 SQL 里 ? 出现顺序不一致。比如UPDATE t_user SET score=? WHERE id=?这两个占位符,bind 时第一个参数下标 1 表示 score,下标 2 表示 id。如果反过来绑定,程序不报错,但数据就更新错了。
3.4 刷新时的闪烁控制
数据量变大后,列表每次刷新会闪。原因很简单:DeleteAllItems 和每个 InsertItem 都会触发控件重绘。解决办法是在刷新前后关掉重绘:
m_list.SetRedraw(FALSE); ReloadUserList(); m_list.SetRedraw(TRUE); m_list.Invalidate();SetRedraw(FALSE) 期间控件不会重画,所有插入操作只在内存里更新,最后 SetRedraw(TRUE) 配合 Invalidate 一次性重绘。这个方法比 LockWindowUpdate 安全,LockWindowUpdate 在某些情况下会拦截鼠标消息,导致拖动滚动条时出现诡异行为。几百行数据场景下,这个改动能让界面观感完全不一样。
4. 参数调优:sqlite3 不是配好就能扛住
4.1 busy_timeout:处理 database is locked
MFC 程序里出现“database is locked”报错,绝大多数原因是 sqlite3 默认 busy_timeout 为 0。也就是说,一旦有其他连接暂时持有锁,sqlite3 会立刻返回 SQLITE_BUSY,而不会等对方释放。单进程单连接的程序也会撞上:杀毒软件扫描数据库文件、备份工具读取、另一个调试实例打开了同一个库,都会造成短时间锁冲突。
解决办法是在打开数据库后立刻设置等待超时:
sqlite3_busy_timeout(m_db, 3000);推荐值在 3000 到 5000 毫秒之间。太短,用户点击按钮时偶尔弹出锁错误;太长,真出现死锁时界面会卡住。busy_timeout 只对数据库忙有效,不能解决死锁。两个连接互相等对方释放资源时,超时到了照样报错。多线程程序里需要额外注意锁顺序,这个不是 dll 能替你解决的。
4.2 journal_mode=WAL:并发读写的缓冲垫
如果程序只有一个连接,默认的 rollback journal 模式已经够用。但很多 MFC 工具程序不止自己一个进程在访问数据库:测试脚本要读库、同事用 sqlite3.exe 查数据、程序内部又开了 worker 线程写日志。这种场景下,把 journal 模式切换成 WAL 会更宽容:
sqlite3_exec(m_db, "PRAGMA journal_mode=WAL;", nullptr, nullptr, nullptr); sqlite3_exec(m_db, "PRAGMA synchronous=NORMAL;", nullptr, nullptr, nullptr);WAL 模式下,写操作先追加到 -wal 文件,读操作还能读旧快照,读写并发能力比默认模式好很多。代价是数据库目录下会多出 .wal 和 .shm 两个文件,备份时不能只拷贝主文件。还有一点,WAL 在网络共享盘上的表现不稳定,如果程序的目标环境是 U 盘或公司共享目录,建议保留默认模式,否则可能出现无法预料的锁异常。
synchronous 配 NORMAL 是 WAL 下的常见组合。它降低了每次提交时强制落盘的强度,换来更高写入吞吐。对崩溃安全要求极高的场景,保持 FULL 更稳,但 MFC 本地工具程序通常没有这个必要。
4.3 批量提交纪律:一万行插入别一条一条 commit
sqlite3 默认每条 INSERT 语句都是自动提交,每写一行就要做一次磁盘同步。循环一万行,就是一万次磁盘写入,耗时完全是线性上涨。正确做法是手动包一个事务:
sqlite3_exec(m_db, "BEGIN;", nullptr, nullptr, nullptr); for (int i = 0; i < 10000; ++i) { sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(m_db, "INSERT INTO t_user(name, score) VALUES(?, ?);", -1, &stmt, nullptr); sqlite3_bind_text(stmt, 1, "name", -1, SQLITE_TRANSIENT); sqlite3_bind_double(stmt, 2, i * 0.1); sqlite3_step(stmt); sqlite3_finalize(stmt); } sqlite3_exec(m_db, "COMMIT;", nullptr, nullptr, nullptr);事务中途失败要执行 ROLLBACK,否则连接还挂着未提交事务,其他连接无法写入。批处理期间不要做界面刷新,把 UI 更新放到 COMMIT 之后。很多“导入卡死”的现场其实不是 sqlite3 慢,是自动提交模式下磁盘 IO 太多,加上 UI 线程又被循环堵住,界面自然假死。
4.4 参数对照表
sqlite3 常用配置参数按 MFC 工程的实际影响整理如下:
| 参数 | 默认值 | 建议值 | 影响 |
|---|---|---|---|
| busy_timeout | 0 | 3000~5000 | 决定锁冲突时等待多久,0 表示立刻报错 |
| journal_mode | delete | 保持默认或 WAL | 决定写事务落盘方式,WAL 适合读写并发 |
| synchronous | FULL | WAL 配 NORMAL | 控制同步策略,越高越安全,也越慢 |
| cache_size | -2000 | 8000 左右 | 页缓存大小,提高重复查询性能 |
| temp_store | file | memory | 临时表和排序是否走内存,数据量大才明显 |
这些参数之间会互相影响。开了 WAL 再调 synchronous=NORMAL 是常规组合;如果 journal 保持默认,却把 synchronous 改成 OFF,数据库崩溃后损坏概率会明显上升。MFC 工具类程序要的是稳定可维护,追求极限写入速度不如把代码写清楚。
5. MFC 工程里 sqlite3 最常翻车的五个场景
5.1 打开数据库失败:加载 DLL 报“应用程序无法正常启动”
现象:编译链接都通过,一运行就报错,对话框都弹不出来。原因基本只有两个:缺 sqlite3.dll,或 dll 的位数和 exe 不匹配。32 位 exe 加载 64 位 dll,Windows 会直接拒绝执行;反过来也一样。文件缺失时错误提示通常是“找不到 sqlite3.dll”。两种问题都发生在输出目录里,而不是源码目录。
解决:先确认解决方案平台是 x86 还是 x64,然后到 exe 输出目录看 dll 是否存在。如果 dll 文件名正确但位数不对,用 Visual Studio 自带的 dumpbin 工具查:
dumpbin /headers sqlite3.dll | findstr machine输出里 14C 表示 x86,8664 表示 x64。查到不匹配,去官网下载对应位数的预编译包,替换 dll 和 lib。这个坑在 MFC 教程里讲得少,因为教程通常默认你复制对了文件,但实际项目里几乎一半的引入失败都发生在这。
5.2 中文乱码:三个源头
现象:插入数据库后,用 sqlite3.exe 命令行查询显示正常,程序界面上却是乱码。或者反过来,程序里显示正常,命令行工具看到的是问号。原因有三个。第一个是 sqlite3_column_text 返回 UTF-8,直接赋给 MFC 的 CString,在 Unicode 工程下必然乱。第二个是用 CString 拼 SQL 时,宽字符被转成默认 ANSI 再送进 sqlite3,编码就变了。第三个是数据库文件本身被某个外部工具用非 UTF-8 编码写过,程序读到旧数据无法还原。
解决:统一走第 3 章的 TcharToUtf8 和 Utf8ToTchar,所有写入和读取都经过转换函数。对于已经被写乱的库,只能先确认源数据是什么编码,再做一次全表转码。这类问题排查起来比较烦,最好的办法是在第一天就定好“程序进出口必须 UTF-8”的规矩。
5.3 多线程共用同一个连接:黑匣子式崩溃
现象:程序在 Debug 下跑一整天没事,换 Release 之后偶发崩溃;或者加了后台线程后,打开对话框时偶尔卡死。原因:sqlite3 编译时默认支持多线程,但它的线程安全是有条件的。同一连接同时被两个线程调用 prepare/step,属于未定义行为,崩溃点和崩溃时机都无法预测。
解决:每线程一个连接,或者给唯一连接的访问加锁。我一般倾向每线程一个连接,sqlite3 打开连接的成本很低,数据库文件路径相同,线程内自己 open、自己 close,互不干扰。真出现两个线程同时写同一张表,交给 busy_timeout 和 WAL 处理锁等待。给唯一连接包一个 CCriticalSection 也能救,但锁的范围要覆盖整个 prepare、bind、step、finalize 周期,少一步都可能漏保护。
5.4 句柄泄漏:对话框一关,文件还是被占用
现象:程序关闭后,数据库文件删不掉,提示被另一个程序占用。原因:某处 prepare 了 stmt 没有 finalize,或者 sqlite3_open 成功之后,中间某个 return 分支跳过了 sqlite3_close。sqlite3_close 会释放连接的资源,但它也只能释放你自己没 finalize 的语句,不会帮你调用根本没执行的 close。
解决:所有 sqlite3_open 的后续分支,检查失败时也要先 close 再 return。所有 sqlite3_prepare_v2 成功之后,无论 sqlite3_step 是否执行成功,都要 sqlite3_finalize。在对话框 OnDestroy 里统一写sqlite3_close(m_db); m_db = nullptr;。Debug 模式下可以用 sqlite3_memory_used 和 _CrtDumpMemoryLeaks 配合确认泄漏源。这个问题隐蔽在现象上:程序不报错,只是文件锁着不放,慢慢才发现每次调试完都得强杀进程。
5.5 在 UI 线程批量写库导致界面假死
现象:点击“导入”“同步”按钮后,窗口标题显示“未响应”,过几秒或几十秒才恢复。原因:循环插入加上自动提交,全部跑在 UI 线程里,消息泵没有机会处理重绘和鼠标点击。
解决:把耗时操作放进 worker 线程,完成后 PostMessage 给主线程刷新列表。最朴素的写法是 AfxBeginThread 跑一个写库函数,函数结束前向对话框窗口发自定义消息,在主线程里调 ReloadUserList。注意 worker 线程里不要直接操作 MFC 控件,控件没有跨线程访问的保护。数据量小时这一条无所谓,一旦有了批量导入需求,这个坑几乎人人都踩。
6. 进阶:验证库文件与一个不绕圈的封装
6.1 用 sqlite3.exe 命令行做健康检查
官方 tools 包里有一个 sqlite3.exe,把它和数据库放到同一目录,在命令行里就能做基础体检:
sqlite3 myapp.db "PRAGMA integrity_check;" sqlite3 myapp.db ".tables" sqlite3 myapp.db "SELECT count(*) FROM t_user;"integrity_check 如果返回 ok,说明数据库物理结构没损坏。如果看到 database disk image is malformed,第一件事是立刻备份整个目录,包括 wal 和 shm 文件,再考虑恢复。这一步操作简单,价值很大,比写一堆检测代码靠谱。
6.2 一个比裸句柄更稳的封装思路
裸 sqlite3* 的生命周期问题,用一个很薄的类就能解决:
class CSqlite3Db { public: CSqlite3Db() : db_(nullptr) {} ~CSqlite3Db() { Close(); } bool Open(LPCTSTR path) { std::string pathUtf8 = TcharToUtf8(path); return sqlite3_open_v2(pathUtf8.c_str(), &db_, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr) == SQLITE_OK; } void Close() { if (db_) { sqlite3_close(db_); db_ = nullptr; } } sqlite3* Get() const { return db_; } private: sqlite3* db_; };这个封装不做什么高级处理,核心价值是让析构替人兜底。对话框成员里放一个 CSqlite3Db 对象,窗口销毁时自动关库,漏关闭的隐患能消掉一大半。至于 prepare 和 finalize 也可以封装成 RAII 样式,但建议先熟练裸 API,出了问题能定位到具体调用链。
我个人的习惯是:每次写完一批 sqlite3 相关改动,先跑一遍 PRAGMA integrity_check,再执行一次“插入-查询-删除”冒烟测试,最后关程序确认数据库文件能被正常重命名。这三步里有任何一步不对,优先怀疑没 finalize、没 close、编码没转换。这三个原因占了 MFC 工程里 sqlite3 问题的大半。希望帮到你。
本文还有配套的精品资源,点击获取