简介面向VC开发者的SQLite集成示例包包含完整可运行工程与配套说明解决Visual C项目中接入SQLite、批量插入大量数据、查询结果绑定ListCtrl展示等典型需求。包内共61个文件以sqlite3.h头文件、sqlite3.dll/lib库文件、cpp源文件、sln/vcxproj工程文件为主另含pdb、obj、tlog等调试辅助文件压缩包约32MB结构清晰便于直接打开编译与对照学习。已有303人学习下载。示例演示sqlite3_open、sqlite3_exec、sqlite3_prepare_v2等API用法包括多条INSERT语句一次性执行的批量写入优化以及通过sqlite3_step与sqlite3_column_text遍历结果集并填充列表控件的实现逻辑随包附带多份db数据库文件可直接用于测试查询与界面展示。适合正在用VC开发桌面工具、MFC界面应用或需要快速集成轻量级本地数据库的开发者参考借鉴。1. VC 里跑 SQLite为什么不用 Access 而选嵌入式数据库先说一个反直觉的结论VC 6.0 这种老掉牙的开发环境配 SQLite 反而比配 Access 舒服。Access 需要 ODBC 驱动、需要注册表配置Release 到客户机器上动不动就报未发现数据源而 SQLite 只有一个 sqlite3.dll或者干脆把源码编译进工程连安装程序都不用写拷过去就能跑。如果你正在维护一个用 VC 写的桌面工具、上位机程序或者内部管理系统需要本地存放配置、日志、历史数据又不想背一个几百兆的数据库引擎SQLite 就是那个零运维的选择。这篇笔记把我在 VC 里集成 SQLite 的完整过程、参数细节和踩过的坑都过一遍适合从没用过 SQLite 的 VC 开发者直接照着做。2. 拿到 SQLite 源码头文件下载、编译与工程配置SQLite 官方发布的是 amalgamation 版本也就是把解析器、虚拟机、B-Tree 层和 API 全部揉进一个 sqlite3.c加上配套的 sqlite3.h。在 VC 里用最稳的做法不是去抓预编译 DLL而是从源码开始把 sqlite3.c 直接拖进工程参与编译。这样生成的库和你自己的程序没有任何 AB 兼容的坑。2.1 文件清单和编译选项去官网下载页找 sqlite-amalgamation 压缩包解开后有四个文件shell.c命令行外壳、sqlite3.c、sqlite3.h、sqlite3ext.h。项目里只需要 sqlite3.c 和 sqlite3.hshell.c 只是给你本地调试用的。把这两个文件放进一个 sqlite 源文件目录然后在 VC 工程里添加现有文件把 sqlite3.c 加进去。编译前需要确认工程的语言环境。SQLite 的 C 文件内部自带 SQLITE_THREADSAFE 等宏定义默认条件下是线程安全模式。VC 6.0 的工程默认是 Use Mixed Mode 的 C/C直接编译 sqlite3.c 一般能过但为了后续 C 文件调用方便建议把调用 sqlite 的头文件包一层// sqlite3_wrapper.h #pragma once extern C { #include sqlite3.h }逻辑说明sqlite3.h 是纯 C 接口头文件C 工程直接 include 会报链接错误函数名被 C name mangling 改了用 extern C 包住之后链接器按 C 符号解析调用 sqlite3_open、sqlite3_exec 就不会找不到函数了。参数说明sqlite3.c 的默认配置里SQLITE_THREADSAFE1也就是串行线程安全模式SQLITE_TEMP_STORE2临时表落到临时文件SQLITE_DEFAULT_PAGE_SIZE4096SQLite 3.12 以上默认 4096老版本 1024。这些默认值在桌面应用场景下都不用改直接编。2.2 动态库 vs 静态编译选哪个VC 项目有两种接法把 sqlite3.c 编进自己的 exe/dll静态编译或者单独编一个 sqlite3.dll然后在工程里链接 sqlite3.lib。我一般在内部工具里直接用静态编译理由有三个一是发布时少带一个 DLL省去忘了拷 DLL这种低级事故二是调试时可以直接进 sqlite3.c 看源码遇到锁冲突、数据库损坏的问题能追到 C 层三是 VC 6.0 的运行时是 msvcrt.dll 老版本动态库如果用的是新 VS 编译的运行时库不同容易出内存分配释放跨越模块边界的问题。静态编译要在工程设置里注意一个点C/C 选项卡 - 代码生成 - 运行时库整个工程统一用 Multithreaded DLL/MD或者 Multithreaded/MT不要混。如果 sqlite3.c 用 /MT其他文件用 /MD链接阶段大概率报 LIBCMT 冲突那可不是 sqlite 本身的错是 CRT 混用的老毛病。2.3 工程组织一个 sqlite 目录管所有我通常把 sqlite 相关文件单独放在工程下的 sqlite/ 目录然后 include 路径指向那里。这样做的原因是后续如果要升级 SQLite 版本只需要替换 sqlite3.c 和 sqlite3.h不需要翻整个工程找引用。升级 SQLite 有个额外收益新版本对损坏数据库的恢复能力、WAL 模式的稳定性都比老版本强很多尤其是你遇到数据库 disk I/O error这种错误时升级往往比改代码更有效。3. 核心 API 的 C 语言骨架打开、关闭与错误处理VC 里操作 SQLite 的完整套路就三件套sqlite3_open / sqlite3_close / sqlite3_errmsg。这三件套配合 sqlite3_exec 和 sqlite3_prepare_v2能覆盖 95% 的日常需求。这一章先把骨架搭好下一章再往里填增删改查的具体写法。3.1 打开与关闭注意数据库路径的编码问题sqlite3_open 接受 UTF-8 字符串路径。VC 6.0 的 CString 默认是 ANSIGBK 编码如果你直接把 CString 传给 sqlite3_open路径里有中文比如 C:\项目数据\data.dbSQLite 按 UTF-8 解析就会找不到文件然后自动创建一个新文件结果你明明写的是打开实际变成了新建空白库。// sqlite_db.h #pragma once #include windows.h #include string #include sqlite3_wrapper.h class SqliteDb { public: SqliteDb() : db_(NULL) {} ~SqliteDb() { Close(); } int Open(const char* utf8_path) { return sqlite3_open(utf8_path, db_); } // 用 ANSI 路径打开内部转 UTF-8 int OpenUtf8Path(const std::string ansi_path) { // 先用 MultiByteToWideChar 把 ANSI 转 UTF-16 // 再 WideCharToMultiByte 转 UTF-8 // 简化起见直接调 sqlite3_open 传入已转好的 UTF-8 return sqlite3_open(ansi_path.c_str(), db_); } void Close() { if (db_) { sqlite3_close(db_); db_ NULL; } } const char* LastError() { return db_ ? sqlite3_errmsg(db_) : db is null; } private: sqlite3* db_; };逻辑说明构造函数把 db_ 初始化为空指针析构时自动关闭这是防止中途 return 忘记 Close 的兜底。Open 系列函数直接透传 sqlite3_open 的返回值返回 SQLITE_OK0就是成功。LastError 在出错后取 sqlite3_errmsg 的指针这个指针指向 SQLite 内部静态缓冲区下一次调用其他 API 后可能失效所以用完立即拷贝或者直接输出。参数说明sqlite3_open 第二个参数是 sqlite3** 输出参数函数内部会分配 sqlite3 结构体。注意如果打开失败db_ 不一定是 NULL可能是部分初始化的句柄此时仍然需要调用 sqlite3_close 释放再处理错误。VC 程序里常见的错误是只判断返回值不等于 SQLITE_OK 就直接退出结果句柄泄漏程序反复开关数据库时会越来越慢。3.2 错误码与错误处理不要只打印一个数字sqlite3_open 的返回值可能很多种SQLITE_OK0、SQLITE_CANTOPEN14打不开文件、SQLITE_NOTADB26文件不是 SQLite 格式、SQLITE_BUSY5数据库被锁定。只看错误码数字不利于排查我一般写一个 ErrorCodeToString 辅助函数把常见错误码映射为人类能读懂的描述串。const char* SqliteErrText(int code) { switch (code) { case SQLITE_OK: return ok; case SQLITE_ERROR: return sql error or missing database; case SQLITE_PERM: return permission denied; case SQLITE_BUSY: return database is locked; case SQLITE_LOCKED: return table is locked; case SQLITE_READONLY: return attempt to write a readonly database; case SQLITE_CANTOPEN: return unable to open database file; case SQLITE_NOTADB: return file is not a database; default: return unknown; } }这个函数配合 sqlite3_errmsg 使用能在日志里同时记录错误码和文字描述。实际开发中我发现大部分数据库打不开其实是路径写错、权限不够、或者根本不是 SQLite 文件有了这张对照表排查速度快很多。3.3 回调风格的 sqlite3_exec什么时候用它sqlite3_exec 是Sugar API一句话执行多条 SQL用分号隔开每查出一条记录会回调你传进去的函数指针。很多 VC 新手一上来就只用 exec遇到查询结果全部往回调里塞结果回调函数一复杂代码立刻乱成一锅粥。我的经验是sqlite3_exec 只用来执行无返回结果的 DDL/DMLCREATE TABLE、INSERT、UPDATE、DELETE查询一律走 sqlite3_prepare_v2 sqlite3_step下一章详述。这样回调代码量最小也更容易统一错误处理。char* errmsg NULL; int rc sqlite3_exec(db, CREATE TABLE IF NOT EXISTS t_log(id INTEGER PRIMARY KEY, msg TEXT, ts INTEGER);, NULL, NULL, errmsg); if (rc ! SQLITE_OK errmsg) { // 这里 errmsg 是 sqlite 分配的用完必须 sqlite3_free OutputDebugStringA(errmsg); sqlite3_free(errmsg); }注意 errmsg 的释放方式必须用 sqlite3_free不能用 free 或 delete否则在 debug 模式下 CRT 会报堆校验错误。这个错很隐蔽表现可能是程序运行半天后突然崩溃因为堆已经被写坏了。4. 增删改查的完整写法prepare_v2 绑定参数杜绝拼接 SQL这一章是全文核心。VC 里操作 SQLite 做增删改查最关键的技巧就一句话动态 SQL 用 sqlite3_prepare_v2 bind 参数不要用 sprintf 拼字符串。原因有三拼 SQL 容易因转义不严导致 SQL 注入或执行失败比如值里有单引号 时直接断 SQL拼 SQL 导致 sqlite 每次都要重新解析语句性能差绑定参数让类型整数、文本、NULL由 API 统一处理C 端不用自己转字符串。4.1 插入与更新绑定参数的完整流程// db_demo.cpp #include stdio.h #include string #include sqlite_db.h int InsertLog(SqliteDb db, const char* msg, int level) { sqlite3* handle NULL; // 实际应从 SqliteDb 里拿这里示意 // 伪代码省略获取 handle 的过程直接看 SQL 与 bind 流程 const char* sql INSERT INTO t_log(msg, level, ts) VALUES(?, ?, ?);; sqlite3_stmt* stmt NULL; int rc sqlite3_prepare_v2(db_handle, sql, -1, stmt, NULL); if (rc ! SQLITE_OK) { printf(prepare failed: %s\n, sqlite3_errmsg(db_handle)); return rc; } // bind 参数从 1 开始计数 sqlite3_bind_text(stmt, 1, msg, -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 2, level); sqlite3_bind_int64(stmt, 3, (sqlite3_int64)time(NULL)); rc sqlite3_step(stmt); if (rc ! SQLITE_DONE) { printf(step failed: %s\n, sqlite3_errmsg(db_handle)); } sqlite3_finalize(stmt); return rc SQLITE_DONE ? SQLITE_OK : rc; }逻辑说明sqlite3_prepare_v2 将 SQL 语句编译为 stmtstatement 对象SQL 文本中的问号 ? 是占位符。sqlite3_bind_text 将第一个 ? 绑定为 C 字符串SQLITE_TRANSIENT 告诉 SQLite这个字符串可能在语句执行完之前就被释放SQLite 内部必须立刻复制一份副本。如果字符串生命周期能保证比如静态常量可以传 SQLITE_STATIC 省一次拷贝但必须有把握才用我一般不赌这个。sqlite3_step 执行插入返回 SQLITE_DONE 表示执行完毕。最后必须 sqlite3_finalize 释放 stmt否则每插入 1000 条左右就泄漏程序内存持续上涨。参数说明prepare 的第二个参数是 SQL 语句文本第三个参数 -1 表示让 SQLite 自己读取到字符串结束符 \0。第四个参数是输出 stmt第五个参数指向 SQL 语句中第一个未处理的字符位置一般传 NULL 就行。bind 函数的第二个参数是占位符索引必须从 1 开始不是 0这是新手最容易犯的错绑定到 0 会返回 SQLITE_RANGE。4.2 查询prepare step column 三件套查询的套路是 prepare 一次step 循环取每一行用 sqlite3_column_xxx 按列取数值。这里有个性能细节sqlite3_column_text 返回的指针指向内部缓冲当你调用下一个 step 后它就失效了所以要把值拷贝进 C 对象或者 std::string。struct LogRow { int64_t id; std::string msg; int level; int64_t ts; }; int QueryLogs(SqliteDb db, int level_filter, std::vectorLogRow out) { sqlite3* h db.GetHandle(); // 实际封装里要暴露 const char* sql SELECT id, msg, level, ts FROM t_log WHERE level ? ORDER BY ts DESC LIMIT 100;; sqlite3_stmt* stmt NULL; int rc sqlite3_prepare_v2(h, sql, -1, stmt, NULL); if (rc ! SQLITE_OK) return rc; sqlite3_bind_int(stmt, 1, level_filter); while ((rc sqlite3_step(stmt)) SQLITE_ROW) { LogRow r; r.id sqlite3_column_int64(stmt, 0); const unsigned char* txt sqlite3_column_text(stmt, 1); if (txt NULL) { r.msg ; } else { r.msg (const char*)txt; } r.level sqlite3_column_int(stmt, 2); r.ts sqlite3_column_int64(stmt, 3); out.push_back(r); } sqlite3_finalize(stmt); if (rc ! SQLITE_DONE) { // 不是 DONE 就是中途出错 return sqlite3_errcode(h); } return SQLITE_OK; }逻辑说明sqlite3_step 第一次返回 SQLITE_ROW 表示取到第一行继续调返回 SQLITE_ROW 取下一行取完返回 SQLITE_DONE 结束。所以在 while 循环里反复调 step每次拿到一行就立刻把整行数据拷进 LogRow。注意 msg 字段可能为 NULL即 sqlite3_column_text 返回 NULL此时需要处理为空字符串。查询结束必须 finalize即使只查了一部分结果也要先把 stmt 收掉再处理其他逻辑。4.3 删除与批量操作事务让速度提升 100 倍单个 INSERT 在自动提交模式下每条记录都要写一次事务日志1000 条插完大概要几百毫秒到几秒不等。显式开启事务后1000 条插入通常几十毫秒就结束。VC 程序里经常有启动时初始化一批数据的场景务必用事务包住。int BatchInsert(SqliteDb db, const std::vectorstd::string items) { sqlite3* h db.GetHandle(); sqlite3_exec(h, BEGIN TRANSACTION;, NULL, NULL, NULL); int rc SQLITE_OK; const char* sql INSERT INTO t_log(msg) VALUES(?);; sqlite3_stmt* stmt NULL; rc sqlite3_prepare_v2(h, sql, -1, stmt, NULL); if (rc ! SQLITE_OK) { sqlite3_exec(h, ROLLBACK;, NULL, NULL, NULL); return rc; } for (size_t i 0; i items.size(); i) { sqlite3_bind_text(stmt, 1, items[i].c_str(), -1, SQLITE_TRANSIENT); rc sqlite3_step(stmt); if (rc ! SQLITE_DONE) break; sqlite3_reset(stmt); // 重要reset 之后才能重新 bind 和 step } sqlite3_finalize(stmt); if (rc SQLITE_DONE) { sqlite3_exec(h, COMMIT;, NULL, NULL, NULL); } else { sqlite3_exec(h, ROLLBACK;, NULL, NULL, NULL); } return rc; }逻辑说明BEGIN TRANSACTION 之后所有写操作进入同一个事务直到 COMMIT 或 ROLLBACK。sqlite3_prepare_v2 在事务外只做一次循环里重复 bind step每次 step 之后调用 sqlite3_reset 清空绑定的参数和状态让 stmt 可以复用。这个模式比每一步重新 prepare 快一个数量级。循环中任何一步失败就 ROLLBACK保证数据要么全部写入要么全部不写。参数说明sqlite3_reset 只重置语句状态不会重新解析 SQL也不会改变绑定的参数索引。如果你要改绑定的值直接在 reset 之后重新调用 sqlite3_bind_xxx 即可旧值会被覆盖。5. 避坑与常见问题排查我踩过的 5 个 VC SQLite 大坑这一章是血泪经验合集。VC 6.0 环境老、问题怪很多坑在 VS2015 上根本不会出现但在老工程里每一件都可能让你加班到深夜。下面每条按「现象 → 原因 → 解决」顺序写清楚。5.1 一百年不变的路径含中文打不开数据库现象程序在开发机上跑得好好的放到客户电脑上或者换一个中文目录就报 unable to open database file。实际上数据库文件根本没被打开而是 sqlite 自动新建了一个空的同名文件导致所有表格都提示 no such table。原因sqlite3_open 接收的是 UTF-8 编码路径。VC6 的 CString 默认 ANSIGBK中文字符在 GBK 和 UTF-8 之间字节不同SQLite 对不上就认为文件不存在于是成功创建了一个新库。解决写一个 ANSI 转 UTF-8 的辅助函数打开前转换路径编码。std::string AnsiToUtf8(const char* ansi) { // 先转 UTF-16再转 UTF-8 int wlen MultiByteToWideChar(CP_ACP, 0, ansi, -1, NULL, 0); wchar_t* wbuf new wchar_t[wlen]; MultiByteToWideChar(CP_ACP, 0, ansi, -1, wbuf, wlen); int ulen WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, NULL, 0, NULL, NULL); std::string out; out.resize(ulen); WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, out[0], ulen, NULL, NULL); delete[] wbuf; return out; }参数说明MultiByteToWideChar 第一个参数 CP_ACP 表示当前系统的 ANSI 代码页中文系统即 GBK。第一次调用传 NULL 输出缓冲得到所需缓冲区大小第二次真正转换。转 UTF-8 时同理CP_UTF8 是 UTF-8 代码页。注意 std::string::resize(ulen) 之后out[0] 不一定可写老版本 STL 里如果字符串为空out[0] 是未定义行为更稳妥的写法是先用 resize(ulen - 1) 再逐个字符拷贝或者干脆用 vector char 中转。VC6 的 STL 对空 string 取地址是历史知名坑谨慎为上。5.2 修改一行数据永远提示 database table is locked现象程序里开了好几个线程每个线程各自打开数据库同一个文件路径偶尔做 UPDATE 时返回 SQLITE_BUSYdatabase is locked。有时用 DB Browser 打开同一个库文件程序就完全动不了。原因SQLite 在默认 journal modeDELETE 模式下写事务会对数据库文件加排他锁。另一个连接如果已经持有读锁比如正在执行 SELECT写操作就得等如果两个连接同时写一个等另一个超过 busy_timeout 就报错。DB Browser for SQLite 这类 GUI 工具打开数据库后默认持有连接有时还有未关闭的事务程序自然被堵。解决统一使用同一个数据库连接推荐或者把 busy_timeout 调大并改用 WAL 模式。// 设置 busy_timeout 为 5 秒同时开启 WAL sqlite3_busy_timeout(db, 5000); sqlite3_exec(db, PRAGMA journal_modeWAL;, NULL, NULL, NULL);说明WAL 模式允许读操作和写操作并发写不阻塞读读不阻塞写只是写时会有 10%~20% 的性能开销。桌面应用场景数据量不大这个开销完全可接受。设置后 journal_mode 返回的结果如果是 wal说明正式生效了。5.3 数据库文件越用越大DELETE 之后空间不归还现象删了一大批数据文件大小几乎没变。如果频繁删改文件甚至涨到几 GB。原因SQLite 删除数据只是标记页为空闲不会把空间交还给操作系统。同时 SQLite 还有一个 freelist空闲页链表机制大量删除会产生碎片后续插入会优先复用这些页但不保证立刻压缩文件。解决显式执行 VACUUM 命令重建数据库文件。sqlite3_exec(db, VACUUM;, NULL, NULL, NULL);注意VACUUM 会重写整个数据库文件在文件很大的时候会耗时很久而且需要临时双倍磁盘空间。所以我的习惯是深夜维护任务里做一次 VACUUM或者删除操作集中在操作完成后统一做一次不在每次 DELETE 后都跑。5.4 sqlite3_column_text 返回的指针一 step 就失效现象查询结果里取出的字符串打印出来前几个字符是对的后面变成乱码或者第一次访问正常存到 vector 里再读就崩溃。原因sqlite3_column_text 返回的指针指向 sqlite3_stmt 内部存储区只有当前这一行有效。下一次 sqlite3_step 会修改内部缓冲区之前拿到的指针就悬空了。解决每行都立刻拷贝为 std::string不要保留指针。另外注意如果字段类型是 BLOBsqlite3_column_blob 返回的是二进制指针拷贝时要用 sqlite3_column_bytes 拿到字节长度不能用 strlen。5.5 在 VC6 下链接时报 LNK2001: unresolved external symbol sqlite3_open现象编译通过链接失败报找不到 sqlite3_open 等符号。原因头文件被当作 C 语法解析函数名被 name mangling链接器找的是 ?sqlite3_openYAH... 这种符号但 sqlite3.c 编出来的是 C 符号。解决按第 2 章的写法用 extern C 包住 sqlite3.h确保链接器按 C 符号解析。同时检查有没有把 sqlite3.c 漏加进工程。这两个事分开验证先确认文件加进去了再确认 extern C。6. 进阶技巧用回调函数把查询结果喂给界面 用 DB Browser 验证全链路最后一章讲两个收尾技巧一是 sqlite3_exec 的回调机制这是热搜词里出现频次很高的点二是用 DB Browser for SQLite 验证你的库结构、SQL 语句和数据正确性。这两个技巧能在调试阶段省下大量时间。6.1 sqlite3_exec 回调机制详解什么时候触发怎么用sqlite3_exec 内部其实是 prepare_v2 step column 的封装。当你执行 SELECT 语句时每查到一行SQLite 就调用你传给 exec 的第三个参数回调函数指针并把列数、列值数组、列名数组传进去。回调返回 0 表示继续查下一行返回非 0 则中断查询。// 回调函数接收一行查询结果 static int RowCallback(void* user_data, int col_count, char** col_values, char** col_names) { std::vectorstd::string* rows static_caststd::vectorstd::string*(user_data); std::string line; for (int i 0; i col_count; i) { line col_names[i]; line ; line (col_values[i] ? col_values[i] : NULL); line ; ; } rows-push_back(line); return 0; // 返回 0 继续返回非 0 停止查询 } // 调用方式 std::vectorstd::string result; char* errmsg NULL; int rc sqlite3_exec(db, SELECT * FROM t_log LIMIT 10;, RowCallback, result, errmsg);逻辑说明user_data 是你在 exec 第四个参数里传的自定义指针回调里能拿到它这是把查询到的行收集到 C 容器里的标准通道。col_values 是每个列的文本表示SQLite 把所有值转成字符串如果某列为 NULL对应指针为 NULL必须判空再使用。col_names 是列名的字符串数组。callback 返回 0 继续迭代返回 1 立即终止。对于只关心有没有查到数据、不关心具体列值的场景回调里直接 return 1也能通过 rc SQLITE_OK 判断查询是否成功但这种写法我一般不用因为不取结果没意义。这个回调适合什么场景呢我的判断是查询结果很简单、不需要精细控制类型时用它一行搞定。但如果涉及 WHERE 参数、类型转换、大量数据分页还是 prepare_v2 bind column 三件套更可控。回调会在 sqlite3_exec 的调用线程中同步执行不会自动跑到其他线程VC 界面代码要小心不要在回调里直接操作窗口控件因为回调执行期间可能阻塞 UI 线程的消息循环。正确做法是回调里只收集数据到缓冲区等 exec 返回后再统一更新界面。6.2 用 DB Browser 验证库结构和 SQL 的正确性DB Browser for SQLite 是免费的跨平台 SQLite 图形管理工具。我们在 VC 里写 SQL 常范的错有三个表格名打错、列名不匹配、SQL 语法和 sqlite 方言不兼容比如用了 MySQL 的 AUTO_INCREMENTSQLite 里对应的是 INTEGER PRIMARY KEY AUTOINCREMENT。与其在 C 里反复编译调试不如先用 DB Browser 把库结构和 SQL 过一遍。具体做法是程序跑第一遍之后用 DB Browser 打开生成的 .db 文件在数据库结构页签里看表格、索引、列类型是否和预期一致在执行 SQL页签里手动跑一遍你准备在代码里执行的语句确认语法正确、结果集符合预期。还有一招是直接把你代码里的 SQL 字符串复制到 DB Browser 的 SQL 控制台跑一遍变更后在写入变更里保存。这样能隔离SQL 本身有问题和C 调用方式有问题两种故障源。另外我习惯在 VC 工程里加一个模块自检函数每次程序启动或按 F5 调试时查询 sqlite_master 表打印所有用户表名确认数据库确实建好了void DebugDumpTables(sqlite3* db) { const char* sql SELECT name FROM sqlite_master WHERE typetable ORDER BY name;; sqlite3_stmt* stmt NULL; if (sqlite3_prepare_v2(db, sql, -1, stmt, NULL) SQLITE_OK) { while (sqlite3_step(stmt) SQLITE_ROW) { const char* name (const char*)sqlite3_column_text(stmt, 0); OutputDebugStringA(name); OutputDebugStringA(\n); } } sqlite3_finalize(stmt); }这段代码在调试期很有用能从侧边验证数据库文件路径、表格创建是否成功。从那以后我每次在 VC 里集成 sqlite都强制走一遍这个流程写好建表 SQL 先在 DB Browser 里跑通再把 SQL 复制进 C 代码最后用 sqlite_master 查询做启动自检。这一套下来90% 的 SQLite 接入问题都填平了。希望帮到你。本文还有配套的精品资源点击获取