简介本资源面向使用Visual C进行桌面开发的程序员聚焦SQLite轻量级嵌入式数据库在VC项目中的集成与实战应用。包内提供可直接编译运行的完整工程涵盖数据库连接建立、大批量数据快速插入、查询结果读取并填充至ListCtrl控件展示等核心环节帮助开发者解决数据存储与界面呈现的衔接问题。资源共61个文件包含sqlite3.h头文件、sqlite3.dll动态库与sqlite3.lib静态库以及cpp源码、vcxproj工程文件、sln解决方案和多个db示例数据库另有编译产物与资源文件压缩包约32.07MB结构完整便于直接复用。目前已有527人学习下载适合需要快速掌握VC环境下SQLite增删改查与批量操作技巧的初中级开发者参考借鉴。1. VC中sqlite的使用为什么老项目还在用它扛数据层接手一个维护了七八年的 MFC 工控上位机时我发现它的配置、报警记录、历史曲线采样点全塞在一个不到 2MB 的.db文件里没有服务端、没有安装依赖拷过去就能跑。这就是 VC 中 sqlite 使用的典型场景单机、离线、数据量中等、部署环境恶劣。SQLite 是进程内嵌的关系型数据库整个引擎编译进你的 exe 或做成一个 DLL不需要独立数据库服务进程不需要网络端口事务直接落盘。对 VC 开发者来说它解决的是「我不想为了存几百兆数据去装 MySQL、还要配 ODBC 数据源」这类问题。适合谁做设备上位机、测试工具、桌面客户端、嵌入式 HMI 的 C 工程师。如果你在做高并发写入或跨机共享这篇的选型结论会告诉你边界在哪。2. 在 VC 工程里把 sqlite3 跑起来三种集成方式与最小可运行代码2.1 源码 amalgamation 集成为什么我优先选它SQLite 官方提供 amalgamation 形式的源码把整个引擎合并成sqlite3.c和sqlite3.h两个文件。VC 工程里最省事的做法就是把这两个文件直接加进项目编译。好处是版本完全可控不依赖系统里装了什么编译出来的 exe 自带数据库能力拷到客户机器上不会因为缺 DLL 而翻车。常见做法是新建一个静态库工程或者直接把sqlite3.c加入主工程。我一般会单独建一个sqlite3静态库项目把sqlite3.c加进去主工程引用它。这样编译一次多个模块复用也避免主工程编译单元过大。需要注意编译选项。SQLite 默认会启用一些线程和内存相关的宏VC 下建议在预处理器里显式定义几个关键宏避免默认行为不符合预期// 在 sqlite3 静态库工程的预处理器定义中加入 // SQLITE_THREADSAFE1 多线程安全模式配合临界区使用 // SQLITE_ENABLE_COLUMN_METADATA 需要获取列元数据时打开 // SQLITE_OMIT_LOAD_EXTENSION 不需要动态加载扩展就关掉减小体积 // SQLITE_DEFAULT_MEMSTATUS0 关闭内存统计提升性能参数说明SQLITE_THREADSAFE有三个取值0 是单线程、1 是串行化、2 是多线程。VC 上位机通常有 UI 线程和采集线程同时访问选 1 最稳但每次调用会加锁性能略降。如果确定只有一个线程碰数据库选 0 能快一点。SQLITE_OMIT_LOAD_EXTENSION关掉后编译产物会小几十 KB对嵌入式场景有意义。2.2 动态库方式什么时候该用 DLL如果多个 exe 要共享同一个数据库引擎或者你不想让主工程编译时间变长可以把 sqlite3 编译成 DLL。导出符号需要处理因为 sqlite3.c 默认不导出。做法是在编译 DLL 时定义SQLITE_API__declspec(dllexport)使用方定义SQLITE_API__declspec(dllimport)。// DLL 工程预处理器 SQLITE_API__declspec(dllexport) // 使用方工程预处理器 SQLITE_API__declspec(dllimport)逻辑说明SQLite 的 API 声明都带SQLITE_API宏默认是空。通过重定义这个宏就能控制符号的导出导入。参数上要注意DLL 和 exe 必须用同一套运行库选项/MD 或 /MT否则跨堆释放内存会出问题。我见过有人 DLL 用 /MT、exe 用 /MD结果sqlite3_free释放时直接崩查了半天。2.3 最小可运行代码打开、建表、插入、查询不管哪种集成方式API 调用是一样的。下面这段代码可以直接抄进你的测试工程#include sqlite3.h #include stdio.h int main() { sqlite3* db NULL; char* errMsg NULL; int rc; // 打开数据库文件不存在会自动创建 rc sqlite3_open(test.db, db); if (rc ! SQLITE_OK) { printf(open failed: %s\n, sqlite3_errmsg(db)); sqlite3_close(db); return -1; } // 建表IF NOT EXISTS 避免重复执行报错 const char* createSql CREATE TABLE IF NOT EXISTS alarm( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL, level INTEGER DEFAULT 0, ts DATETIME DEFAULT CURRENT_TIMESTAMP);; rc sqlite3_exec(db, createSql, NULL, NULL, errMsg); if (rc ! SQLITE_OK) { printf(create failed: %s\n, errMsg); sqlite3_free(errMsg); sqlite3_close(db); return -1; } // 插入一条记录 const char* insertSql INSERT INTO alarm(code, level) VALUES(E001, 2);; rc sqlite3_exec(db, insertSql, NULL, NULL, errMsg); if (rc ! SQLITE_OK) { printf(insert failed: %s\n, errMsg); sqlite3_free(errMsg); } // 查询并打印 sqlite3_stmt* stmt NULL; rc sqlite3_prepare_v2(db, SELECT id, code, level FROM alarm;, -1, stmt, NULL); if (rc SQLITE_OK) { while (sqlite3_step(stmt) SQLITE_ROW) { int id sqlite3_column_int(stmt, 0); const unsigned char* code sqlite3_column_text(stmt, 1); int level sqlite3_column_int(stmt, 2); printf(id%d code%s level%d\n, id, code, level); } } sqlite3_finalize(stmt); sqlite3_close(db); return 0; }逻辑说明sqlite3_open打开或创建数据库文件返回句柄。sqlite3_exec用于执行不需要逐行处理的 SQL比如建表、插入、更新。查询用sqlite3_prepare_v2编译成语句对象再循环sqlite3_step取行sqlite3_column_*按列索引取值。参数上sqlite3_prepare_v2的第三个参数是 SQL 长度传 -1 表示自动计算到结尾。sqlite3_column_text返回的指针在下次 step 或 finalize 后失效需要拷贝走。提示sqlite3_exec的错误信息用sqlite3_free释放不要用 free因为 SQLite 可能用自己的内存分配器。3. 参数绑定与事务把插入性能从每秒几十条拉到几万条3.1 为什么拼接 SQL 字符串是血泪坑新手最容易写的就是用sprintf拼 SQL然后sqlite3_exec。两个问题一是注入风险虽然单机场景不担心攻击但字段里带单引号直接语法错误二是性能每条 SQL 都要重新解析编译。我实测过拼接方式插入一万条报警记录要十几秒换成参数绑定加事务后不到一秒。参数绑定用sqlite3_bind_*系列函数配合sqlite3_prepare_v2预编译一次、循环重置执行const char* sql INSERT INTO alarm(code, level) VALUES(?, ?);; sqlite3_stmt* stmt NULL; sqlite3_prepare_v2(db, sql, -1, stmt, NULL); sqlite3_exec(db, BEGIN TRANSACTION;, NULL, NULL, NULL); // 显式开事务 for (int i 0; i 10000; i) { sqlite3_bind_text(stmt, 1, E001, -1, SQLITE_STATIC); sqlite3_bind_int(stmt, 2, i % 5); sqlite3_step(stmt); sqlite3_reset(stmt); // 重置语句保留绑定 sqlite3_clear_bindings(stmt); // 清除绑定值避免残留 } sqlite3_exec(db, COMMIT;, NULL, NULL, NULL); sqlite3_finalize(stmt);逻辑说明?是占位符sqlite3_bind_text的第四个参数是长度-1 表示自动算到结尾第五个参数是析构行为SQLITE_STATIC表示字符串在 step 前不会被释放SQLITE_TRANSIENT表示 SQLite 自己拷贝一份。参数上如果绑定的字符串是临时缓冲区必须用SQLITE_TRANSIENT否则 step 时可能读到已释放内存。sqlite3_reset把语句恢复到 prepare 后的状态可以重新绑定再执行省去重复编译。3.2 事务的边界一次事务多少条合适事务把多次写操作合并成一次磁盘同步这是性能提升的关键。但事务不是越大越好。我一般按 1000 到 5000 条一批提交。太大内存里要缓存回滚日志异常断电恢复时间长太小磁盘同步次数多提升有限。// 分批提交的写法 const int BATCH 2000; sqlite3_exec(db, BEGIN;, NULL, NULL, NULL); for (int i 0; i total; i) { // bind step if ((i 1) % BATCH 0) { sqlite3_exec(db, COMMIT;, NULL, NULL, NULL); sqlite3_exec(db, BEGIN;, NULL, NULL, NULL); } } sqlite3_exec(db, COMMIT;, NULL, NULL, NULL);参数说明BATCH取值跟磁盘类型有关。机械盘同步慢可以取大一点SSD 同步快取小一点减少锁持有时间。如果采集线程和 UI 线程并发事务持有写锁期间 UI 读会被阻塞所以批量插入最好放在独立线程并且每批之间让出时间片。3.3 WAL 模式读写并发下的后悔药默认的 rollback journal 模式下写会阻塞读。上位机场景里采集线程在写、UI 线程在查很容易卡界面。开启 WAL 模式后读写可以并发写不阻塞读读不阻塞写。sqlite3_exec(db, PRAGMA journal_modeWAL;, NULL, NULL, NULL); sqlite3_exec(db, PRAGMA synchronousNORMAL;, NULL, NULL, NULL);逻辑说明WAL 模式会生成-wal和-shm两个附属文件数据库目录需要可写。synchronousNORMAL在 WAL 下是安全的只在 checkpoint 时同步性能比 FULL 好很多。参数上synchronous有 OFF、NORMAL、FULL、EXTRA 四档OFF 最快但断电可能损坏NORMAL 在 WAL 下推荐FULL 最稳但慢。注意 WAL 模式在共享目录比如网络盘上不能用会报错。注意WAL 文件在程序异常退出后可能残留下次打开会自动恢复。如果恢复失败检查目录权限和磁盘空间。4. 避坑与排查VC 下 sqlite 最常见的五类翻车4.1 中文乱码不是数据库的错现象插入的中文查出来是乱码或者查询条件带中文匹配不到。原因VC 工程默认用多字节字符集而 SQLite 内部按 UTF-8 存储。你传进去的 GBK 字节被当成 UTF-8 解释。解决统一转成 UTF-8 再绑定。用MultiByteToWideChar转宽字符再WideCharToMultiByte转 UTF-8。// GBK - UTF-8 char* GbkToUtf8(const char* gbk) { int wlen MultiByteToWideChar(CP_ACP, 0, gbk, -1, NULL, 0); wchar_t* wbuf new wchar_t[wlen]; MultiByteToWideChar(CP_ACP, 0, gbk, -1, wbuf, wlen); int ulen WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, NULL, 0, NULL, NULL); char* ubuf new char[ulen]; WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, ubuf, ulen, NULL, NULL); delete[] wbuf; return ubuf; // 调用方负责 delete[] }参数说明CP_ACP是当前系统 ANSI 代码页中文系统就是 GBK。如果工程已经用 Unicode 字符集直接WideCharToMultiByte转 UTF-8 即可。绑定的时候用SQLITE_TRANSIENT让 SQLite 拷贝一份避免缓冲区释放后数据失效。4.2 数据库文件被锁谁没关句柄现象程序运行中想用外部工具打开 db 文件提示被占用或者第二次sqlite3_open同一个文件失败。原因SQLite 默认在打开期间持有文件锁而且如果有语句没 finalize连接不会真正关闭。解决确保每个sqlite3_prepare_v2都有对应的sqlite3_finalize每个sqlite3_open都有对应的sqlite3_close。用 RAII 封装是个好习惯。class DbGuard { public: DbGuard(sqlite3* db) : db_(db) {} ~DbGuard() { if (db_) sqlite3_close(db_); } private: sqlite3* db_; };如果确实需要外部工具同时访问开 WAL 模式能缓解但写的时候仍然会锁。彻底方案是程序退出时关闭连接或者用只读方式打开。4.3 内存泄漏prepare 了不 finalize现象程序跑久了内存持续上涨任务管理器里看到句柄数也在涨。原因sqlite3_prepare_v2创建的语句对象不释放SQLite 内部会缓存编译结果长期不 finalize 就是泄漏。解决每个 prepare 配一个 finalize异常分支也要保证执行。用sqlite3_next_stmt可以遍历未释放的语句调试时打印出来定位。// 调试用列出所有未 finalize 的语句 sqlite3_stmt* s sqlite3_next_stmt(db, NULL); while (s) { printf(unfinalized: %s\n, sqlite3_sql(s)); s sqlite3_next_stmt(db, s); }4.4 多线程崩溃一个连接不能跨线程随便用现象采集线程和 UI 线程共用一个sqlite3*偶尔崩溃在sqlite3_step里。原因即使开了SQLITE_THREADSAFE1同一个连接也不能被多个线程同时使用串行化模式只是保证 API 调用是线程安全的不保证逻辑上的并发使用。解决每个线程用自己的连接或者用临界区把数据库操作包起来。我一般给数据库封装一个类内部用CRITICAL_SECTION保护所有公开方法。class Db { public: void Exec(const char* sql) { EnterCriticalSection(cs_); sqlite3_exec(db_, sql, NULL, NULL, NULL); LeaveCriticalSection(cs_); } private: sqlite3* db_; CRITICAL_SECTION cs_; };参数说明临界区粒度要控制好不要把整个批量插入都包进去否则 UI 线程等太久。可以在批量插入的每批之间释放锁。4.5 编译报错找不到 sqlite3.h 或链接错误现象fatal error C1083: 无法打开包括文件: sqlite3.h或者LNK2019: 无法解析的外部符号 sqlite3_open。原因头文件路径没加或者静态库没链接。解决项目属性里附加包含目录加上 sqlite3.h 所在目录链接器输入里加上 sqlite3.lib。如果用源码集成确认sqlite3.c在编译列表里。另一个常见坑是 C/C 混合编译sqlite3.h 没有extern C保护C 工程里包含会名字修饰不匹配。解决包含时加extern C。extern C { #include sqlite3.h }5. 进阶技巧用 sqlite3 的扩展能力把查询和备份做扎实5.1 自定义函数把业务逻辑下推到 SQL 层SQLite 允许注册自定义函数在 SQL 里直接调用 C 函数。比如报警等级需要根据数值动态计算可以注册一个level_name函数static void levelName(sqlite3_context* ctx, int argc, sqlite3_value** argv) { int level sqlite3_value_int(argv[0]); const char* name unknown; switch (level) { case 0: name info; break; case 1: name warn; break; case 2: name error; break; } sqlite3_result_text(ctx, name, -1, SQLITE_STATIC); } // 注册 sqlite3_create_function(db, level_name, 1, SQLITE_UTF8, NULL, levelName, NULL, NULL);逻辑说明sqlite3_create_function的第二个参数是 SQL 里用的函数名第三个是参数个数第四个是编码。注册后可以SELECT level_name(level) FROM alarm;。参数上sqlite3_result_text的最后一个参数用SQLITE_STATIC表示返回的字符串是静态存储不需要 SQLite 拷贝如果是动态分配的用SQLITE_TRANSIENT让 SQLite 拷贝后自己释放。5.2 在线备份不停止写入也能备份SQLite 提供在线备份 API可以在数据库运行期间把内容复制到另一个文件不需要停采集线程。这对工控场景很实用定时备份不影响生产。sqlite3* pBackup NULL; sqlite3_open(backup.db, pBackup); sqlite3_backup* bk sqlite3_backup_init(pBackup, main, db, main); if (bk) { // 每次复制 100 页循环直到完成 int rc; do { rc sqlite3_backup_step(bk, 100); if (rc SQLITE_BUSY || rc SQLITE_LOCKED) { Sleep(10); // 让出时间片等锁释放 } } while (rc SQLITE_OK || rc SQLITE_BUSY || rc SQLITE_LOCKED); sqlite3_backup_finish(bk); } sqlite3_close(pBackup);参数说明sqlite3_backup_step的第二个参数是每次复制的页数-1 表示一次复制完。分步复制的好处是每步之间可以处理锁冲突不会长时间阻塞写操作。SQLITE_BUSY和SQLITE_LOCKED时 Sleep 一下重试直到返回SQLITE_DONE。备份完成后记得sqlite3_backup_finish否则目标库可能不完整。5.3 用 EXPLAIN 看查询计划索引有没有生效数据量到几十万行后查询变慢第一反应是加索引。但索引有没有被用上不能靠猜。用EXPLAIN QUERY PLAN看EXPLAIN QUERY PLAN SELECT * FROM alarm WHERE code E001;输出里如果看到SCAN TABLE alarm说明全表扫描索引没生效。看到SEARCH TABLE alarm USING INDEX idx_code说明走了索引。常见索引不生效的原因查询条件对列做了函数运算、类型不匹配、或者统计信息过期。用ANALYZE;更新统计信息后再看。我一般会在建表后对常用查询列建索引但不过度建。每个索引都会拖慢插入速度报警表插入频繁索引控制在两三个以内。如果查询模式复杂考虑用覆盖索引把查询涉及的列都放进索引里避免回表。5.4 一个我踩过的坑DATETIME 默认值不是你想的那样建表时用DEFAULT CURRENT_TIMESTAMP存进去的是 UTC 时间不是本地时间。上位机显示的时候差 8 小时查了半天以为是时区配置问题。解决要么在插入时显式传本地时间字符串要么查询时用datetime(ts, localtime)转换。我现在的习惯是插入时用sqlite3_bind_text绑本地时间格式YYYY-MM-DD HH:MM:SS查询直接比较字符串简单可控。// 获取本地时间字符串 SYSTEMTIME st; GetLocalTime(st); char ts[32]; sprintf_s(ts, %04d-%02d-%02d %02d:%02d:%02d, st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); sqlite3_bind_text(stmt, 3, ts, -1, SQLITE_TRANSIENT);这个习惯帮我省了很多跨时区部署的麻烦。数据库里存什么格式显示层怎么格式化最好在项目初期就定死不然后期改起来到处是坑。希望帮到你。本文还有配套的精品资源点击获取