简介一份演示在MFC对话框程序中集成SQLite3数据库的完整示例工程基于Visual Studio 2010开发面向初学MFC或需要在Windows桌面应用中加入嵌入式数据库的开发者也可供课程设计与小型工具开发参考。项目覆盖从引入SQLite库、连接与打开数据库文件到建表、插入、删除、修改及查询的完整CRUD流程同时演示了标准同步查询与回调式查询两种方式并包含错误码检查、资源释放等易忽略但关键的细节。压缩包共38个文件整体约7.01MB主要包含源程序.h/.cpp、Visual Studio工程配置文件、可运行程序以及SQLite相关运行库.dll/.lib/.exe和数据库文件并同时提供32位与64位版本便于不同环境部署。已有1337人学习下载示例附带编译产物和测试数据库读者既可阅读源码理解调用关系也可直接运行查看效果再对照SQLite官方接口迁移到自己的项目中。1. MFC使用sqlite3其实不难难的是让这对组合在对话框工程里跑通很多 MFC 学习者第一次搜“MFC 使用sqlite3 例子”找到的往往是控制台版 demo拿回对话框工程却发现头文件找不到、中文路径打不开、列表控件刷新不出来。MFC 是 Windows 上的桌面界面框架sqlite3 是 C 语言写的嵌入式数据库两者本身都不复杂复杂的是字符集、链接选项和资源释放这些环境问题。这篇文章从库配置讲起用一个最小对话框工程走完增删改查再把常用操作收进一个封装类最后列出五条踩坑记录。适合要用 MFC 做本地配置存储、离线采集或工具软件数据管理的开发者不需要数据库服务器。2. MFC使用sqlite3的工程配置源码、静态库、DLL三条路线怎么选2.1 sqlite3 交付形态与 MFC 工程的默认差异sqlite3 的官方发布形态是合并版amalgamation通常就是 sqlite3.h、sqlite3.c 两个文件。很多 sqlite3 下载安装教程里也会告诉你把这两个文件直接加进工程就能用。这句话本身不假但它忽略了一个前提控制台工程和 MFC 对话框工程的预编译头、字符集、C 运行时库配置并不完全一致。直接在 MFC 工程里拖一个 sqlite3.c 进去第一时间会遇到的不是数据库问题而是编译器把 C 文件当成 C 来编译而报出的各种类型转换和函数重定义错误。所以在动手写代码之前先把接入方式定下来。我的经验是三种方式按使用场景选择学习阶段把 sqlite3.c 和 sqlite3.h 源码文件加入工程省去所有库路径配置产品交付用静态库 sqlite3.lib发布物只有一个 exe只有需要单独升级数据库引擎、或者第三方依赖强制要求 DLL 时才用动态库。三者的差异可以整理成一张表接入方式需要加入的文件发布物典型场景源码sqlite3.c sqlite3.h单个 exe学习、内嵌功能静态库sqlite3.h sqlite3.lib单个 exe交付、维护简单动态库sqlite3.h sqlite3.lib sqlite3.dllexe dll独立升级数据库引擎这里还要区分一个概念sqlite3.lib 和 sqlite3.dll 是两个不同形态的库前者在链接期驻留到 exe后者在运行期加载。网上不少教程会把两者混在一起说导致后面出现“编译过了运行找不到 DLL”的问题。MFC 工程一般默认使用动态链接的 MFC 库这和 sqlite3 用静态库并不冲突sqlite3.lib 进入 exe 后就不再需要额外 DLL。如果你的 Visual Studio 里连 MFC 选项都没有说明安装时没有勾选 MFC 组件。找一个 Visual Studio 离线安装 MFC 的教程或者在安装器的“单个组件”里选中“适用于最新 v143 生成工具的 C MFC(x86 和 x64)”补装一次即可。这条配置和 sqlite3 无关但它是 MFC 教程里最容易漏的一步缺了它后面的对话框工程根本建不出来。2.2 头文件路径、附加库目录和 pragma comment 三行配置现在假设你已经拿到 sqlite3.h 和 sqlite3.lib并把它们放在工程目录下的 vendor\sqlite3 里。这是很多正规 C 工程常用的目录结构好处是依赖项随工程走换一台机器或换一个队友都不丢。接下来只需要改三个地方。第一把 include 路径指到 sqlite3.h 所在目录项目属性 - C/C - 常规 - 附加包含目录填入 $(ProjectDir)vendor\sqlite3。第二把库文件目录指到 sqlite3.lib 所在目录链接器 - 常规 - 附加库目录填入 $(ProjectDir)vendor\sqlite3\lib。第三在预编译头 stdafx.h 或 pch.h 里写入下列两行#pragma comment(lib, sqlite3.lib) #include sqlite3.h第一行的作用是把 sqlite3.lib 写进链接命令行第二行让所有源文件都能看到 sqlite3 的接口声明。这里有一个细节如果把这两行写在某个 cpp 文件里那只有这个 cpp 能看到声明其他对话框文件要再写一遍。放进预编译头是最省心的办法。MFC 的预编译头在 VS2022 里默认叫 pch.h老工程叫 stdafx.h这两种名字对应同一个用途。如果你在项目属性里勾选了“使用预编译头”而实际没有对应头文件编译会报“无法查找或打开预编译头文件”这一般不是 sqlite3 的问题而是预编译头设置与新工程模板不一致。2.3 先定 x86 还是 x64一个决定整个调试过程的选项sqlite3 数据库文件格式与 CPU 位数无关同一个 notes.db 拷到 32 位程序和 64 位程序里都能打开。但 sqlite3.lib 本身分位数32 位程序必须链接 32 位版本的 lib64 位程序必须链接 64 位版本的 lib。如果混用链接器通常报“无法解析的外部符号 sqlite3_open”或“无法打开文件 sqlite3.lib”这两个错误都容易让人误以为头文件路径写错其实问题出在平台的 lib 路径上。开发 MFC 程序时我的建议是第一个对话框工程先统一到 x64现在的新机器基本都是 64 位系统MFC 对话框默认生成的 Win32 平台反而会让后续调试多一道转换。切换的方法是项目属性 - 配置管理器 - 活动解决方案平台 - 新建 - x64。切换之后回到 2.2 的附加库目录确认路径里的子目录仍然是 $(ProjectDir)vendor\sqlite3\lib如果当初把 32 位库和 64 位库放在同一个目录里最好拆成 lib\x86 和 lib\x64 两个子目录再用 $(Platform) 宏拼路径例如 $(ProjectDir)vendor\sqlite3\lib$(Platform)。这一步是很多人反复编译才记住的血泪经验数据库 API 本身跨平台自由但 MFC 工程的链接器一点自由都不给库文件位数必须和 exe 完全一致。开发期先定一个平台跑通全流程比一会儿换 Win32 一会儿换 x64 要省下大量时间。3. 在MFC对话框里跑通sqlite3增删改查一个最小可复现例子3.1 例子的界面和控件 ID一个文本框、一个列表、两个按钮这一节的目标是做一个类似“备注管理”的小窗口顶部输入框填写内容点添加按钮写入数据库中间的列表按倒序显示所有记录点删除按钮移除选中项。控件安排如下控件 ID类型作用IDC_EDIT_NOTECEdit输入备注内容IDC_LIST_NOTESCListBox显示记录列表IDC_BTN_ADDCButton添加记录IDC_BTN_DELCButton删除选中记录在对话框编辑器里放好这四个控件后为 IDC_LIST_NOTES 添加 CListBox 类型变量 m_listNotes为 IDC_EDIT_NOTE 添加 CEdit 类型变量 m_editNote。有一点必须注意CListBox 的 Sort 属性要设为 False否则 AddString 进去的行会按字母排序显示顺序错乱后后面的 ID 绑定逻辑就全乱了。这一套界面做完其实就是 sqlite3 基本操作里最常见的主干连接数据库、建表、插入、查询、删除。下面几个小节按事件函数拆开写全部可以直接粘贴到对话框类的 cpp 里。3.2 初始化数据库建表语句、数据库目录和中文路径转 UTF-8数据库文件放在 exe 同目录下文件名 notes.db。在 OnInitDialog 里先取 exe 所在目录再把完整路径转成 UTF-8 字节交给 sqlite3_open。注意实现文件顶部要包含 atlconv.h以下所有用到 CW2A、CA2W 转换宏的代码都一样。BOOL CMFCSQLiteDlg::OnInitDialog() { CDialogEx::OnInitDialog(); USES_CONVERSION; wchar_t szPath[MAX_PATH] { 0 }; ::GetModuleFileNameW(NULL, szPath, MAX_PATH); WCHAR* p wcsrchr(szPath, L\\); if (p) *p 0; m_strDbPath.Format(L%s\\notes.db, szPath); std::string dbPathUtf8 CW2A(m_strDbPath.GetString(), CP_UTF8); sqlite3* db nullptr; int rc sqlite3_open(dbPathUtf8.c_str(), db); if (rc ! SQLITE_OK) { CStringW strErr CA2W(sqlite3_errmsg(db), CP_UTF8); CStringW strMsg; strMsg.Format(L数据库打开失败: %s, strErr.GetString()); AfxMessageBox(strMsg); sqlite3_close(db); return FALSE; } const char* sqlCreate CREATE TABLE IF NOT EXISTS note( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP);; char* errMsg nullptr; rc sqlite3_exec(db, sqlCreate, nullptr, nullptr, errMsg); if (rc ! SQLITE_OK) { CStringW strErr CA2W(errMsg, CP_UTF8); AfxMessageBox(strErr); sqlite3_free(errMsg); } sqlite3_close(db); RefreshList(); return TRUE; }这里最值得记住的是编码链路。SQLite 的 sqlite3_open 只接受 UTF-8 编码的 char* 路径而 MFC 在 Unicode 字符集下的 CString 是 UTF-16。如果不转换中文目录就是打不开。CW2A 加 CP_UTF8 这个组合比直接 cast 成 CStringA 可靠得多后者在中文 Windows 上得到的是 GBK 字节SQLite 不认。建表语句用 CREATE TABLE IF NOT EXISTS 保证重复启动不报错create_time 用 CURRENT_TIMESTAMP 自动填充省掉在 MFC 代码里手动格式化时间的步骤。3.3 添加记录为什么用 bind_text 而不是拼 SQL 字符串添加按钮的响应函数里先取输入框文本再打开数据库用预处理语句插入。代码里没有用字符串拼 SQL这是有意为之原因后面说。void CMFCSQLiteDlg::OnBnClickedBtnAdd() { USES_CONVERSION; CStringW strNote; GetDlgItemText(IDC_EDIT_NOTE, strNote); if (strNote.IsEmpty()) return; std::string dbPathUtf8 CW2A(m_strDbPath.GetString(), CP_UTF8); sqlite3* db nullptr; if (sqlite3_open(dbPathUtf8.c_str(), db) ! SQLITE_OK) return; sqlite3_stmt* stmt nullptr; const char* sqlInsert INSERT INTO note(content) VALUES(?); if (sqlite3_prepare_v2(db, sqlInsert, -1, stmt, nullptr) ! SQLITE_OK) { sqlite3_close(db); return; } std::string noteUtf8 CW2A(strNote.GetString(), CP_UTF8); sqlite3_bind_text(stmt, 1, noteUtf8.c_str(), (int)noteUtf8.size(), SQLITE_TRANSIENT); if (sqlite3_step(stmt) ! SQLITE_DONE) { CStringW strErr CA2W(sqlite3_errmsg(db), CP_UTF8); CStringW strMsg; strMsg.Format(L插入失败: %s, strErr.GetString()); AfxMessageBox(strMsg); } sqlite3_finalize(stmt); sqlite3_close(db); RefreshList(); }sqlite3_prepare_v2 的第三个参数传 -1表示让 SQLite 自己判断 SQL 语句到哪里结束第四个参数是输出参数拿到 stmt 句柄。sqlite3_bind_text 的第五个参数 SQLITE_TRANSIENT 意思是SQLite 在内部复制一份文本因为 noteUtf8 在函数退出后就销毁了。如果这里误写成 SQLITE_STATICSQLite 会认为这个缓冲区一直有效函数返回后 stmt 里就是悬空指针轻则数据乱码重则崩溃。这和 MFC 字符串内存泄漏话题里常见的误报是同一种现象先检查 stmt 有没有 finalize再检查有没有拿着已释放的缓冲区。用 ? 占位符绑定参数另一个好处是不用处理文本里的单引号。用户输入“its ok”时拼 SQL 要手动把单引号翻倍bind 方式完全不用管。SQL 注入在这个本地数据库场景里威胁不大但转义出错是实打实的绑定参数一次解决。3.4 查询记录sqlite3_step 循环和 UTF-8 回显 CStringW刷新列表的函数被 OnInitDialog 和添加、删除按钮共同调用。每次先清空 CListBox再打开数据库按 id 倒序查出所有记录一行行塞进列表。查询结果的每一列通过 sqlite3_column_xxx 系列函数按下标取下标从 0 开始。void CMFCSQLiteDlg::RefreshList() { USES_CONVERSION; m_listNotes.ResetContent(); std::string dbPathUtf8 CW2A(m_strDbPath.GetString(), CP_UTF8); sqlite3* db nullptr; if (sqlite3_open(dbPathUtf8.c_str(), db) ! SQLITE_OK) return; sqlite3_stmt* stmt nullptr; const char* sqlQuery SELECT id, content FROM note ORDER BY id DESC; if (sqlite3_prepare_v2(db, sqlQuery, -1, stmt, nullptr) ! SQLITE_OK) { sqlite3_close(db); return; } while (sqlite3_step(stmt) SQLITE_ROW) { int noteId sqlite3_column_int(stmt, 0); const unsigned char* text sqlite3_column_text(stmt, 1); if (text nullptr) continue; CStringW strContent CA2W((const char*)text, CP_UTF8); CStringW strLine; strLine.Format(L%d - %s, noteId, strContent.GetString()); int index m_listNotes.AddString(strLine); m_listNotes.SetItemData(index, (DWORD_PTR)noteId); } sqlite3_finalize(stmt); sqlite3_close(db); }查询里的“ORDER BY id DESC”让最新记录显示在最上面这是备注类工具最常见的排序。CA2W 把 SQLite 返回的 UTF-8 字节还原成 CStringW这一段不做转换列表里的中文就会变成问号。SetItemData 把主键 noteId 存在列表项上后面删除时直接用不需要从字符串里再解析 id 出来。sqlite3_step 的返回值是 SQLITE_ROW 时表示还有下一行数据SQLITE_DONE 表示查询结束SQLITE_ERROR 则是执行出错。这个循环写法是 sqlite3 查询的标准姿势几乎所有查询场景都可以套用。3.5 删除记录从 ItemData 取主键别信行号删除按钮的响应函数里先取当前选中行再从这一行取出之前 SetItemData 保存的 noteId。这里不直接用行号做删除条件是因为 CListBox 的显示行号受排序、删除后重排的影响行号不稳定。void CMFCSQLiteDlg::OnBnClickedBtnDel() { USES_CONVERSION; int nSel m_listNotes.GetCurSel(); if (nSel LB_ERR) return; int noteId (int)m_listNotes.GetItemData(nSel); std::string dbPathUtf8 CW2A(m_strDbPath.GetString(), CP_UTF8); sqlite3* db nullptr; if (sqlite3_open(dbPathUtf8.c_str(), db) ! SQLITE_OK) return; sqlite3_stmt* stmt nullptr; const char* sqlDelete DELETE FROM note WHERE id?; if (sqlite3_prepare_v2(db, sqlDelete, -1, stmt, nullptr) SQLITE_OK) { sqlite3_bind_int(stmt, 1, noteId); sqlite3_step(stmt); } sqlite3_finalize(stmt); sqlite3_close(db); RefreshList(); }sqlite3_bind_int 和 bind_text 同属绑定参数族只是类型不同。DELETE 语句走同一套 prepare/bind/step/finalize 流程不要漏掉 finalize否则下一次打开数据库时可能拿到 SQLITE_BUSY这就是很多人遇到的“数据库文件被占用”的前兆。4. 把sqlite3接口收进CSQLiteHelper类减少MFC工程里的重复样板4.1 为什么要封装裸调 sqlite3 接口的三个失控点直接在主对话框代码里写 sqlite3_open、prepare、step、finalize小例子没问题功能一多就失控。第一个失控点是连接指针的生命周期sqlite3_open 返回的 sqlite3* 是一块堆内存每个函数都要负责 close中途 return 一多就容易漏。第二个失控点是 sqlite3_stmt 忘记 finalize这个句柄不释放数据库文件会被持续加锁程序退出时还可能触发内存泄漏报告。第三个失控点是错误信息分散在 errmsg、错误码和 errMsg 回调里排查时要到处翻。MFC 对话框的生命周期本身就长一个工具窗口开几个小时很常见资源管理问题会被放大。封装类的目的不是把 sqlite3 包装成某个高大上的 ORM而是把“打开、关闭、执行、取错误”这几个最容易写错的动作收口。MFC 四大类把窗口、视图、文档、对话框抽象得很好但它并不提供数据库访问的通用封装sqlite3 的接口是纯 C 函数自己包一个 helper 类比硬套框架更顺手。4.2 头文件设计连接、执行、错误三个职责CSQLiteHelper 只承担连接生命周期、简单 SQL 执行、错误信息三件事不封装复杂的查询结果。查询仍然由调用方自己 prepare 和 step因为 SQLite 的查询结果是一行一行流式获取的硬塞进一个类里只会让接口变得很难用。// CSQLiteHelper.h #pragma once #include afx.h #include sqlite3.h class CSQLiteHelper { public: CSQLiteHelper(); ~CSQLiteHelper(); bool Open(const CStringW strDbPath); void Close(); bool Execute(const char* sql); sqlite3* GetHandle() const { return m_pDB; } CStringW GetLastError() const; private: sqlite3* m_pDB; CStringW m_strLastError; };头文件里只暴露两个操作类方法Open 和 Execute。GetHandle 给外部做 prepare 用GetLastError 拿最近一次错误。Execute 只用来执行不需要返回数据的语句比如建表、BEGIN、COMMIT。查询一律由外部拿到句柄后自己走 prepare 流程这样类本身不限制 SQLite 的灵活用法。4.3 实现文件路径转换、错误信息和析构纪律// CSQLiteHelper.cpp #include stdafx.h #include CSQLiteHelper.h #include atlconv.h CSQLiteHelper::CSQLiteHelper() : m_pDB(nullptr) { } CSQLiteHelper::~CSQLiteHelper() { Close(); } bool CSQLiteHelper::Open(const CStringW strDbPath) { USES_CONVERSION; Close(); std::string dbPathUtf8 CW2A(strDbPath.GetString(), CP_UTF8); int rc sqlite3_open(dbPathUtf8.c_str(), m_pDB); if (rc ! SQLITE_OK) { if (m_pDB) { m_strLastError CA2W(sqlite3_errmsg(m_pDB), CP_UTF8); sqlite3_close(m_pDB); m_pDB nullptr; } return false; } return true; } void CSQLiteHelper::Close() { if (m_pDB) { sqlite3_close(m_pDB); m_pDB nullptr; } } bool CSQLiteHelper::Execute(const char* sql) { USES_CONVERSION; char* errMsg nullptr; int rc sqlite3_exec(m_pDB, sql, nullptr, nullptr, errMsg); if (rc ! SQLITE_OK) { if (errMsg) { m_strLastError CA2W(errMsg, CP_UTF8); sqlite3_free(errMsg); } return false; } return true; } CStringW CSQLiteHelper::GetLastError() const { return m_strLastError; }sqlite3_exec 只适合执行没有参数、没有结果的语句它内部封装了 prepare/step/finalize 全过程但把回调函数、错误字符串、结果集都暴露出来直接用容易绕晕。Execute 只接 SQL 字符串成功返回 true失败把错误码存进 m_strLastError。析构里的 Close 只是一个兜底不要把析构当作后悔药。MFC 对象的析构时机有时候比预想晚如果连接拖到对话框销毁才释放期间别的程序可能打不开这个数据库文件。所以实际使用中用完后显式调用 Close 比依赖析构函数更可靠。4.4 用 CSQLiteHelper 重写例子的添加流程代码量对比原来添加按钮的逻辑大概二十行封装后核心代码缩到十行左右void CMFCSQLiteDlg::OnBnClickedBtnAdd() { USES_CONVERSION; CStringW strNote; GetDlgItemText(IDC_EDIT_NOTE, strNote); if (strNote.IsEmpty()) return; CSQLiteHelper db; if (!db.Open(m_strDbPath)) { AfxMessageBox(db.GetLastError()); return; } sqlite3_stmt* stmt nullptr; if (sqlite3_prepare_v2(db.GetHandle(), INSERT INTO note(content) VALUES(?), -1, stmt, nullptr) ! SQLITE_OK) { AfxMessageBox(db.GetLastError()); return; } std::string noteUtf8 CW2A(strNote.GetString(), CP_UTF8); sqlite3_bind_text(stmt, 1, noteUtf8.c_str(), (int)noteUtf8.size(), SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); db.Close(); RefreshList(); }封装之后open 和 close 的配对关系更清楚错误信息统一从 GetLastError 拿不再到处写 CA2W(sqlite3_errmsg) 这一串。prepare 和 bind 仍然保留在业务层因为你迟早会遇到需要分页查询、模糊搜索、批量写入的场景这些都需要直接操作 stmt封装类挡在中间反而碍事。5. MFC sqlite3避坑实录字符集、链接库和资源释放的5个现场5.1 中文目录打不开数据库sqlite3_open 的 UTF-8 要求现象exe 放在“D:\项目工具\”目录下程序启动时报数据库打开失败把数据库文件挪到英文目录后一切正常。原因sqlite3_open 的第二个参数接收的是 UTF-8 编码的 char*而 CStringA 在中文 Windows 上得到的是 GBK 字节。SQLite 拿 GBK 路径去解析中文目录直接找不到返回 SQLITE_CANTOPEN。这个坑很容易被归咎于权限或杀毒软件实际上是编码问题。解决任何传给 sqlite3_open 的路径一律先转成 UTF-8std::string dbPathUtf8 CW2A(strDbPath.GetString(), CP_UTF8);排查时可以临时加一句输出把 dbPathUtf8.c_str() 打印到日志里用文本编辑器看它是不是正常的 UTF-8 字符。只要看到一串类似“E5 9B BE”这样的十六进制基本就能判断转换链条没断。提示MFC 的 CString 在工程使用 Unicode 字符集时是 CStringW也就是 UTF-16。跨过 SQLite 边界的所有非 ASCII 文本都要做一次 UTF-16 到 UTF-8 的转换。5.2 列表控件出现问号查询结果忘了转码现象添加中文记录成功数据库文件里也能查到但 CListBox 刷新后显示的全是“???”。原因sqlite3_column_text 返回的是 UTF-8 字节直接把 const unsigned char* 当 char* 赋给 CString等于把 UTF-8 字符串交给 Unicode 界面去解释。UTF-8 的多字节序列在 UTF-16 里映射不到正确字符显示成问号是常见结果。解决回显前用 CA2W 转回来CStringW strContent CA2W((const char*)text, CP_UTF8);如果列表控件本身绑定了 CString 变量也要保证字符串是从 UTF-8 正确转换后再塞进控件的。代码里出现“CA2W(text)”这行后中文问号问题会立刻消失。5.3 编译过了链接失败无法解析的外部符号 sqlite3_open现象错误列表出现 LNK2019无法解析的外部符号 sqlite3_open但 sqlite3.h 明明已经包含进来了。原因头文件找到了声明链接器却没有找到实现。常见有四种情况lib 文件没写进附加依赖项#pragma comment(lib, sqlite3.lib) 写在了没被编译的 cpp 里加的是 32 位 lib 但主工程是 x64或者同时把 sqlite3.c 和 sqlite3.lib 加进工程造成重复定义后链接器先报更早的错误。解决按顺序排查。先看预编译头里有没有 #pragma comment(lib, sqlite3.lib)再看项目属性 - 链接器 - 输入 - 附加依赖项 有没有 sqlite3.lib再看附加库目录里的实际路径是不是 $(ProjectDir)vendor\sqlite3\lib$(Platform)$(Platform) 展开后是否与当前活动平台一致。最后确认工程里没有同时存在 sqlite3.c 和 sqlite3.lib二者选其一。5.4 文件被占用sqlite3_stmt 未 finalize 导致 SQLITE_BUSY现象程序第一次读写正常第二次插入数据时返回 5SQLITE_BUSY或者在 Windows 资源管理器里删除 notes.db 时提示文件正在被占用。原因SQLite 没有内存回收机制。sqlite3_prepare_v2 生成的 sqlite3_stmt 必须逐个 sqlite3_finalize漏掉任何一个连接关闭时数据库文件仍然处于锁定状态。MFC 对话框里的按钮事件是反复触发的每点一次添加按钮就 prepare 一次忘了 finalize 就多一份句柄文件锁越积越多。解决每个 prepare 都配一个 finalize函数所有 return 分支都要覆盖到。排查时可以在 sqlite3_close 之前调用 sqlite3_next_stmt 遍历还有多少残留 stmt把数量打到调试输出里。用第 4 章的 CSQLiteHelper 之后连接集中在 Open/Close但 stmt 仍然要自己收尾finalize 这个动作无法被封装类完全替代。5.5 把 sqlite3.c 直接加进 MFC 工程后一堆编译错误现象按网上教程把 sqlite3.c 和 sqlite3.h 拖进 VS 工程编译后出现大量 C 语言函数与 C 命名冲突、类型不匹配、目标文件重定义的错误。原因sqlite3.c 是 C 语言源文件MFC 工程的 cpp 编译器默认按 C 语法编译它C 语言里一些隐式转换在 C 里直接报错。另一种情况是工程里同时加入了 sqlite3.c 和 sqlite3.lib链接时重复定义。解决走源码路线时右键 sqlite3.c - 属性 - 常规 - 项类型改成“C/C 代码”并确保编译选项里没有强制按 C 编译走静态库路线时把 sqlite3.c 从工程里排除只保留 sqlite3.h 和 sqlite3.lib。两条路只能选一条源码和 lib 混用是编译错误的高发区。6. 进阶用事务和预处理语句把批量写入提速10倍并验证数据文件6.1 循环插入的隐藏开销事务、reset 与 bind 复用工具类程序偶尔也会遇到批量导入需求比如从文本文件导入几千条配置。如果每条记录都用 prepare - bind - step - finalize 走一遍速度会非常慢。SQLite 默认是自动提交模式每一条 INSERT 都是一次单独的事务磁盘要同步一次。把一万条插入包进一个 BEGIN/COMMIT 事务里磁盘同步变成一次提速通常是十倍以上。另一个关键是复用 stmt。prepare 只在事务外做一次循环里用 bind 更新参数、step 执行、reset 重置而不是每条 SQL 都重新 prepare。代码如下sqlite3_exec(db.GetHandle(), BEGIN, nullptr, nullptr, nullptr); sqlite3_stmt* stmt nullptr; sqlite3_prepare_v2(db.GetHandle(), INSERT INTO note(content) VALUES(?), -1, stmt, nullptr); for (int i 0; i 10000; i) { std::string noteUtf8 ...; // 每次替换内容 sqlite3_bind_text(stmt, 1, noteUtf8.c_str(), (int)noteUtf8.size(), SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_reset(stmt); } sqlite3_finalize(stmt); sqlite3_exec(db.GetHandle(), COMMIT, nullptr, nullptr, nullptr);sqlite3_reset 不会清空绑定值它只是把 stmt 状态恢复到可以重新执行的位置。如果下一条数据的长度变化重新 bind 一下就行SQLite 内部会自动处理缓冲区的增长和释放。事务里如果有一条失败整个事务回滚一万条数据不会留下半截。6.2 用 sqlite3.exe 验证数据确认建表结构与中文编码程序写完不要急着关 IDE。打开命令行用 sqlite3.exe 指向生成的 notes.db执行sqlite3 notes.db .schema select id, content, create_time from note limit 10;.schema 能看到建表语句是不是你预期的那样select 能看到实际存储的内容。MFC 界面里中文正常命令行里也应该是同样的中文如果命令行里乱码说明写入阶段编码就错了。还有一个小习惯交付前用文本编辑器打开 notes.db 看前几百字节SQLite 的表名和 SQL 语句都以明文存在文件头附近如果看到奇怪的 GBK 字节说明某个环节把 UTF-8 混用了。我做 MFC 加 sqlite3 这类小工具时最深的教训是字符集问题一定是第一排查项其次就是每个 sqlite3_stmt 都要有归宿。把这两条养成肌肉记忆之后这个组合能覆盖本地缓存、离线采集、配置管理这一类需求而且发布物简单到只有一个 exe。希望帮到你。本文还有配套的精品资源点击获取
