恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

VC中集成SQLite:从编译到增删改查的避坑指南

  • 首页
  • 资讯中心
  • /
  • VC中集成SQLite:从编译到增删改查的避坑指南

相关资讯

模拟传输课件精讲:从AM到FM的调制主线与教学技巧 2026/10/12 18:20:07
关闭不必要的网络服务:提升安全访问与网络稳定性的实操指南 2026/10/12 18:20:07
5G信令分析实战:从抓包到注册失败定位的排查指南 2026/10/12 18:15:07

最新资讯

AI大模型如何重塑软件开发流程:用TaoToken统一Key打通Cline MCP与Windsurf BYOK
WizardLM-2 开源即删库复盘:MT-Bench 评测超越 GPT-4,7B 追平 Qwen1.5-32B 的实测路径
CTF逆向入门实战:从easy-100题掌握二进制分析与调试技巧
国产CAD主机厂落地难题拆解:从部署故障到跨软件协同的快速响应
BEM叶素动量理论工程实现:稳定求解诱导因子a与a‘
同为 8B 梯队:Qwen3.8 与上一代 Qwen3-8B 到底差在哪

今日推荐

Debian新手入门:从部署到日常操作的完整指南
MongoDB复制集扩缩容实战:从rs.add到选主事故复盘
条形码目标检测数据集实战:从YOLOv8训练到部署

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

VC中集成SQLite:从编译到增删改查的避坑指南

