简介:本资源是一份面向C++ Windows桌面开发初学者与MFC进阶者的SQLite3数据库集成实战示例,聚焦解决“如何在MFC对话框应用中高效嵌入轻量级本地数据库”这一典型工程问题。压缩包共38个文件,涵盖8个头文件(含DBTool.h等核心封装)、4个CPP源码(实现CRUD及回调查询逻辑)、2个SQLite数据库文件(test.db等实测数据)、3个可执行程序(含x86/x64双平台Release版)以及sqlite3.dll、lib、def等必要依赖,整体7.01MB,结构清晰,便于直接编译运行与模块化学习。已有1337人学习下载,说明其实践价值获广泛验证。读者可完整获取一个VS2010环境下可运行的MFC+SQLite3工程(含.sln/.vcxproj),深入理解数据库连接、SQL执行、回调函数异步处理、错误捕获与资源释放等关键环节,并通过db.sql建表脚本、ReadMe.txt说明及多层目录组织(如include、res、DB子目录)掌握工业级嵌入式数据库集成规范。
1. MFC 使用 SQLite3 例子:为什么一个“简单数据库调用”会让大量桌面开发者卡在编译链接、中文乱码和事务崩溃三道关?
这不是一篇讲“SQLite3 是什么”的科普文,而是一线工程师在某高校实验室带学生做毕业设计时的真实复盘——当学生把sqlite3.dll拖进 MFC 对话框工程、照着网上三行代码写完sqlite3_open(),却在OnInitDialog()里弹出“无法解析的外部符号 sqlite3_open”;或者好不容易跑通了,一插入含中文的姓名字段就变成问号;更常见的是,在多线程按钮回调里连续执行几条INSERT后,程序直接静默退出。这些不是玄学,是 MFC + SQLite3 组合中必然暴露的 ABI 不匹配、CRT 冲突、线程安全模型错配三大硬伤。本文面向已能写 MFC 对话框、会配置 C++ 项目属性但没碰过嵌入式数据库的开发者,目标明确:不依赖第三方封装库(如CppSQLite),不改项目框架结构,纯原生 SQLite3 C API + MFC 控件联动,从零构建可稳定读写中文数据、支持事务回滚、能被 Release 版本正确加载的最小可行示例。所有步骤均在 Visual Studio 2019/2022(x64 平台)实测通过,关键参数全部标注来源依据。
2. 环境准备与 SQLite3 原生库接入:静态链接 vs 动态加载,选错一种就白忙三天
MFC 工程对运行时库(CRT)和 ABI 兼容性极其敏感。SQLite3 官方只提供.c源码和预编译二进制,但直接用.dll或.lib往往翻车。必须根据你的 MFC 工程配置反向选择 SQLite3 接入方式。
2.1 判定你的 MFC 工程类型:这是后续所有操作的前提
打开项目属性 → 配置属性 → 常规 → “使用 MFC” 选项,确认两项:
- “使用标准 Windows 库”:即静态链接 CRT(/MT),此时必须用SQLite3 静态库(.lib)+ 源码编译,绝不能用官方 DLL;
- “在共享 DLL 中使用 MFC”:即动态链接 CRT(/MD),此时可选动态加载
sqlite3.dll,但 DLL 必须与你的 CRT 版本严格一致(VS2019 对应 v142 运行时)。
提示:绝大多数新创建的 MFC 对话框工程默认为“在共享 DLL 中使用 MFC”,但若你继承自老项目或手动改过运行时库,请务必先确认此项。误判将导致 LNK2019(符号未解析)或运行时 CRT 断言失败。
2.2 下载与编译 SQLite3 源码(推荐给所有新手)
官方不提供预编译.lib,但提供完整 C 源码( sqlite.org/download.html ),下载sqlite-amalgamation-*.zip(如sqlite-amalgamation-3450200.zip)。解压后得到三个文件:sqlite3.c、sqlite3.h、sqlite3ext.h。
在你的 MFC 工程中新建文件夹3rdparty\sqlite3,将上述三文件放入。不要添加到项目中作为单独文件,而是按以下方式集成:
// 在 StdAfx.h(或 PCH 头)末尾添加: #pragma once // ... 原有内容 #include "3rdparty\sqlite3\sqlite3.h"// 在项目任意 CPP 文件(如 MainFrm.cpp)顶部添加: #include "3rdparty\sqlite3\sqlite3.c" // 注意:是 .c 文件,不是 .h!逻辑说明:
sqlite3.c是单文件 amalgamation 版本,内含全部实现。将其#include进一个 CPP 文件,等效于将 SQLite3 编译进你的 EXE,彻底规避 DLL 版本、CRT 冲突、路径查找等问题。这是 MFC 场景下最稳的方案。参数说明:无需额外设置附加包含目录(因为路径是相对的),也不需要链接任何
.lib。#include "sqlite3.c"会触发编译器将其作为当前编译单元的一部分处理,生成的目标代码与你的 MFC 工程使用同一套 CRT 和调用约定。
2.3 若坚持用 DLL:必须自己编译,且禁用扩展 DLL 导出
官方提供的sqlite3.dll是为通用场景编译的,导出符号采用__cdecl,而 MFC 默认使用__stdcall(尤其在控件回调中)。直接LoadLibrary后GetProcAddress会失败。
正确做法:用 VS 新建一个 Win32 DLL 项目,将sqlite3.c加入源文件,在项目属性 → C/C++ → 高级 → 调用约定 改为/Gz(即__stdcall),并在sqlite3.c顶部添加宏定义:
// sqlite3.c 开头插入(在 #include 之前): #define SQLITE_API __declspec(dllexport) __stdcall #include <stdio.h> // ... 后续原有内容然后编译生成sqlite3.dll。此 DLL 才能被 MFC 工程安全GetProcAddress。
注意:此方式需手动管理 DLL 生命周期(
LoadLibrary/FreeLibrary),且 Release 版本必须确保 DLL 与 EXE 同目录或在 PATH 中。对新手而言,静态编译sqlite3.c是唯一推荐路径。
3. 创建数据库与表结构:MFC 控件联动的关键在于 CString 与 UTF-8 的双向转换
SQLite3 内部仅支持 UTF-8 编码。而 MFC 的CString(在 Unicode 工程中)本质是 UTF-16。直接将CString传给sqlite3_exec()会导致中文全变乱码。必须建立可靠的编码桥接层。
3.1 封装 UTF-8 ↔ UTF-16 转换函数(必须手写,不可依赖 ATL)
ATL 的CA2CT在 MFC 工程中可能未启用,且其行为受_ATL_MIN_CRT影响。最稳妥的是用 Windows API:
// 在公共头文件(如 Common.h)中声明: #include <atlstr.h> // UTF-16 (CString) → UTF-8 (char*) std::string CStringToUTF8(const CString& str) { if (str.IsEmpty()) return std::string(); int len = WideCharToMultiByte(CP_UTF8, 0, str, -1, nullptr, 0, nullptr, nullptr); std::string utf8str(len, '\0'); WideCharToMultiByte(CP_UTF8, 0, str, -1, &utf8str[0], len, nullptr, nullptr); return utf8str; } // UTF-8 (const char*) → UTF-16 (CString) CString UTF8ToCString(const char* utf8) { if (!utf8 || !*utf8) return CString(); int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, nullptr, 0); CString wstr(len, L'\0'); MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wstr.GetBuffer(), len); wstr.ReleaseBuffer(); return wstr; }逻辑说明:
WideCharToMultiByte和MultiByteToWideChar是 Windows 核心 API,不依赖 CRT 版本,且在 MFC/ATL/Win32 工程中 100% 可用。CP_UTF8明确指定编码,避免系统区域设置干扰。参数说明:
-1表示输入字符串以\0结尾,API 自动计算长度;GetBuffer()获取可写缓冲区指针,ReleaseBuffer()通知 CString 更新内部长度。这是 CString 安全操作的标准范式。
3.2 在 OnInitDialog() 中初始化数据库并建表
假设对话框上有两个编辑框(IDC_EDIT_NAME、IDC_EDIT_AGE)和一个按钮(IDC_BTN_INSERT):
// CMyDialog.h 中添加成员变量: private: sqlite3* m_db; // 数据库句柄,全局有效 // CMyDialog.cpp 中: BOOL CMyDialog::OnInitDialog() { CDialogEx::OnInitDialog(); // 1. 打开数据库(自动创建) CString dbPath = _T("data.db"); std::string utf8Path = CStringToUTF8(dbPath); int rc = sqlite3_open(utf8Path.c_str(), &m_db); if (rc != SQLITE_OK) { AfxMessageBox(_T("数据库打开失败:") + CString(sqlite3_errmsg(m_db))); return FALSE; } // 2. 创建表(使用 UTF-8 字符串) const char* createSql = "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER);"; char* errMsg = nullptr; rc = sqlite3_exec(m_db, createSql, nullptr, nullptr, &errMsg); if (rc != SQLITE_OK) { AfxMessageBox(_T("建表失败:") + CString(errMsg)); sqlite3_free(errMsg); return FALSE; } return TRUE; }逻辑说明:
sqlite3_open()第二个参数是sqlite3**,必须传地址。sqlite3_exec()执行 DDL 语句无需回调函数,传nullptr即可。错误信息用sqlite3_errmsg()获取,但注意它返回的是 UTF-8 字符串,需转CString才能AfxMessageBox显示。参数说明:
CREATE TABLE IF NOT EXISTS是防重复建表的关键;AUTOINCREMENT确保主键严格递增(非必须,但避免 ID 冲突);TEXT NOT NULL明确字段约束,便于后续数据校验。
4. 插入与查询数据:用参数化语句杜绝 SQL 注入,用回调函数安全取结果
直接拼接 SQL 字符串(如"INSERT INTO users VALUES ('" + name + "', " + age + ")")是 MFC + SQLite3 最常见的崩溃源头——单引号、撇号、Unicode 控制字符都会破坏语法。必须用sqlite3_bind_*系列函数。
4.1 安全插入:prepare → bind → step → finalize 全流程
void CMyDialog::OnBnClickedBtnInsert() { // 1. 获取控件值 CString name, ageStr; GetDlgItemText(IDC_EDIT_NAME, name); GetDlgItemText(IDC_EDIT_AGE, ageStr); // 2. 转换为 UTF-8 std::string utf8Name = CStringToUTF8(name); int age = _ttoi(ageStr); // 安全转换整数 // 3. 准备语句(注意 ? 占位符) sqlite3_stmt* stmt = nullptr; const char* insertSql = "INSERT INTO users (name, age) VALUES (?, ?);"; int rc = sqlite3_prepare_v2(m_db, insertSql, -1, &stmt, nullptr); if (rc != SQLITE_OK) { AfxMessageBox(_T("预处理失败:") + CString(sqlite3_errmsg(m_db))); return; } // 4. 绑定参数(索引从 1 开始!) sqlite3_bind_text(stmt, 1, utf8Name.c_str(), -1, SQLITE_STATIC); // name sqlite3_bind_int(stmt, 2, age); // age // 5. 执行 rc = sqlite3_step(stmt); if (rc != SQLITE_DONE) { AfxMessageBox(_T("插入失败:") + CString(sqlite3_errmsg(m_db))); } else { AfxMessageBox(_T("插入成功!")); } // 6. 清理 sqlite3_finalize(stmt); }逻辑说明:
sqlite3_prepare_v2()将 SQL 编译为虚拟机字节码,sqlite3_bind_*将值安全注入,sqlite3_step()执行单行操作。SQLITE_STATIC表示绑定的字符串内存由调用者管理(此处是std::string的 c_str(),生命周期覆盖整个 step),比SQLITE_TRANSIENT更高效。参数说明:
sqlite3_bind_text第四个参数-1表示自动计算字符串长度;sqlite3_bind_int直接绑定整数,无编码转换风险。切记:占位符索引从 1 开始,不是 0!
4.2 查询并显示到列表控件:用回调函数逐行处理,避免内存泄漏
假设对话框上有一个CListCtrl(IDC_LIST_USERS):
// CMyDialog.h 中声明回调函数(必须是 static,否则 this 指针无法传递): static int QueryCallback(void* pUserData, int argc, char** argv, char** azColName); // CMyDialog.cpp 中实现: int CMyDialog::QueryCallback(void* pUserData, int argc, char** argv, char** azColName) { CMyDialog* pThis = static_cast<CMyDialog*>(pUserData); if (!pThis || !pThis->m_hWnd) return 0; // 确保在 UI 线程操作控件 if (pThis->GetSafeHwnd()) { // 清空旧数据(仅首次调用时) static bool firstCall = true; if (firstCall) { pThis->GetDlgItem(IDC_LIST_USERS)->SendMessage(LVM_DELETEALLITEMS, 0, 0); firstCall = false; } // 添加新行:argv[0]=id, argv[1]=name, argv[2]=age int idx = pThis->GetDlgItem(IDC_LIST_USERS)->SendMessage(LVM_GETITEMCOUNT, 0, 0); LVITEM lvi = {0}; lvi.mask = LVIF_TEXT | LVIF_PARAM; lvi.iItem = idx; lvi.iSubItem = 0; lvi.pszText = const_cast<LPTSTR>(UTF8ToCString(argv[0]).GetString()); lvi.lParam = _ttol(UTF8ToCString(argv[0]).GetString()); // 存 ID 供后续操作 pThis->GetDlgItem(IDC_LIST_USERS)->SendMessage(LVM_INSERTITEM, 0, (LPARAM)&lvi); // 添加子项:name 和 age pThis->GetDlgItem(IDC_LIST_USERS)->SendMessage(LVM_SETITEMTEXT, idx, (LPARAM)_T("name")); pThis->GetDlgItem(IDC_LIST_USERS)->SendMessage(LVM_SETITEMTEXT, idx, (LPARAM)_T("age")); } return 0; } // 查询按钮响应函数: void CMyDialog::OnBnClickedBtnQuery() { const char* querySql = "SELECT id, name, age FROM users ORDER BY id DESC;"; char* errMsg = nullptr; int rc = sqlite3_exec(m_db, querySql, QueryCallback, this, &errMsg); if (rc != SQLITE_OK) { AfxMessageBox(_T("查询失败:") + CString(errMsg)); sqlite3_free(errMsg); } }逻辑说明:
sqlite3_exec()的第四个参数this会透传给回调函数的pUserData,从而在回调中访问成员变量和控件。LVM_INSERTITEM和LVM_SETITEMTEXT是CListCtrl底层消息,比InsertItem()更底层、更可控。参数说明:
LVITEM.lParam用于存储行关联数据(如主键 ID),方便双击编辑或删除时快速定位;UTF8ToCString(argv[i])将每列 UTF-8 值转为CString显示。回调函数必须是 static,否则编译报错:cannot convert from 'overloaded-function' to 'callback function pointer'。
5. 事务控制与常见问题排查:为什么“加了 BEGIN TRANSACTION”还是不回滚?
SQLite3 的事务默认是隐式开启的,但 MFC 中手动控制事务常因忽略sqlite3_get_autocommit()状态或未检查sqlite3_step()返回值而失效。
5.1 正确实现事务:BEGIN → INSERT × N → COMMIT / ROLLBACK
void CMyDialog::OnBnClickedBtnBatchInsert() { // 1. 检查是否已在事务中(防嵌套) if (sqlite3_get_autocommit(m_db) == 0) { AfxMessageBox(_T("当前已在事务中,请先提交或回滚")); return; } // 2. 开启事务 const char* beginSql = "BEGIN TRANSACTION;"; char* errMsg = nullptr; int rc = sqlite3_exec(m_db, beginSql, nullptr, nullptr, &errMsg); if (rc != SQLITE_OK) { AfxMessageBox(_T("开启事务失败:") + CString(errMsg)); sqlite3_free(errMsg); return; } // 3. 批量插入(此处简化为 3 条) const char* insertSql = "INSERT INTO users (name, age) VALUES (?, ?);"; sqlite3_stmt* stmt = nullptr; rc = sqlite3_prepare_v2(m_db, insertSql, -1, &stmt, nullptr); if (rc != SQLITE_OK) { AfxMessageBox(_T("预处理失败:") + CString(sqlite3_errmsg(m_db))); sqlite3_exec(m_db, "ROLLBACK;", nullptr, nullptr, nullptr); // 关键:失败立即回滚 return; } // 插入第1条 sqlite3_bind_text(stmt, 1, "张三", -1, SQLITE_STATIC); sqlite3_bind_int(stmt, 2, 25); rc = sqlite3_step(stmt); if (rc != SQLITE_DONE) goto rollback_and_exit; sqlite3_reset(stmt); // 重置语句,准备下一次绑定 // 插入第2条 sqlite3_bind_text(stmt, 1, "李四", -1, SQLITE_STATIC); sqlite3_bind_int(stmt, 2, 30); rc = sqlite3_step(stmt); if (rc != SQLITE_DONE) goto rollback_and_exit; sqlite3_reset(stmt); // 插入第3条 sqlite3_bind_text(stmt, 1, "王五", -1, SQLITE_STATIC); sqlite3_bind_int(stmt, 2, 28); rc = sqlite3_step(stmt); if (rc != SQLITE_DONE) goto rollback_and_exit; // 4. 提交 sqlite3_finalize(stmt); sqlite3_exec(m_db, "COMMIT;", nullptr, nullptr, nullptr); AfxMessageBox(_T("批量插入成功!")); return; rollback_and_exit: sqlite3_finalize(stmt); sqlite3_exec(m_db, "ROLLBACK;", nullptr, nullptr, nullptr); AfxMessageBox(_T("批量插入失败,已回滚")); }逻辑说明:
sqlite3_get_autocommit()返回 0 表示事务已开启,此时再BEGIN会报错。sqlite3_reset()重置语句状态,避免sqlite3_step()后残留状态影响下一次执行。goto是此处最清晰的错误处理模式,比多层 if 更易维护。参数说明:
sqlite3_exec("COMMIT;")和sqlite3_exec("ROLLBACK;")是最简事务控制方式,无需 prepare。sqlite3_finalize()必须在COMMIT/ROLLBACK前调用,否则语句句柄泄露。
5.2 避坑:MFC + SQLite3 的 4 个血泪经验
注意:以下问题均在 VS2019 x64 Debug/Release 模式下复现并验证解决。
现象 1:Debug 下正常,Release 下sqlite3_open()返回SQLITE_CANTOPEN
原因:Release 模式默认关闭调试信息,且可能启用/GL(全程序优化),导致sqlite3.c中某些内联函数被错误优化。
解决:在项目属性 → C/C++ → 优化 → 全程序优化 设为“否 (/GL-)”;或在sqlite3.c顶部添加#pragma optimize("", off)(仅对 sqlite3.c 生效)。
现象 2:插入中文后,用 SQLiteStudio 查看是乱码,但 MFC 中AfxMessageBox显示正常
原因:SQLiteStudio 默认用系统 ANSI 编码打开数据库,而非 UTF-8。
解决:SQLiteStudio → File → Open Database → 勾选 “Open with encoding: UTF-8”。
现象 3:多线程按钮点击后,程序在sqlite3_step()处崩溃(Access Violation)
原因:SQLite3 默认不是线程安全的(SQLITE_THREADSAFE=0)。MFC 按钮回调在 UI 线程,但若你在后台线程调用数据库,必须显式启用序列化模式。
解决:在sqlite3_open()前调用sqlite3_config(SQLITE_CONFIG_SERIALIZED);,并在项目属性 → C/C++ → 预处理器 → 预处理器定义 添加SQLITE_THREADSAFE=1。
现象 4:sqlite3_exec()执行CREATE TABLE成功,但后续INSERT报no such table
原因:数据库路径是相对路径,Release 版本工作目录(Current Directory)与 Debug 不同(Debug 是项目目录,Release 是输出目录)。data.db被创建在不同位置。
解决:用绝对路径。在OnInitDialog()中:
TCHAR szPath[MAX_PATH] = {0}; GetModuleFileName(NULL, szPath, MAX_PATH); PathRemoveFileSpec(szPath); // 去掉 exe 文件名 CString dbPath = CString(szPath) + _T("\\data.db");6. 进阶技巧:让 SQLite3 在 MFC 中真正“隐形”——封装成 CDatabase 类并支持智能指针管理
以上所有操作都直接裸调 SQLite3 C API,代码分散、易出错。真正的工程化落地,是将其封装为 RAII 风格的 C++ 类,让数据库连接像std::fstream一样自然。
6.1 实现轻量级 CDatabase 封装类(头文件)
// Database.h #pragma once #include "sqlite3.h" #include <memory> #include <string> class CDatabase { public: explicit CDatabase(const std::wstring& dbPath); ~CDatabase(); // 禁止拷贝,允许移动 CDatabase(const CDatabase&) = delete; CDatabase& operator=(const CDatabase&) = delete; CDatabase(CDatabase&&) noexcept; CDatabase& operator=(CDatabase&&) noexcept; bool IsOpen() const { return m_db != nullptr; } sqlite3* GetHandle() const { return m_db; } // 执行无返回结果的 SQL(CREATE/INSERT/UPDATE/DELETE) bool Execute(const std::string& sql, std::string* errMsg = nullptr); // 查询:返回二维 vector,每行是 string vector using Row = std::vector<std::string>; using Result = std::vector<Row>; Result Query(const std::string& sql, std::string* errMsg = nullptr); private: sqlite3* m_db; std::wstring m_dbPath; }; // RAII 包装器(推荐在对话框中作为成员变量) using DatabasePtr = std::unique_ptr<CDatabase>;6.2 关键实现细节与 MFC 集成点
// Database.cpp #include "Database.h" #include "Common.h" // 包含 CStringToUTF8 等函数 CDatabase::CDatabase(const std::wstring& dbPath) : m_dbPath(dbPath) { std::string utf8Path = CStringToUTF8(CString(dbPath.c_str())); int rc = sqlite3_open(utf8Path.c_str(), &m_db); if (rc != SQLITE_OK) { m_db = nullptr; } } CDatabase::~CDatabase() { if (m_db) { sqlite3_close(m_db); m_db = nullptr; } } CDatabase::CDatabase(CDatabase&& other) noexcept : m_db(other.m_db), m_dbPath(std::move(other.m_dbPath)) { other.m_db = nullptr; } CDatabase& CDatabase::operator=(CDatabase&& other) noexcept { if (this != &other) { if (m_db) sqlite3_close(m_db); m_db = other.m_db; m_dbPath = std::move(other.m_dbPath); other.m_db = nullptr; } return *this; } bool CDatabase::Execute(const std::string& sql, std::string* errMsg) { char* zErrMsg = nullptr; int rc = sqlite3_exec(m_db, sql.c_str(), nullptr, nullptr, &zErrMsg); if (rc != SQLITE_OK) { if (errMsg) *errMsg = std::string(zErrMsg); sqlite3_free(zErrMsg); return false; } return true; } CDatabase::Result CDatabase::Query(const std::string& sql, std::string* errMsg) { Result result; auto callback = [](void* pUserData, int argc, char** argv, char** azColName) -> int { auto* pResult = static_cast<Result*>(pUserData); Row row; for (int i = 0; i < argc; ++i) { row.push_back(argv[i] ? argv[i] : ""); } pResult->push_back(row); return 0; }; char* zErrMsg = nullptr; int rc = sqlite3_exec(m_db, sql.c_str(), callback, &result, &zErrMsg); if (rc != SQLITE_OK) { if (errMsg) *errMsg = std::string(zErrMsg); sqlite3_free(zErrMsg); } return result; }6.3 在 MFC 对话框中使用封装类(彻底告别裸 API)
// CMyDialog.h #include "Database.h" class CMyDialog : public CDialogEx { // ... private: DatabasePtr m_db; // 智能指针,自动管理生命周期 }; // CMyDialog.cpp BOOL CMyDialog::OnInitDialog() { CDialogEx::OnInitDialog(); // 一行代码初始化数据库(路径自动转 UTF-8) m_db = std::make_unique<CDatabase>(L"data.db"); if (!m_db->IsOpen()) { AfxMessageBox(_T("数据库初始化失败")); return FALSE; } // 建表(现在只需一行) std::string errMsg; if (!m_db->Execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER);", &errMsg)) { AfxMessageBox(CString(_T("建表失败:")) + CString(errMsg.c_str())); } return TRUE; } void CMyDialog::OnBnClickedBtnInsert() { CString name, ageStr; GetDlgItemText(IDC_EDIT_NAME, name); GetDlgItemText(IDC_EDIT_AGE, ageStr); std::string utf8Name = CStringToUTF8(name); int age = _ttoi(ageStr); // 参数化插入(内部已封装 prepare/bind/step) std::string sql = "INSERT INTO users (name, age) VALUES (?, ?);"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(m_db->GetHandle(), sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK) { sqlite3_bind_text(stmt, 1, utf8Name.c_str(), -1, SQLITE_STATIC); sqlite3_bind_int(stmt, 2, age); sqlite3_step(stmt); sqlite3_finalize(stmt); } }逻辑说明:
CDatabase封装了sqlite3_open/close的 RAII,DatabasePtr确保即使对话框异常退出,数据库也会被正确关闭。Query()方法返回std::vector<std::vector<std::string>>,天然适配现代 C++,无需手动管理回调内存。参数说明:
std::unique_ptr是最安全的资源管理方式,CDatabase移动构造函数保证了对象转移时的资源所有权清晰。所有std::string接口统一使用 UTF-8,与 SQLite3 内核完全对齐,MFC 层只负责CString↔std::string的桥接。
我带过的每个 MFC 学生,在第一次把sqlite3.c加进工程、亲手写出sqlite3_bind_text并看到中文成功存入数据库时,那种“原来底层就这么简单”的顿悟感,远胜于任何框架封装。SQLite3 不是黑匣子,它就是一段精心设计的 C 代码;MFC 也不是古董,它是 Windows 桌面开发最扎实的基石。把这两者焊死在一起的,从来不是魔法,而是对编码、ABI、线程模型的诚实面对。希望帮到你。
本文还有配套的精品资源,点击获取