简介:这份资源面向使用 Visual C++ 进行桌面开发的程序员,聚焦在 VC 项目中集成 SQLite 轻量级嵌入式数据库这一常见需求,帮助解决数据库连接、大批量数据快速插入以及查询结果在 ListCtrl 控件中展示等实际问题,适合具备一定 C++ 与 MFC 基础、希望掌握本地数据存储方案的开发者参考。压缩包共 61 个文件,约 32.07MB,包含 sqlite3.h、sqlite3.lib、sqlite3.dll 等核心库与头文件,以及 cpp、h 源码、vcxproj 与 sln 工程文件、多个 db 数据库样例和 exe 可执行程序,另附 pdb、obj、tlog 等编译中间产物,构成一套可直接运行的完整工程。目前已有 199 人学习下载。读者可从中获得 SQLite 在 VC 下的完整集成范例、批量插入与数据读取的代码思路,以及数据库文件与工程目录的组织方式,便于快速套用到自己的项目中。
1. 在 VC 里用 SQLite:为什么我宁愿手写 C API 也不碰 ORM
接手一个 MFC 老项目时,配置数据散在几十个 ini 文件里,改一个参数要翻三台机器的目录。我第一反应是塞个 SQLite 进去——单文件、零配置、不用装服务,扔进安装包就能跑。但真动手才发现,VC 环境下用 SQLite 的坑比想象中多:官方只给了一个sqlite3.c和sqlite3.h,没有 lib、没有 dll、没有工程文件,新手拿到手往往不知道从哪下手。这篇笔记就围绕「VC + SQLite」这条线,把编译、封装、参数绑定、事务和几个血泪坑一次讲透。适合正在用 MFC、Win32 或纯 C++ 做桌面工具,需要本地持久化但又不想引入 MySQL、PostgreSQL 这类重方案的开发者。读完你能拿到一套可直接编译进工程的 SQLite 集成方案,以及一份我踩过的坑清单。
2. 把 sqlite3.c 编进 VC 工程:三种集成方式怎么选
SQLite 官方发布的源码包叫 amalgamation,核心就两个文件:sqlite3.c(约 800 万行预处理后的单文件)和sqlite3.h。VC 里集成它有三条路,选错了后面维护会很痛苦。
2.1 静态编译 vs 动态 DLL vs 系统自带
先说结论:桌面工具优先静态编译。原因很直接——SQLite 的 ABI 在不同版本间不保证兼容,你依赖系统里的winsqlite3.dll或者自己带一个 dll,都可能因为版本错位导致sqlite3_prepare_v2返回莫名其妙的错误码。静态编译把 SQLite 直接链进你的 exe,版本完全可控,安装包也不用额外带文件。
三种方式对比:
| 方式 | 产物 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 静态编译 | 代码进 exe | 无依赖、版本可控 | exe 增大 500KB~1MB | 桌面工具、安装包分发 |
| 动态 DLL | sqlite3.dll + lib | exe 小、可替换 | 需带 dll、ABI 风险 | 多程序共享、插件架构 |
| 系统 winsqlite3 | 无 | 零体积 | 版本随系统、API 受限 | 仅 Win10+ 且不挑版本 |
我一般选静态。下面按静态编译走一遍。
2.2 在 VS 里新建静态库工程并编译
不要直接把sqlite3.c拖进你的主工程——它编译慢,而且会污染主工程的预编译头设置。正确做法是单独建一个静态库工程。
步骤:
- VS 里「新建项目」→「静态库」→ 命名
sqlite3lib。 - 把
sqlite3.c和sqlite3.h拷进工程目录,添加到项目。 - 项目属性里改几个关键项(见下表)。
- 编译,得到
sqlite3lib.lib。
关键编译参数:
| 属性 | 设置值 | 原因 |
|---|---|---|
| C/C++ → 代码生成 → 运行库 | 多线程 (/MT) 或 多线程调试 (/MTd) | 与主工程一致,否则链接报 LNK2038 |
| C/C++ → 预编译头 | 不使用预编译头 | sqlite3.c 不认 stdafx.h |
| C/C++ → 高级 → 编译为 | 编译为 C 代码 (/TC) | 防止按 C++ 编译引发符号问题 |
| 链接器 → 系统 → 子系统 | 未设置 | 静态库不需要 |
编译时如果报error C2065: 'INVALID_FILE_ATTRIBUTES'之类,多半是 Windows SDK 版本太老,装个新 SDK 即可。
2.3 主工程链接与第一个可运行示例
主工程里把sqlite3lib.lib加进「链接器 → 输入 → 附加依赖项」,头文件目录指向sqlite3.h所在路径。然后写第一个测试:
#include "sqlite3.h" #include <stdio.h> int main() { sqlite3* db = nullptr; // 打开或创建 test.db,不存在则新建 int rc = sqlite3_open("test.db", &db); if (rc != SQLITE_OK) { // sqlite3_errmsg 返回最近一次错误的可读描述 printf("open failed: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); return -1; } const char* sql = "CREATE TABLE IF NOT EXISTS config(" "id INTEGER PRIMARY KEY," "key TEXT NOT NULL UNIQUE," "value TEXT);"; char* errMsg = nullptr; // 第三个参数是回调,执行非查询语句传 nullptr rc = sqlite3_exec(db, sql, nullptr, nullptr, &errMsg); if (rc != SQLITE_OK) { printf("exec failed: %s\n", errMsg); sqlite3_free(errMsg); // errMsg 由 SQLite 分配,必须用它自己的 free } sqlite3_close(db); return 0; }逻辑说明:sqlite3_open的第二个参数是二级指针,SQLite 内部会分配一个sqlite3结构并回填。即使 open 失败,db 句柄也可能非空,所以失败分支里仍要sqlite3_close。sqlite3_exec适合执行建表、插入这类不需要逐行取结果的语句,错误信息通过errMsg返回,必须用sqlite3_free释放,用free会崩。
参数说明:sqlite3_open还有个变体sqlite3_open_v2,第四个参数可以传SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE等标志,做只读打开或共享缓存时用它。日常用sqlite3_open够了。
3. 参数绑定与事务:别用 sprintf 拼 SQL
能跑通建表之后,下一步就是增删改查。这里有个新手最容易翻车的地方:用sprintf把用户输入拼进 SQL 字符串。轻则中文乱码,重则 SQL 注入把表删了。
3.1 prepare / bind / step 三步走
SQLite 的 C API 查询流程固定三步:sqlite3_prepare_v2编译语句 →sqlite3_bind_*绑定参数 →sqlite3_step执行。查询语句在 step 返回SQLITE_ROW时逐行取数据,返回SQLITE_DONE结束。
// 插入:用 ? 占位,绑定参数 const char* insertSql = "INSERT INTO config(key, value) VALUES(?, ?);"; sqlite3_stmt* stmt = nullptr; int rc = sqlite3_prepare_v2(db, insertSql, -1, &stmt, nullptr); if (rc != SQLITE_OK) { /* 处理错误 */ } // 参数下标从 1 开始,不是 0 sqlite3_bind_text(stmt, 1, "server_ip", -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, "192.168.1.10", -1, SQLITE_TRANSIENT); rc = sqlite3_step(stmt); if (rc != SQLITE_DONE) { /* 处理错误 */ } // 每次 prepare 都要对应一次 finalize,否则内存泄漏 sqlite3_finalize(stmt);逻辑说明:sqlite3_prepare_v2的第三个参数是 SQL 长度,传 -1 表示让 SQLite 自己算到\0。第四个参数回填 stmt 句柄。sqlite3_bind_text的第四个参数也是长度,-1 表示按\0结尾;第五个参数SQLITE_TRANSIENT告诉 SQLite「这份数据你复制一份存着」,因为传入的字符串可能是临时变量。如果传SQLITE_STATIC,你必须保证字符串在 step 之前一直有效,否则会读到野指针。
参数说明:绑定函数按类型分sqlite3_bind_int、sqlite3_bind_int64、sqlite3_bind_double、sqlite3_bind_text、sqlite3_bind_blob、sqlite3_bind_null。下标从 1 开始,绑错了不会报错,只会默默写错数据,这点很坑。
3.2 查询与列取值
查询比插入多一步取列:
const char* querySql = "SELECT id, value FROM config WHERE key = ?;"; sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db, querySql, -1, &stmt, nullptr); sqlite3_bind_text(stmt, 1, "server_ip", -1, SQLITE_TRANSIENT); // step 返回 SQLITE_ROW 表示还有一行,SQLITE_DONE 表示结束 while (sqlite3_step(stmt) == SQLITE_ROW) { int id = sqlite3_column_int(stmt, 0); // 第 0 列 const unsigned char* val = sqlite3_column_text(stmt, 1); // 第 1 列 printf("id=%d value=%s\n", id, val ? (const char*)val : "NULL"); } sqlite3_finalize(stmt);逻辑说明:sqlite3_column_*的下标从 0 开始,和 bind 的 1 起始正好相反,这是另一个高频错误点。sqlite3_column_text返回的指针由 SQLite 内部管理,下一次 step 或 finalize 后失效,要留数据必须自己拷走。如果列值为 NULL,返回空指针,直接printf("%s")会崩,所以上面做了判空。
3.3 事务:批量插入的性能分水岭
默认情况下 SQLite 每条 INSERT 都是一个独立事务,每次都要写磁盘、刷日志。插 1 万条数据可能要几十秒。包进一个事务里,同样的量能压到几百毫秒。
sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr); for (int i = 0; i < 10000; ++i) { sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db, "INSERT INTO config(key,value) VALUES(?,?);", -1, &stmt, nullptr); char key[32]; sprintf(key, "k_%d", i); sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, "v", -1, SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); } sqlite3_exec(db, "COMMIT;", nullptr, nullptr, nullptr);逻辑说明:BEGIN和COMMIT之间所有写操作合并成一次磁盘同步。如果中途出错,用ROLLBACK回滚。注意事务期间如果程序崩溃,SQLite 靠 journal 文件自动回滚,不会留下半截数据。
参数说明:SQLite 默认的 journal 模式是 DELETE,每次事务会创建和删除 journal 文件。如果追求更高并发,可以开 WAL 模式:PRAGMA journal_mode=WAL;,读和写不再互相阻塞。但 WAL 会额外生成-wal和-shm文件,分发数据库时要一起带上,或者先PRAGMA wal_checkpoint(TRUNCATE);合并回去。
4. 避坑与排查:VC 下用 SQLite 的五个真实翻车记录
这一章全是我自己踩过的,按「现象 → 原因 → 解决」写,你遇到时能直接对号入座。
4.1 中文写入后变成乱码
现象:用sqlite3_bind_text写入「测试」两个字,用 DB Browser for SQLite 打开显示乱码,或者读出来是问号。
原因:VC 工程默认字符集可能是 Unicode(宽字符),而sqlite3_bind_text接收的是 UTF-8 窄字符串。如果你把CString或wchar_t*直接强转成char*传进去,编码就错了。
解决:统一转 UTF-8。MFC 下用CStringA或WideCharToMultiByte转:
CStringW wstr = L"测试"; int len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, nullptr, 0, nullptr, nullptr); char* utf8 = new char[len]; WideCharToMultiByte(CP_UTF8, 0, wstr, -1, utf8, len, nullptr, nullptr); sqlite3_bind_text(stmt, 1, utf8, -1, SQLITE_TRANSIENT); delete[] utf8;读出来时反向转回CStringW。SQLite 内部一律按 UTF-8 存储,别指望它帮你做编码转换。
4.2 链接报 LNK2038 运行库不匹配
现象:主工程编译通过,链接时报LNK2038: 检测到"RuntimeLibrary"的不匹配项: 值"MT_StaticRelease"不匹配值"MD_DynamicRelease"。
原因:静态库工程用了 /MT,主工程用了 /MD(或反过来)。运行库不一致会导致堆分配和释放跨模块出错。
解决:两个工程的「C/C++ → 代码生成 → 运行库」设成同一个值。要么都 /MT,要么都 /MD。Debug 和 Release 也要分别对应(/MTd 对 /MTd,/MDd 对 /MDd)。
4.3 sqlite3_exec 的 errMsg 用 free 释放导致崩溃
现象:执行失败后调用free(errMsg),程序在退出时崩溃或堆损坏。
原因:errMsg是 SQLite 用自己的内存分配器分配的,和 CRT 的堆不是同一个。
解决:永远用sqlite3_free(errMsg)。同理,sqlite3_column_text返回的指针不要 free,它归 SQLite 管。
4.4 多线程访问同一个 db 句柄报 SQLITE_BUSY
现象:主线程写数据的同时,工作线程读同一个 db,偶尔返回SQLITE_BUSY或直接崩。
原因:SQLite 默认的线程模式是 serialized(可多线程),但前提是编译时没关掉。如果你编译 sqlite3.c 时定义了SQLITE_THREADSAFE=0,多线程访问就是未定义行为。即使开了线程安全,同一个连接上的并发写仍会互相阻塞。
解决:每个线程用独立的sqlite3*连接,或者所有数据库操作都丢到同一个线程的消息队列里串行执行。我一般选后者,简单可靠。如果确实要多连接,开 WAL 模式能缓解读写冲突。
4.5 数据库文件被占用无法删除
现象:程序退出后想删掉 test.db,提示「文件正在被另一个程序使用」。
原因:sqlite3_close返回了SQLITE_BUSY,说明还有 stmt 没 finalize。SQLite 不会强行关闭有未完成语句的连接。
解决:检查所有sqlite3_prepare_v2是否都有对应的sqlite3_finalize。用 RAII 包一层最省心:
class StmtGuard { sqlite3_stmt* stmt_; public: StmtGuard(sqlite3_stmt* s) : stmt_(s) {} ~StmtGuard() { if (stmt_) sqlite3_finalize(stmt_); } sqlite3_stmt* get() { return stmt_; } };另外,sqlite3_close失败时可以用sqlite3_next_stmt遍历还没释放的语句,定位泄漏点。
5. 进阶:用 sqlite3_blob 存二进制与自定义排序
基础增删改查跑通后,有两个场景值得单独说:存图片/文件这类二进制数据,以及让中文按拼音排序。
5.1 用 BLOB 存二进制并流式读写
小文件(几 KB 到几 MB)直接存进 BLOB 列最省事,不用管文件路径和权限。但别用sqlite3_bind_blob一次性塞大文件,内存会爆。用增量 BLOB API:
// 先插入一行占位,拿到 rowid sqlite3_exec(db, "INSERT INTO files(name,data) VALUES('a.bin', zeroblob(0));", nullptr, nullptr, nullptr); sqlite3_int64 rowid = sqlite3_last_insert_rowid(db); // 打开 BLOB 句柄准备写入 sqlite3_blob* blob = nullptr; sqlite3_blob_open(db, "main", "files", "data", rowid, 1, &blob); // 分块写入,每次 4KB char buf[4096]; size_t total = 0; while (/* 从文件读一块到 buf */) { sqlite3_blob_write(blob, buf, 4096, total); total += 4096; } sqlite3_blob_close(blob);逻辑说明:zeroblob(0)先占位,sqlite3_blob_open的第六个参数 1 表示可写。sqlite3_blob_write的第四个参数是偏移量,支持随机写。读的时候用sqlite3_blob_read,同样分块。这样内存占用恒定,几个 GB 的文件也能处理。
参数说明:BLOB 句柄打开期间,该行不能被其他连接修改,否则sqlite3_blob_write返回SQLITE_ABORT。写完记得sqlite3_blob_close,否则和 stmt 泄漏一样会卡住 close。
5.2 注册自定义排序规则处理中文
SQLite 默认的BINARY排序按字节比较,中文排序结果是乱的。想按拼音排,得注册自定义 collation:
int pinyinCompare(void*, int nA, const void* a, int nB, const void* b) { // 这里简化:实际应把 UTF-8 转成拼音再比较 // 可用 Windows API CompareStringEx 配合 LOCALE_CHINESE return CompareStringEx(L"zh-CN", 0, (LPCWSTR)a, nA/2, (LPCWSTR)b, nB/2, nullptr, nullptr, 0) - 2; } sqlite3_create_collation(db, "PINYIN", SQLITE_UTF16, nullptr, pinyinCompare); // 建表时指定:CREATE TABLE t(name TEXT COLLATE PINYIN);逻辑说明:sqlite3_create_collation注册一个名字,建表或查询时用COLLATE PINYIN引用。回调里拿到的是原始字节,需要自己转成宽字符再调系统 API。注意SQLITE_UTF16标志要和实际传入的数据编码匹配,传错了比较结果无意义。
参数说明:排序规则一旦被索引引用就不能随便改,改了要REINDEX。如果只是偶尔排序,不如在查询时用ORDER BY配合应用层排序,省得维护 collation。
5.3 验证集成是否成功的三个检查点
写完代码别急着交付,走一遍这三个检查:
- 版本一致性:
sqlite3_libversion()打印出来,和sqlite3.h里的SQLITE_VERSION对一下,确保头文件和库是同一版。 - 完整性检查:
PRAGMA integrity_check;返回ok才算数据库没损坏。发布前跑一次,能提前发现 journal 残留问题。 - 压力测试:开事务插 10 万条,计时。正常应该在 1 秒内。如果超过 5 秒,检查是不是漏了事务或者 journal 模式不对。
从那以后我每次把 SQLite 集成进新工程,都会先跑一遍PRAGMA integrity_check和版本核对,再写业务代码。这个习惯帮我省过至少两次「数据莫名其妙读不出来」的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取