发布时间:2026/10/12 18:20:07
VC中集成SQLite:从编译到增删改查的避坑指南 简介本资源面向在Visual C环境下进行桌面开发的程序员聚焦SQLite嵌入式数据库的集成与实战应用帮助解决VC项目中数据库连接、大批量数据快速插入以及查询结果在ListCtrl控件中展示等常见问题。压缩包共61个文件约32.07MB包含sqlite3.h头文件、sqlite3.dll动态库与sqlite3.lib静态库等核心依赖以及cpp源码、vcxproj工程文件、sln解决方案、rc资源脚本和多个db示例数据库另附exe可执行文件与pdb调试符号便于直接编译运行与调试。资源中提供了完整的工程示例涵盖sqlite3_open建立连接、sqlite3_exec批量执行INSERT语句提升写入效率、sqlite3_prepare_v2配合sqlite3_step遍历结果集并填充ListCtrl以及sqlite3_close释放连接等关键流程读者可据此快速掌握VC与SQLite的配合方式。目前已有527人学习下载适合需要为桌面应用添加本地数据存储与界面展示能力的开发者参考借鉴。1. VC中sqlite的使用为什么你的第一个数据库Demo总在编译期翻车在VC里用sqlite最反直觉的一点是它根本不需要你装数据库服务也不需要配置数据源。你拿到的就是一个.c和一个.h编译进你的工程程序跑起来就能读写本地文件。听起来简单到不像真的但恰恰因为太简单很多人在第一步就卡住了——不是链接报错就是运行时提示数据库文件打不开或者中文写进去变成乱码。这个标题讲的是在Visual C环境下把SQLite嵌进C工程这件事。适合两类人一类是刚接手MFC或Win32桌面项目、需要本地持久化但不想引入重型数据库的开发者另一类是写过SQLite但一直在Linux或命令行下用、切到VC就各种水土不服的人。核心要解决的问题就三个怎么把SQLite的源码正确编进VC工程、怎么用C接口做增删改查、怎么避开Windows下特有的编码和路径坑。下面按我实际搭工程的顺序拆开讲。2. 把SQLite源码编进VC工程从下载到第一个可执行文件2.1 为什么选源码集成而不是动态库在VC里用SQLite常见做法有三种用官方预编译的DLL、用vcpkg之类的包管理器装、直接把sqlite3.c和sqlite3.h两个文件拖进工程。我一般会选第三种原因很实际。预编译DLL的问题在于版本和调用约定容易对不上。你拿到的sqlite3.dll可能是用某个特定运行库编的你的工程用/MD还是/MT、Debug还是Release稍有不匹配就是一堆LNK2019。包管理器虽然省事但团队里每个人环境不同有人装了有人没装CI上还得额外配一步时间久了就是隐患。源码集成就两个文件加到工程里一起编调用约定、运行库、优化选项全部跟着你的工程走不存在ABI问题。代价是编译时间多几秒但换来的是完全可控。SQLite官方把源码做成了单一翻译单元sqlite3.c加上sqlite3.h就能编不需要其他依赖。具体操作去SQLite官网下载页拿amalgamation压缩包解压后得到sqlite3.c、sqlite3.h可能还有sqlite3ext.h。把sqlite3.c和sqlite3.h拷到你的工程目录在VS里右键项目→添加→现有项把sqlite3.c加进去。头文件路径加到项目属性的C/C→常规→附加包含目录。2.2 编译选项里必须改的三个地方源码加进去直接编大概率能过但有几个选项不改后面会出问题。第一个是预处理器定义。在项目属性→C/C→预处理器→预处理器定义里加上SQLITE_ENABLE_COLUMN_METADATA和SQLITE_ENABLE_FTS5。前者让你能拿到列名相关的元数据写通用查询封装时很有用后者开启全文检索哪天要做搜索功能不用重新编。如果确定用不到全文检索FTS5可以不加能省一点体积。第二个是运行库一致性。项目属性→C/C→代码生成→运行库Debug下用/MTd或/MDdRelease下用/MT或/MD整个工程包括sqlite3.c必须一致。我见过有人主工程用/MDsqlite3.c单独设了/MT链接时一堆重复符号查了半天。第三个是C语言编译。sqlite3.c是C文件如果你的工程是CVS会用C编译器编它一般没问题但某些严格模式下会报类型转换警告。可以在sqlite3.c的属性里把C/C→高级→编译为设为编译为C代码避免不必要的C语义干扰。// stdafx.h 或 pch.h 里加确保整个工程都能用 #include sqlite3.h // 链接时不需要额外指定库因为sqlite3.c已经编进工程了 // 如果编译报无法解析的外部符号检查sqlite3.c是否真的参与了编译这段没什么复杂逻辑就是确保头文件可见。关键点是sqlite3.c必须作为源文件加入编译而不是只放在目录里。有人把文件拷进去但忘了添加到项目编译能过链接时报一堆未解析符号回头查半天。2.3 用sqlite3_open跑通第一个连接工程配好后写个最小验证打开一个数据库文件建一张表插一条数据读出来。#include sqlite3.h #include stdio.h int main() { sqlite3* db nullptr; char* errMsg nullptr; // 打开或创建数据库文件路径用UTF-8或宽字符转换后的形式 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 user( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER);; rc sqlite3_exec(db, sql, nullptr, nullptr, errMsg); if (rc ! SQLITE_OK) { printf(exec failed: %s\n, errMsg); sqlite3_free(errMsg); // errMsg由sqlite分配必须用sqlite3_free释放 sqlite3_close(db); return -1; } sqlite3_close(db); return 0; }sqlite3_open的第二个参数是数据库文件路径。传:memory:就是内存库程序退出数据就没了适合做单元测试。传具体文件名就是磁盘库文件不存在会自动创建。返回值rc等于SQLITE_OK才算成功失败时用sqlite3_errmsg拿错误描述。sqlite3_exec适合执行不需要返回结果集的SQL比如建表、插入、更新。第五个参数是错误信息输出如果传了errMsg出错时sqlite会分配一块内存存错误描述用完必须sqlite3_free否则内存泄漏。这个点很多人第一次用会忘。sqlite3_close关闭连接。如果还有未finalize的语句close会返回SQLITE_BUSY所以正规做法是先确保所有stmt都释放了再close。3. 增删改查的C接口写法prepared statement才是正路3.1 为什么不用sqlite3_exec拼字符串上面用sqlite3_exec建表没问题但如果你用它来插入用户输入的数据比如拼一个INSERT INTO user(name) VALUES( name )那就踩进SQL注入的坑了。name里如果带个单引号SQL直接语法错误带个恶意构造的字符串数据就被改了。正确做法是用prepared statement也就是sqlite3_prepare_v2 sqlite3_bind_* sqlite3_step这一套。它把SQL语句和参数分开参数通过绑定接口传入sqlite自己处理转义既安全又高效。批量插入时同一个stmt可以反复reset再bind省去重复解析SQL的开销。3.2 插入与查询的完整代码模板// 插入一条用户记录参数化绑定 bool insertUser(sqlite3* db, const char* name, int age) { const char* sql INSERT INTO user(name, age) VALUES(?, ?);; sqlite3_stmt* stmt nullptr; int rc sqlite3_prepare_v2(db, sql, -1, stmt, nullptr); if (rc ! SQLITE_OK) { printf(prepare failed: %s\n, sqlite3_errmsg(db)); return false; } // 绑定参数索引从1开始 sqlite3_bind_text(stmt, 1, name, -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 2, age); rc sqlite3_step(stmt); if (rc ! SQLITE_DONE) { printf(step failed: %s\n, sqlite3_errmsg(db)); sqlite3_finalize(stmt); return false; } sqlite3_finalize(stmt); // 必须释放否则资源泄漏 return true; } // 查询并遍历结果集 void queryUsers(sqlite3* db) { const char* sql SELECT id, name, age FROM user WHERE age ?;; sqlite3_stmt* stmt nullptr; int rc sqlite3_prepare_v2(db, sql, -1, stmt, nullptr); if (rc ! SQLITE_OK) { printf(prepare failed: %s\n, sqlite3_errmsg(db)); return; } sqlite3_bind_int(stmt, 1, 18); // SQLITE_ROW表示还有数据SQLITE_DONE表示结束 while ((rc sqlite3_step(stmt)) SQLITE_ROW) { int id sqlite3_column_int(stmt, 0); const unsigned char* name sqlite3_column_text(stmt, 1); int age sqlite3_column_int(stmt, 2); printf(id%d name%s age%d\n, id, name, age); } if (rc ! SQLITE_DONE) { printf(query error: %s\n, sqlite3_errmsg(db)); } sqlite3_finalize(stmt); }sqlite3_prepare_v2的第三个参数是SQL长度传-1表示自动计算到字符串结尾。第四个参数是输出stmt第五个参数一般传nullptr除非你需要知道SQL有没有被完整消费。sqlite3_bind_text的最后一个参数很关键。传SQLITE_TRANSIENT表示sqlite会复制一份字符串你传的指针后面可以立即释放传SQLITE_STATIC表示sqlite直接用你的指针你必须保证在stmt执行完之前那块内存有效。新手建议一律用SQLITE_TRANSIENT省心。sqlite3_step的返回值要区分SQLITE_ROW表示查询返回了一行SQLITE_DONE表示语句执行完毕插入、更新、建表成功都是这个其他值都是错误。查询时用while循环遍历所有行非查询语句调一次step判断是否等于SQLITE_DONE即可。sqlite3_finalize释放stmt每个prepare都必须对应一个finalize否则连接关闭时会报SQLITE_BUSY。这个错误在调试时经常遇到回头查代码发现某个分支忘了finalize。3.3 事务与批量插入的性能差异单条插入如果不包事务SQLite默认每条语句一个事务每次都要写磁盘。插一万条就是一万次磁盘同步慢到怀疑人生。包一个事务一万条可能不到一秒。// 批量插入用事务包裹性能差距是数量级的 bool batchInsert(sqlite3* db, const std::vectorstd::pairstd::string,int users) { sqlite3_exec(db, BEGIN TRANSACTION;, nullptr, nullptr, nullptr); const char* sql INSERT INTO user(name, age) VALUES(?, ?);; sqlite3_stmt* stmt nullptr; sqlite3_prepare_v2(db, sql, -1, stmt, nullptr); for (const auto u : users) { sqlite3_bind_text(stmt, 1, u.first.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 2, u.second); if (sqlite3_step(stmt) ! SQLITE_DONE) { sqlite3_exec(db, ROLLBACK;, nullptr, nullptr, nullptr); sqlite3_finalize(stmt); return false; } // 重置stmt以便下一次绑定不reset的话第二次bind会报错 sqlite3_reset(stmt); } sqlite3_finalize(stmt); sqlite3_exec(db, COMMIT;, nullptr, nullptr, nullptr); return true; }关键点是sqlite3_reset。同一个stmt在step返回SQLITE_DONE后必须reset才能重新bind。不reset直接bindsqlite会返回SQLITE_MISUSE。reset之后stmt回到刚prepare完的状态但绑定值还在重新bind会覆盖。事务用BEGIN TRANSACTION和COMMIT包裹出错时ROLLBACK。注意sqlite3_exec执行BEGIN/COMMIT时不需要错误回调但生产代码里最好检查返回值。4. Windows下用SQLite的避坑清单编码、路径、并发4.1 中文乱码sqlite3_bind_text的编码陷阱现象用sqlite3_bind_text插入中文用sqlite3_column_text读出来是乱码或者用其他工具打开数据库看到问号。原因SQLite内部默认按UTF-8存储文本。VC工程里如果用了MBCS字符集你的char字符串是本地编码GBK直接bind进去sqlite把它当UTF-8存读出来自然乱。如果工程用了Unicode字符集你拿到的是wchar_t直接转char*也会丢信息。解决统一转成UTF-8再bind。Windows下用WideCharToMultiByte把宽字符转UTF-8或者如果源是GBK先用MultiByteToWideChar转宽字符再转UTF-8。读出来时反向转回需要的编码。// 宽字符转UTF-8用于bind前处理 std::string wideToUtf8(const std::wstring wstr) { if (wstr.empty()) return std::string(); int size WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int)wstr.size(), nullptr, 0, nullptr, nullptr); std::string result(size, 0); WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int)wstr.size(), result[0], size, nullptr, nullptr); return result; }如果工程是MBCS且不想大改可以在打开数据库后执行PRAGMA encoding UTF-8;确保编码一致但根本办法还是统一用UTF-8。我一般建议新工程直接上Unicode字符集所有进出SQLite的字符串都走UTF-8中转。4.2 路径问题相对路径的当前目录不是你以为的那个现象sqlite3_open(test.db)返回成功但文件不知道建到哪去了或者换个方式启动程序就提示打不开。原因相对路径是相对于进程的当前工作目录不是exe所在目录。在VS里调试时工作目录默认是项目目录直接双击exe运行时工作目录可能是桌面或别的地方。MFC程序用文件对话框拿到的路径一般是绝对路径没问题但硬编码相对路径就会飘。解决要么用绝对路径要么在程序启动时用GetModuleFileName拿到exe路径拼出数据库文件的绝对路径。路径里的反斜杠在C字符串里要转义或者直接用正斜杠SQLite在Windows下也认。// 获取exe所在目录拼出数据库绝对路径 std::wstring getDbPath() { wchar_t buf[MAX_PATH] {0}; GetModuleFileNameW(nullptr, buf, MAX_PATH); std::wstring path(buf); size_t pos path.find_last_of(L\\/); if (pos ! std::wstring::npos) { path path.substr(0, pos 1); } path Ldata\\app.db; return path; }拿到宽字符路径后如果要传给sqlite3_open需要转成UTF-8。sqlite3_open在Windows下对UTF-8路径的支持是可靠的不要直接传宽字符。4.3 多线程访问一个连接不能跨线程乱用现象主线程打开数据库工作线程拿去查询偶尔崩溃或返回SQLITE_MISUSE。原因SQLite默认的线程模式是serialized理论上一个连接可以多线程用但实际中如果编译时选了multi-thread模式一个连接同一时刻只能被一个线程使用。而且就算serialized模式多线程争抢一个连接性能也差。解决每个线程用自己的sqlite3连接或者所有数据库操作都走同一个线程。如果确实需要多线程并发写开WAL模式打开数据库后执行PRAGMA journal_modeWAL;读和写可以并发写与写还是串行。WAL模式下数据库目录会多出-wal和-shm文件这是正常的。// 打开WAL模式提升并发读性能 sqlite3_exec(db, PRAGMA journal_modeWAL;, nullptr, nullptr, nullptr); sqlite3_exec(db, PRAGMA synchronousNORMAL;, nullptr, nullptr, nullptr);synchronous设为NORMAL在WAL模式下是安全和性能的平衡点FULL更安全但每次写都fsync慢。如果数据丢了也能重建NORMAL够用。4.4 编译期报LNK2019检查这三个地方现象链接时报无法解析的外部符号符号名看起来像sqlite3_xxx。原因sqlite3.c没参与编译或者参与编译了但被排除在链接之外或者调用约定不一致。解决第一确认sqlite3.c在解决方案资源管理器里且属性里“从生成中排除”是“否”。第二确认头文件包含路径正确没有包含到另一个版本的sqlite3.h。第三如果用了extern C确保sqlite3.h被包在extern C块里C工程包含C头文件必须这样。第四检查运行库设置是否一致前面提过。5. 把SQLite用稳的几个进阶习惯5.1 用RAII封装连接和语句裸调sqlite3_open和sqlite3_finalize代码里分支一多就容易漏。我习惯用简单的RAII类包一层构造时打开析构时关闭stmt同理。这样即使中间抛异常或提前return资源也能释放。class DbConn { public: DbConn(const char* path) { if (sqlite3_open(path, db_) ! SQLITE_OK) { db_ nullptr; } } ~DbConn() { if (db_) sqlite3_close(db_); } sqlite3* get() const { return db_; } bool ok() const { return db_ ! nullptr; } private: sqlite3* db_ nullptr; }; class Stmt { public: Stmt(sqlite3* db, const char* sql) { if (sqlite3_prepare_v2(db, sql, -1, stmt_, nullptr) ! SQLITE_OK) { stmt_ nullptr; } } ~Stmt() { if (stmt_) sqlite3_finalize(stmt_); } sqlite3_stmt* get() const { return stmt_; } bool ok() const { return stmt_ ! nullptr; } private: sqlite3_stmt* stmt_ nullptr; };这两个类不复杂但能省掉大量手写finalize的重复代码。注意拷贝构造和赋值要禁用避免双重释放。5.2 用sqlite3_changes确认写操作影响行数执行UPDATE或DELETE后想知道到底改了几行用sqlite3_changes。它返回最近一条INSERT/UPDATE/DELETE影响的行数。在事务里每条语句后调一次能拿到每条的影响数事务提交后再调拿到的是最后一条的。sqlite3_exec(db, UPDATE user SET age age 1 WHERE age 30;, nullptr, nullptr, nullptr); int affected sqlite3_changes(db); printf(updated %d rows\n, affected);这个值在业务逻辑里有用比如删除时判断是否真的删到了记录没删到可能要给用户提示。5.3 备份与导出用sqlite3_backup做在线备份SQLite自带在线备份API不用停服务不用锁库。sqlite3_backup_init sqlite3_backup_step sqlite3_backup_finish三步。适合做定时备份或导出快照。bool backupDb(sqlite3* src, const char* dstPath) { sqlite3* dst nullptr; if (sqlite3_open(dstPath, dst) ! SQLITE_OK) return false; sqlite3_backup* bk sqlite3_backup_init(dst, main, src, main); if (!bk) { sqlite3_close(dst); return false; } // -1表示一次性备份全部页面也可以分步传具体页数 sqlite3_backup_step(bk, -1); sqlite3_backup_finish(bk); sqlite3_close(dst); return true; }备份过程中源库如果有写操作SQLite会自动处理备份文件是一致的。分步备份时如果返回SQLITE_BUSY等一会儿再step。5.4 一个我常犯的错误忘了检查prepare的返回值最后说个血泪教训。有次写查询sqlite3_prepare_v2返回了错误但我没检查直接拿stmt去step结果stmt是空指针程序直接崩。后来养成习惯每个prepare后面立刻判断返回值不ok就打印sqlite3_errmsg并返回。这个错误在SQL写错时最常见比如表名拼错、列名不对prepare阶段就会失败而不是等到step。另外sqlite3_errmsg返回的字符串是英文的调试时看关键词就行比如no such table就是表不存在near ... syntax error就是SQL语法问题。生产环境如果要给用户看得自己映射成中文提示。希望帮到你。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号