
简介这份 SQLite3 VC6.0 资源面向仍在 Visual C 6.0 环境下开发或维护旧项目的开发者提供从官方源码编译得到的 Windows 平台 SQLite3 库及相关工程材料可用于数据库引擎学习、源码查阅、自定义编译与现有应用调试。压缩包为 7z 格式体积约 5.4MB文件总数与类型明细上游暂未提供从描述可知其中包含 VC6.0 工作空间、工程配置、SQLite3 文档、静态库/动态库测试示例以及编译产物便于直接链接或二次构建。已有 228 人浏览学习。对于需要理解 SQLite3 内部实现、验证 API 用法或在老环境中排查链接问题的团队这份材料能提供可运行的工程入口、测试参考和文档线索并保留源码级研究空间适合中级开发者按需取用。1. 老工程要接 sqlite3VC6.0 这道坎得先迈过去去年接手一个维护了十几年的 MFC 老程序需求很朴素把本地的配置、流水日志和几张业务小表落盘装个数据库服务不现实评估下来 sqlite3 是唯一合理的选择——单文件、无服务端、静态链接进去就能跑。麻烦在于开发环境还是 VC6.0编译器停在 C89 那一代而手上的 sqlite3 amalgamation 是拿新编译器喂出来的。把 sqlite3.c 拖进工程点一下编译从第一个头文件开始一路飘红这是必然的不是运气不好。这篇笔记就按实际动手的顺序拆文件怎么摆、编译报错怎么改、接口怎么封、中文怎么塞进去、哪些地方最容易加班适合手里有老工程又不想换工具链的人。2. 拆包amalgamation 的三个文件哪个能进 VC6 工程先别急着建工程。从 sqlite3 官方渠道拿到的 amalgamation 版本解压出来通常就那么几个文件但每个的用途天差地别塞错一个后面全是无用功。2.1 sqlite3.c、sqlite3.h、sqlite3ext.h 各管什么文件作用要不要进工程sqlite3.c内核全部实现压成的一个编译单元要且只要这一个 .csqlite3.h公共 API 声明自带extern C守卫要业务工程只引这个sqlite3ext.h给可加载扩展模块用的虚表头不要普通增删改查用不上shell.c命令行工具内含main不要会和你的main打架sqlite3.c 是把几百个源文件合并出来的单编译单元好处是没有头文件依赖地狱坏处是它一个文件就顶别人一个模块的量。VC6 的编译器在处理这种体量的单一编译单元时本身就容易出状况这一点第 3 章会细说。sqlite3ext.h 是扩展机制的头只有在写虚表扩展、做自定义函数模块时才需要普通业务代码引了反而多一层编译负担。shell.c 是那个能敲.tables的命令行程序它有自己的main一旦混进静态库里链接阶段就会报重复定义这是新手最常见的一个开局失误。我的习惯是建一个独立的静态库工程专门编 sqlite3.c业务工程只引头文件和 .lib。这么做有两个好处一是编译 sqlite3.c 那几分钟只在库工程里发生业务代码改动不用重编它二是宏定义线程模型、裁剪开关集中在一个工程的设置里不会出现「主工程改了宏但库没重编」这种查半天的问题。2.2 在 VC6 里建静态库工程目录和设置目录建议这样摆业务工程和第三方库彻底分开project_root/ ├── app/ 业务工程MFC 或 Console │ └── app.dsp ├── third_party/ │ └── sqlite3/ │ ├── sqlite3.c │ ├── sqlite3.h │ ├── sqlite3lib.dsp │ └── lib/ - 库工程输出目录 └── data/ 运行期生成的 .db 文件建工程的步骤按这个顺序走中间别跳File → New → Projects → Win32 Static Library工程名 sqlite3lib路径指到third_party/sqlite3/。把 sqlite3.c 加入工程sqlite3.h 不用加头文件不需要编译。Project → Settings → C/C → Precompiled Headers选Not using precompiled headers。这一步必须做否则 VC6 会去找 stdafx.h直接 C1083。同一个设置页的 Code GenerationRuntime Library 选和业务工程完全一致的那一项。业务工程是 Debug 就用 Debug Multithreaded DLL是 Release 就用 Multithreaded DLL。两边不一致链接期必炸这是第 5 章要展开的坑。Project → Settings → Link → Output file name改成lib/sqlite3_d.lib之类把输出统一收进 lib 目录。在业务工程里Project → Settings → C/C → Preprocessor → Additional include directories 加上..\third_party\sqlite3Link 页的 Object/library modules 里加上..\third_party\sqlite3\lib\sqlite3_d.lib。第 4 步那个运行库一致的要求很多人觉得是老生常谈但在 VC6 上它真的会以LNK2005: _malloc already defined这种面目出现而且报错指向的是一堆 CRT 内部符号完全看不出是自己库的设置问题。2.3 编译开关哪些宏现在就该定下来在库工程的 Preprocessor definitions 里加下面这几个一次定好后面少折腾宏取值影响什么时候别这么设SQLITE_THREADSAFE0去掉互斥锁相关代码体积和调用开销都降有多个线程访问同一个sqlite3*连接时必须设 1SQLITE_OMIT_LOAD_EXTENSION定义即可去掉动态加载扩展的能力需要.load扩展时保留SQLITE_OMIT_DEPRECATED定义即可去掉一批老接口老代码里还在用sqlite3_exec之外的废弃 API 就别加SQLITE_ENABLE_COLUMN_METADATA定义即可能用sqlite3_column_table_name那组函数不需要从结果集反查表名就别开会占体积SQLITE_TEMP_STORE2临时表优先放内存临时数据量大到吃内存的场景要往下调SQLITE_DEFAULT_MEMSTATUS0关掉内存统计省一点全局开销需要调sqlite3_memory_used做诊断时保留SQLITE_THREADSAFE0是最值得先想清楚的一个。VC6 时代的老程序很多是单线程界面线程 一个工作线程的结构如果只有工作线程碰数据库设 0 完全没问题但只要有任何一个界面回调里也去查了一下表就必须设 1否则出问题是概率事件而且崩的位置和原因对不上排查成本极高。改完宏一定要Build → Rebuild All。VC6 的依赖跟踪非常弱头文件和宏的改动经常不触发重编你以为改了其实没改这种「黑匣子」式的困惑我踩过不止一次。3. 编译适配VC6 编译器啃 sqlite3.c 的六处改动工程建好之后点编译才是真正开始干活的地方。这一章的报错都是我在 VC6 上按这个顺序遭遇的按顺序改能省时间。3.1 先认清 VC6 的 C 语言边界VC6 自带编译器大致是 C89 加上一部分微软扩展它不具备 C99 的这些东西没有long long64 位整型只能写__int64没有stdint.h、inttypes.h、stdbool.h这三个标准头没有inline关键字C 模式下要用__inline不支持可变参数宏#define F(...) __VA_ARGS__变量声明仍然必须在语句块开头C99 那种「用到再声明」不行。而 amalgamation 版本是跟着主流编译器走的近些年的版本里已经默认假定stdint.h存在、long long可用。所以第一次编译的报错几乎必然集中在头文件和整型上。一个提醒VC6 其实是支持//行注释的这一点和很多人的印象相反所以不要看到报错就先去批量替换注释风格那是白费功夫。3.2 类型与头文件的替换清单先在 sqlite3.c 文件最开头第一个#include之前插一段兼容定义/* sqlite3_vc6_compat.h 的内容直接贴进 sqlite3.c 顶部 */ #if defined(_MSC_VER) (_MSC_VER 1300) /* VC6 的 _MSC_VER 是 1200 */ typedef signed char int8_t; typedef unsigned char uint8_t; typedef short int16_t; typedef unsigned short uint16_t; typedef int int32_t; typedef unsigned int uint32_t; typedef __int64 int64_t; typedef unsigned __int64 uint64_t; #define SQLITE_VC6_BUILD 1 #endif这段的作用就是把 C99 那套定长整型名字补上让后面的代码能用int64_t这种写法。__int64是微软自己的 64 位整型写法VC6 认它不认long long两者在内存布局上是一回事。然后是全文件的批量替换一共三类long long→__int64unsigned long long→unsigned __int64#include stdint.h、#include inttypes.h这两行注释掉兼容定义已经放在前面了。替换用编辑器的全局替换功能注意勾选「区分大小写」和「全字匹配」否则会把long long_var这种变量名误伤。还有一种情况是inline报错。如果看到C2054: expected ( to follow inline说明这个函数在 C 模式下被声明成了inline。改法不是一个个替换而是在文件顶部加一行#if defined(_MSC_VER) (_MSC_VER 1300) #define inline __inline #endif放在所有代码之前一行就覆盖全文件。3.3 编译错误逐条对照改完上面这些再编剩下的报错会少很多剩下的一般是这几种报错原文真实原因处理方式fatal error C1083: Cannot open include file: stdint.h该版本 C 标准头不存在注释掉该#include用 3.2 的兼容块error C2632: long followed by long is illegal用了long long换成__int64error C2065: int64_t : undeclared identifier兼容块没放对位置确认它在第一个#include之前error C2054: expected ( to follow inlineC 模式下的inline顶部加#define inline __inlinefatal error C1002: compiler is out of heap space in pass 2单编译单元太大VC6 编译器自己先撑不住见下面的处理顺序fatal error C1060: compiler is out of heap space同上另一种表现形式同上C1002 / C1060 这两个不是代码写错了是 32 位编译器处理超大翻译单元时的资源问题。我一般按这个顺序试Project → Settings → C/C → Generate browse info 取消勾选。这个选项会生成 .bsc 浏览数据库非常吃内存老工程里通常默认开着。Link 页里的 Generate debug info 也收一收Release 下本来就该关。优化级别从 /O2 降到 /O1或者干脆 Debug 配置下关优化先编通。确保工程不在网络盘或很慢的磁盘上VC6 编译中间文件时对 IO 很敏感。这四条走完绝大多数机器都能编过。真正的血泪经验是第 1 条——当年为了一个 C1002 折腾了半天最后发现只是浏览器信息生成开着。编过之后先别急着写业务代码写个最小的main验证一下链接#include sqlite3.h #include stdio.h int main() { /* 只取版本号不建库不建表验证符号能链上 */ printf(sqlite version: %s\n, sqlite3_libversion()); return 0; }能打印出版本字符串说明头文件路径、静态库路径、运行库设置三件事都是对的可以往下走了。如果这一步就报LNK2001: unresolved external symbol _sqlite3_libversion问题一定在链接配置上不要怀疑代码。4. 接口封装打开、查询、事务的最小可用层sqlite3 的 C 接口本身不难难的是在老工程里用得稳。这一层我一般只封四个函数打开、执行语句、参数化写入、参数化查询够覆盖九成场景再往上封 ORM 在老项目里是给自己找麻烦。4.1 打开数据库与 PRAGMA 基线#include sqlite3.h #include stdio.h /* 执行一条不需要结果的语句顺带把错误吃掉并打印 */ static void ExecSilent(sqlite3* db, const char* sql) { char* err NULL; if (sqlite3_exec(db, sql, NULL, NULL, err) ! SQLITE_OK err ! NULL) { printf([pragma] %s failed: %s\n, sql, err); sqlite3_free(err); /* 必须用 sqlite3_free不能用 free */ } } sqlite3* OpenDb(const char* file) { sqlite3* db NULL; /* 第三个参数不需要sqlite3_open 只认两个参数 */ if (sqlite3_open(file, db) ! SQLITE_OK) { if (db ! NULL) { sqlite3_close(db); /* open 失败也可能分配了句柄必须关 */ } return NULL; } /* 拿不到锁时最多重试 3000 毫秒而不是立刻返回 SQLITE_BUSY */ sqlite3_busy_timeout(db, 3000); ExecSilent(db, PRAGMA synchronous NORMAL;); ExecSilent(db, PRAGMA cache_size 2000;); ExecSilent(db, PRAGMA temp_store MEMORY;); return db; }逻辑上只有三件事打开、设超时、设参数。sqlite3_open失败时句柄不一定是空的很多人只写return NULL不关句柄跑久了句柄泄漏最后表现成「第一次能开第二次打不开数据库」查起来很绕。参数说明busy_timeout的单位是毫秒。设成 0 意味着只要锁被占用就立刻返回SQLITE_BUSY业务代码必须自己处理重试设成 3000 相当于把重试逻辑交给 sqlite3 内部。单进程单连接场景基本不会撞锁但只要有第二个进程比如用命令行工具开着看数据就会有。synchronous NORMAL是持久性和速度的折中。默认 FULL 每次提交都强制落盘小数据量业务感觉不明显批量写的时候差距就出来了。如果这批数据丢了不可接受比如财务流水老老实实留 FULL。cache_size的单位是页2000 页按默认页大小算大概是几 MB 量级老机器上不要设太大MFC 程序本身内存占用就不低。temp_store MEMORY让临时表和排序中间结果放内存。数据量小的时候能明显减少磁盘临时文件数据量大的时候反而可能吃爆内存按实际数据规模取舍。4.2 prepare / bind / step 的四步封装带参数写入是最常用的也最容易写错。标准写法是 prepare 一次、循环 bind/step/reset/* 批量插入SQL 只 prepare 一次循环复用 */ int InsertUsers(sqlite3* db, const char* const* names, const int* ages, int n) { sqlite3_stmt* stmt NULL; const char* sql INSERT INTO t_user(name, age) VALUES(?, ?); int i, rc; /* 参数 -1 表示让 sqlite3 自己数 SQL 字符串长度 */ rc sqlite3_prepare(db, sql, -1, stmt, NULL); if (rc ! SQLITE_OK) { return rc; } for (i 0; i n; i) { /* 第 5 个参数 SQLITE_TRANSIENT 表示让 sqlite 复制一份字符串 */ sqlite3_bind_text(stmt, 1, names[i], -1, SQLITE_TRANSIENT); sqlite3_bind_int (stmt, 2, ages[i]); rc sqlite3_step(stmt); if (rc ! SQLITE_DONE) { sqlite3_finalize(stmt); return rc; } sqlite3_reset(stmt); /* 复位到可再次绑定的状态 */ sqlite3_clear_bindings(stmt); /* 清掉上一轮的值避免残留 */ } sqlite3_finalize(stmt); /* 一定要 finalize否则语句对象泄漏 */ return SQLITE_OK; }几个关键点必须说清楚sqlite3_bind_text的第 5 个参数是最容易翻车的地方。传SQLITE_TRANSIENTsqlite 会把字符串复制一份存到自己管理的内存里你后面怎么改原缓冲区都不影响传SQLITE_STATIC它只记个指针要求这块内存在step完成之前一直有效。如果你的names[i]来自一个循环里的局部char buf[64]用SQLITE_STATIC就会在下一轮循环被覆盖写进库的数据看起来「随机错乱」而且因为地址没变编译器也不会报任何东西。这种问题我见过不止一次统一用SQLITE_TRANSIENT是最省心的选择多一次拷贝的代价在这种老业务里根本不值一提。sqlite3_reset和sqlite3_clear_bindings的作用不一样。reset把语句恢复到刚 prepare 完、可以重新 bind 的状态但已经绑定的值还在clear_bindings才是清空。只 reset 不清 bind如果某一轮某个参数忘了绑它会沿用上一轮的值表现成「数据串行」——这属于典型的玄学 bug因为代码看起来一点问题都没有。sqlite3_finalize必须调用即使是出错提前返回的分支。stmt 是有状态的资源不释放的话语句对象会一直挂着最终耗尽内存。查询那边同理sqlite3_step返回SQLITE_ROW表示有一行数据SQLITE_DONE表示结束了。取列的时候要注意一件事sqlite3_column_text返回的指针在reset或finalize之后就会失效如果业务层要长期保存这个值必须自己strcpy到自己的缓冲区里不能只存指针。4.3 事务与批量写入的吞吐差批量写不包事务性能会难看到让人怀疑 sqlite3 是不是有问题。原因是每一条INSERT都独立提交一次每次提交都要走一次同步落盘动作行数越多落盘次数越多。int ImportBatch(sqlite3* db, const char* const* names, const int* ages, int n) { int rc; /* BEGIN IMMEDIATE 立刻拿写锁避免中途升级锁失败 */ rc sqlite3_exec(db, BEGIN IMMEDIATE;, NULL, NULL, NULL); if (rc ! SQLITE_OK) { return rc; } rc InsertUsers(db, names, ages, n); if (rc SQLITE_OK) { rc sqlite3_exec(db, COMMIT;, NULL, NULL, NULL); } else { /* 任何一步失败都回滚别留半截数据 */ sqlite3_exec(db, ROLLBACK;, NULL, NULL, NULL); } return rc; }事务包好之后几万行数据的感觉完全是另一个量级因为整批数据只有一次提交、一次落盘。BEGIN有三种形式BEGIN、BEGIN IMMEDIATE、BEGIN EXCLUSIVE区别在于拿锁的时机。默认的BEGIN是延迟拿锁直到第一条写语句才升级成写锁多进程场景下可能在升级时失败IMMEDIATE一开始就拿写锁失败得早、也更好处理。批量导入这种一上来就要写全部数据的场景用IMMEDIATE更合适。还有一个细节事务没提交就关闭连接数据会丢。这不是 sqlite3 的问题是事务语义本身如此。老代码里如果在异常分支里直接sqlite3_close而不 ROLLBACK看起来是回滚了未提交的东西本来就不生效但如果逻辑写在循环里、中途 return 了前面的数据也没提交成功表现成「明明跑了但数据没进去」。4.4 中文与编码的处理sqlite3 内部一律 UTF-8。VC6 的工程默认是 GBKWindows 代码页 936直接把 GBK 字符串bind_text进去读出来还能对得上因为存进去和取出来用的是同一套错误编码看起来没问题。真正的麻烦出现在三种情况用别的工具打开这个库、库里混了别的来源的数据、或者要做全文比对。这时候就会出现典型的乱码。稳妥的做法是在写入前把 GBK 转成 UTF-8读取后再转回来。VC6 上有MultiByteToWideChar/WideCharToMultiByte走 UTF-16 中转/* 代码页936 GBK65001 UTF-8 */ static void Convert(const char* from, char* to, int toSize, UINT fromCp, UINT toCp) { /* 先转宽字符再转目标编码VC6 没有直接的 GBK-UTF8 函数 */ WCHAR wbuf[1024]; int wlen MultiByteToWideChar(fromCp, 0, from, -1, wbuf, 1024); if (wlen 0) { WideCharToMultiByte(toCp, 0, wbuf, -1, to, toSize, NULL, NULL); } }调用时机很明确所有sqlite3_bind_text之前调一次 GBK→UTF-8所有sqlite3_column_text之后调一次 UTF-8→GBK。库里的数据始终是 UTF-8对外接口始终是 GBK中间这层转换是固定的不要在不同地方混着写否则过几个月自己都分不清哪个字段是什么编码。注意MultiByteToWideChar的缓冲区长度要按最坏情况预估。GBK 中文是两个字节UTF-8 中文通常是三个转换后长度会变长缓冲区不够会截断截断点还可能落在多字节字符中间产生半个字符。5. 避坑排查VC6 接 sqlite3 最常翻车的四个现场这一章按我实际遇到的顺序排每个都写清楚现象、原因和解决遇到对得上的直接照做。5.1 编译期改完了还是报旧错误现象按第 3 章把long long全替换了重新编译还是报C2632: long followed by long is illegal而且报错行号指向的代码看着已经是__int64了。原因VC6 的增量编译依赖跟踪非常不可靠尤其是对宏定义和头文件的改动。它经常拿旧的 .obj 直接链接你在编辑器里改的内容根本没进编译流程。另一个可能是工程里存在多份 sqlite3.c 的副本你改的是 A实际编译的是 B。解决Build → Rebuild All无条件全量重编一次。如果还报用 Windows 搜索确认工程目录下有几个 sqlite3.c把多余的删掉或从工程里移除。这件事之后我养成了一个习惯任何涉及宏和头文件的改动一律 Rebuild All不再相信增量编译。5.2 链接期LNK2005 与 unresolved external symbol现象库编出来了业务工程链的时候报一堆LNK2005: _malloc already defined in LIBCMTD.lib或者LNK2001: unresolved external symbol _sqlite3_open。原因两种错误根因不同。LNK2005是运行库版本不一致库工程用了静态 CRT/MT 系列业务工程用了动态 CRT/MD 系列两边都把 CRT 的符号定义了一遍链接器不知道该用哪个。LNK2001通常是 .lib 路径写错、或者压根没加进 Link 页符号根本没找到。解决先看两边工程 Project → Settings → C/C → Code Generation 里的 Runtime Library 是不是同一项不一致就改成一致。然后 Clean 掉两边的中间文件重新全编。LNK2001的话在 Link 页确认 Object/library modules 里有那个 .lib 的完整相对路径路径里不要有空格和中文。还有一种隐蔽情况业务工程是 Unicode 配置、库工程是 ANSI这种不会直接报链接错但字符串传进去会是怪值属于后面第 5.4 条的范围。5.3 运行期database is locked 反复出现现象程序跑一段时间后写操作开始返回SQLITE_BUSY错误信息是database is locked重启程序又好了。原因常见有三种。一是没设busy_timeout撞上锁直接失败二是有一段代码开事务后没提交也没回滚直接把连接留着写锁一直不释放三是有另一个进程比如你开着命令行工具在看数据持有锁。解决首先在OpenDb里加sqlite3_busy_timeout给一个合理的重试窗口。然后全局搜一遍BEGIN确认每个BEGIN都有配对的COMMIT或ROLLBACK重点是异常分支——很多老代码在 catch 块里只打了日志就返回了事务挂在那里。最后确认有没有第二个进程在访问同一个 .db 文件开发期这种情况特别常见。一个排查技巧把sqlite3_errcode和sqlite3_errmsg的返回值在出错时打出来SQLITE_BUSY和SQLITE_LOCKED是两回事前者是文件锁冲突后者是同一连接内部的语句冲突处理方式不一样。只打一句「数据库错误」等于没打。5.4 数据层中文乱码与 64 位整型的格式化现象中文写进去用别的工具打开是乱码或者 64 位 ID 打出来是负数、是 0、或者是一串完全不对的数字。原因中文是编码问题见 4.4。整型的问题出在格式化字符串上——VC6 的 CRT 不认%lld而sqlite3_column_int64返回的是__int64用%lld去格式化结果是未定义的常见表现就是打出 0 或者负数。解决中文统一走 4.4 那层转换不要图省事。整型格式化用微软自己的写法%I64dsqlite3_int64 id sqlite3_column_int64(stmt, 0); char buf[32]; _stprintf(buf, _T(%I64d), id); /* VC6 上用 %I64d不是 %lld */如果是写日志printf系列同理%I64d也能用在sprintf上。这一条看起来很小但 64 位 ID 在业务表里到处都是用错了会让整个排查方向跑偏——你会以为是数据写错了其实是打印错了。6. 交付前的自检一个三十行的冒烟程序库编通了、接口封好了、数据也能读了最后一步是给自己留一个能在任何机器上快速判断「这个 .db 文件是不是好的」的小工具。老项目交付现场经常没法装调试器能跑一个控制台程序把问题定位到「文件坏了」还是「代码坏了」能省掉大量来回沟通。6.1 冒烟自检程序#include sqlite3.h #include stdio.h /* exec 的回调每行结果调一次这里只打印第一列 */ static int RowCb(void* unused, int argc, char** argv, char** colName) { if (argc 0 argv[0] ! NULL) { printf( %s\n, argv[0]); } return 0; /* 返回非 0 会中断查询 */ } int SmokeTest(const char* file) { sqlite3* db NULL; char* err NULL; int rc; if (sqlite3_open(file, db) ! SQLITE_OK) { printf(open failed: %s\n, sqlite3_errmsg(db)); if (db) sqlite3_close(db); return -1; } /* 第一关文件结构完整性检查正常只会输出一行 ok */ printf([1] integrity_check:\n); rc sqlite3_exec(db, PRAGMA integrity_check;, RowCb, NULL, err); if (rc ! SQLITE_OK) { printf( failed: %s\n, err ? err : unknown); sqlite3_free(err); sqlite3_close(db); return -2; } /* 第二关列出所有表确认 schema 符合预期 */ printf([2] tables:\n); sqlite3_exec(db, SELECT name FROM sqlite_master WHERE typetable ORDER BY name;, RowCb, NULL, err); /* 第三关统计一张关键业务表的行数确认数据真的在 */ printf([3] row count:\n); sqlite3_exec(db, SELECT COUNT(*) FROM t_user;, RowCb, NULL, err); sqlite3_close(db); return 0; }三关的定位能力是递进的。第一关PRAGMA integrity_check检查的是文件本身如果它返回的不是ok说明这个 .db 文件至少在页结构层面有问题后面的代码怎么改都没用得先想办法恢复数据。第二关查sqlite_master能看到所有表和索引如果客户现场说「表不存在」跑一下就知道是库连错了路径还是真没建表。第三关查行数确认数据在不在避免把「查不到」和「没写进去」混为一谈。回调函数返回非 0 会中断当前查询这一点在只想看前几行的时候有用但本文还有配套的精品资源点击获取