简介:这份资源面向在 C++ 环境下进行数据库开发的程序员与学习者,聚焦微软 ADODB(ActiveX Data Objects for Database)这一 COM 接口的实战应用。包内以 ADODatabase.h、ADORecordset.h 等头文件与对应 cpp 源码为核心,演示了 Connection 建立连接、Command 执行 SQL、Recordset 遍历结果集等对象模型用法,并涉及游标类型、记录锁定、字段操作、错误处理与连接池等要点,同时包含 regtlibv12 注册 ADODB 类型库的相关内容。资源共 79 个文件,涵盖 h、cpp 源码,obj、tlog、pdb 等编译中间产物,以及 dll、lib、exp、tlh、tli 等库与类型库文件,另有 sln、vcxproj 工程配置和 rc、manifest 等资源文件,压缩包约 38.43MB。目前已有 110 人学习。读者可借助这套基础框架理解 C++ 调用 ADODB 的完整流程,掌握连接管理、SQL 执行与结果集处理的核心操作,为数据库编程能力提升提供可参考的代码范例。
1. 从 AdoDB.rar 说起:C++ 里那套被低估的数据库访问层
如果你手头有一个叫AdoDB.rar的压缩包,里面躺着ADODB c++相关的源码,还夹着一个regtlibv12的注册脚本,那你大概率正面对一个典型的 Windows 原生 C++ 数据库访问场景:用 COM 组件去连 SQL Server、Access 或者 Oracle,而不是走 ODBC 那套 C 接口。ADODB 本身是微软数据访问体系里最上层的一环,它把 OLE DB 的复杂度包了一层,让你在 C++ 里用类似脚本的方式操作 Recordset。问题在于,C++ 没有脚本语言那种运行时绑定,你得靠#import生成 tlb 包装类,或者手动处理IDispatch,而regtlibv12就是用来注册类型库的。这套东西在 VS2010 到 VS2015 时代是主流,现在虽然被 ORM 和现代连接库挤到角落,但在维护老系统、对接遗留数据库、或者需要极致控制 COM 生命周期的场景里,它依然能打。这篇文章不聊虚的,就围绕这个压缩包背后的技术栈,把环境配置、类型库注册、连接字符串、Recordset 遍历、事务处理和那几个必踩的坑,一条条拆开讲清楚。适合谁看?手上有一堆遗留 C++ 代码要连数据库的维护者,或者想搞明白 COM 时代数据访问到底怎么跑起来的工程师。
2. 把 ADODB 跑起来:从 regtlibv12 注册到第一个 Connection 对象
2.1 为什么是 ADODB 而不是直接上 ODBC
在 Windows 上让 C++ 连数据库,路子其实不少:ODBC API、OLE DB、ADO、还有后来的各种原生客户端。ADODB 的定位很特殊,它不直接跟数据库驱动打交道,而是坐在 OLE DB 上面,通过 COM 接口暴露一套对象模型。你拿到的是Connection、Command、Recordset、Field这些对象,操作方式跟 VBScript 里几乎一样,但底层还是 COM 调用。
选它的理由通常有三个。第一,代码量少。用 ODBC 你得SQLAllocHandle、SQLBindCol、SQLFetch一路写下来,ADODB 里一个Open加一个while (!rs->EndOfFile)就完事。第二,数据库无关性做得比较自然。换数据库主要改连接字符串,Recordset 的遍历逻辑基本不动。第三,跟遗留系统兼容。很多老系统的数据访问层就是 ADODB 写的,你新写的模块如果走别的路子,事务和连接池对不上,反而更麻烦。
但代价也很明显:COM 的引用计数、_com_ptr_t的智能指针行为、VARIANT的封送处理,这些在脚本里被隐藏的东西,在 C++ 里全得自己管。regtlibv12就是第一道门槛。
2.2 regtlibv12 到底在干什么,为什么非跑不可
regtlibv12是 Visual C++ 附带的一个工具,全称是 Register Type Library,版本号对应 VC 的某个发行版。它的作用是把一个.tlb或者.dll里内嵌的类型库信息写进注册表,让 COM 运行时能通过CLSID和IID找到对应的接口定义。
你如果用#import指令引入 ADODB,编译器会尝试从注册表里找msado15.dll的类型库。如果没注册,或者注册的版本不对,就会报C1083或者LNK2019,说找不到_com_ptr_t相关的符号。这时候regtlibv12就是解药。
常见做法是在命令行里跑:
regtlibv12 "C:\Program Files\Common Files\System\ado\msado15.dll"注意路径要按实际系统来,32 位和 64 位系统的 ADO 组件位置不一样。跑完之后,注册表里HKEY_CLASSES_ROOT\TypeLib下面会出现对应的条目。你可以用oleview或者直接看注册表确认。
提示:如果系统是 64 位但你的程序编译成 32 位,必须用 32 位的
regtlibv12去注册 32 位的msado15.dll,否则#import还是会失败。这个坑我见过太多次,症状是编译过了但运行时报CoCreateInstance返回REGDB_E_CLASSNOTREG。
2.3 用 #import 生成包装类,还是手写 IDispatch
#import是 C++ 里用 ADODB 最省事的方式。它会在编译时读取类型库,生成.tlh和.tli两个文件,里面是智能指针包装类。你写代码的时候直接用ADODB::_ConnectionPtr、ADODB::_RecordsetPtr就行。
#import "C:\Program Files\Common Files\System\ado\msado15.dll" \ no_namespace rename("EOF", "EndOfFile")这里no_namespace是防止命名空间污染,rename("EOF", "EndOfFile")是因为EOF在 C++ 里可能跟其他宏冲突。生成的包装类会自动处理AddRef和Release,比手动调IDispatch::Invoke舒服太多。
但#import有个副作用:它依赖注册表里的类型库路径。如果你把程序拷到另一台机器,而那台机器没注册对应的 ADO 版本,编译期就会出问题。所以发布的时候要么带上msado15.dll并注册,要么改用CoCreateInstance加手动QueryInterface,后者代码量大但部署干净。
我一般会在项目里保留一个ado_import.h,把#import和必要的using声明放进去,这样换环境的时候只改一个文件。
2.4 第一个能跑的 Connection:连接字符串与错误处理
连接字符串是 ADODB 的命门。写错了不会给你友好的提示,通常就是一个HRESULT加一句“未指定的错误”。下面是一个连 SQL Server 的典型写法:
#include <comdef.h> #include "ado_import.h" int main() { CoInitialize(NULL); // 初始化 COM 库,线程模型默认 STA try { ADODB::_ConnectionPtr conn; conn.CreateInstance(__uuidof(ADODB::Connection)); // Provider 指定 OLE DB 提供者,Data Source 是服务器地址 // Initial Catalog 是数据库名,User ID 和 Password 是凭据 conn->Open( "Provider=SQLOLEDB;Data Source=127.0.0.1,1433;" "Initial Catalog=TestDB;User ID=sa;Password=YourPass;", "", "", ADODB::adConnectUnspecified); if (conn->State == ADODB::adStateOpen) { printf("连接成功\n"); } conn->Close(); } catch (_com_error& e) { // ErrorMessage() 返回的是简短描述,Description 更详细 printf("COM 错误: %s\n", (const char*)e.Description()); } CoUninitialize(); return 0; }这段代码里几个关键点:CoInitialize不能省,否则CreateInstance直接失败;conn->Open的第三个参数是User ID,第四个是Password,如果连接字符串里已经写了,这里传空字符串就行;adConnectUnspecified表示同步连接,异步连接需要额外处理事件。
错误处理用_com_error捕获,e.Description()返回的是BSTR,需要转成char*才能打印。如果Description是空的,可以看e.ErrorMessage()或者e.Error()拿HRESULT去查。
注意:连接字符串里的
Provider不要写MSDASQL,那是 ODBC 的桥接,性能差一截。SQL Server 用SQLOLEDB或者MSOLEDBSQL,后者是新版驱动,支持 TLS 1.2。
3. Recordset 遍历与参数化查询:把数据真正取出来
3.1 打开 Recordset 的四种游标与锁组合
拿到Connection之后,下一步就是取数据。ADODB 的Recordset打开方式决定了你能怎么遍历、能不能改、并发性能如何。核心参数是CursorType和LockType。
CursorType常见值:adOpenForwardOnly只能往前滚,开销最小;adOpenStatic是静态快照,可以前后滚,但看不到别人改的数据;adOpenKeyset能看到别人改的值,但看不到新增行;adOpenDynamic全动态,开销最大。
LockType常见值:adLockReadOnly只读;adLockPessimistic悲观锁,编辑时立刻锁记录;adLockOptimistic乐观锁,更新时才检查冲突;adLockBatchOptimistic批量更新。
我一般这么选:只读报表用adOpenForwardOnly + adLockReadOnly,这是最快的组合;需要分页或者前后翻的用adOpenStatic + adLockReadOnly;要编辑单条记录的用adOpenKeyset + adLockOptimistic。adOpenDynamic除非真的需要实时看到所有变化,否则别碰,性能掉得厉害。
ADODB::_RecordsetPtr rs; rs.CreateInstance(__uuidof(ADODB::Recordset)); // Source 可以是表名、SQL 语句或者存储过程名 // ActiveConnection 传已经打开的 Connection 对象 rs->Open("SELECT id, name, created_at FROM users", conn.GetInterfacePtr(), ADODB::adOpenStatic, ADODB::adLockReadOnly, ADODB::adCmdText);adCmdText告诉 ADO 第一个参数是 SQL 文本,如果是表名就用adCmdTable,存储过程用adCmdStoredProc。指定命令类型能省掉 ADO 的猜测过程,稍微快一点。
3.2 遍历字段:别用 GetItem,用 GetCollect
遍历 Recordset 的时候,取字段值有两种方式:rs->Fields->GetItem("name")->Value和rs->Fields->GetCollect("name")。前者返回FieldPtr,后者直接返回VARIANT。在循环里GetCollect更省事,也更快,因为它少了一层智能指针的引用计数操作。
while (!rs->EndOfFile) { _variant_t vId = rs->Fields->GetCollect("id"); _variant_t vName = rs->Fields->GetCollect("name"); // VARIANT 转具体类型,注意类型不匹配会抛 _com_error long id = vId.lVal; _bstr_t name = vName.bstrVal; printf("id=%ld, name=%s\n", id, (const char*)name); rs->MoveNext(); } rs->Close();这里_variant_t和_bstr_t是comdef.h里的包装类,自动管理内存。vId.lVal直接取 long 值,但如果数据库里 id 是bigint,lVal会截断,得用llVal。vName.bstrVal取 BSTR,转char*用_bstr_t的转换运算符。
提示:如果字段可能为 NULL,
GetCollect返回的VARIANT类型是VT_NULL,直接取lVal会得到 0 或者垃圾值。正确做法是先判断vId.vt != VT_NULL。
3.3 参数化查询:用 Command 对象挡住注入
拼接 SQL 字符串是自找麻烦,引号转义、日期格式、二进制字段,每个都能让你加班。ADODB 的Command对象支持参数化,虽然比现代库啰嗦,但安全性和可维护性好太多。
ADODB::_CommandPtr cmd; cmd.CreateInstance(__uuidof(ADODB::Command)); cmd->ActiveConnection = conn; cmd->CommandText = "INSERT INTO users (name, age) VALUES (?, ?)"; cmd->CommandType = ADODB::adCmdText; // 创建参数并追加,顺序必须和 SQL 里的 ? 一致 cmd->Parameters->Append(cmd->CreateParameter( "name", ADODB::adVarWChar, ADODB::adParamInput, 50, _variant_t("张三"))); cmd->Parameters->Append(cmd->CreateParameter( "age", ADODB::adInteger, ADODB::adParamInput, sizeof(int), _variant_t((long)30))); cmd->Execute(NULL, NULL, ADODB::adExecuteNoRecords);CreateParameter的五个参数分别是:参数名、数据类型、方向、大小、值。adVarWChar对应nvarchar,大小是字符数不是字节数。adExecuteNoRecords告诉 ADO 这条语句不返回结果集,省掉 Recordset 的创建开销。
参数化查询的坑在于:不是所有 OLE DB 提供者都支持?占位符。SQL Server 的SQLOLEDB支持,但某些 Access 驱动只认adParamInput加命名参数。如果Execute报“参数不足”,先检查提供者文档。
3.4 事务:用 Connection 的 BeginTrans 还是自己写 SQL
ADODB 的Connection对象自带BeginTrans、CommitTrans、RollbackTrans,底层对应 OLE DB 的ITransactionLocal。用起来很简单:
conn->BeginTrans(); try { // 这里执行多条 Command 或者 Recordset 更新 cmd1->Execute(NULL, NULL, ADODB::adExecuteNoRecords); cmd2->Execute(NULL, NULL, ADODB::adExecuteNoRecords); conn->CommitTrans(); } catch (_com_error& e) { conn->RollbackTrans(); printf("事务回滚: %s\n", (const char*)e.Description()); }但要注意嵌套事务。ADODB 的BeginTrans返回一个级别值,嵌套调用时只有最外层提交才真正生效。如果你在存储过程里也开了事务,两边混用容易死锁。我一般建议:要么全用 ADODB 的事务,要么全在存储过程里用BEGIN TRAN,别混。
另一个坑是连接断开时未提交的事务。如果程序崩溃或者网络断了,Connection对象析构时 ADODB 会尝试回滚,但如果是进程被杀,数据库那边可能挂着锁。所以事务代码一定要放在try/catch里,catch里显式RollbackTrans。
4. 避坑与排查:ADODB 在 C++ 里最容易翻车的五个地方
4.1 编译报错 C1083 找不到 msado15.tlh
现象:#import之后编译器说找不到msado15.tlh或者msado15.tli,或者报LNK2019找不到_com_ptr_t相关符号。
原因:类型库没注册,或者注册的路径跟#import里写的不一致。#import会先查注册表,找不到才去指定路径找。如果注册表里指向一个不存在的旧路径,就会失败。
解决:先用regtlibv12重新注册msado15.dll,确认注册表HKCR\TypeLib下有对应条目。如果还不行,在#import里写绝对路径,并且加上no_namespace之外的exclude排除冲突的接口。实在不行就改用CoCreateInstance手动创建,绕开#import。
4.2 运行时 CoCreateInstance 返回 REGDB_E_CLASSNOTREG
现象:编译通过,运行到CreateInstance或者CoCreateInstance时返回0x80040154,提示类未注册。
原因:32 位程序调了 64 位的 ADO 组件,或者反过来。Windows 的 COM 注册是分视图的,32 位程序只能看到 32 位的注册项。
解决:确认程序的目标平台。如果是 32 位,用C:\Windows\SysWOW64下的regsvr32注册 32 位的msado15.dll;如果是 64 位,用System32下的。regtlibv12也有 32/64 之分,别搞混。
4.3 Recordset 遍历时 GetCollect 返回 VT_NULL 导致崩溃
现象:数据库字段允许 NULL,遍历时直接取lVal或者bstrVal,程序崩溃或者拿到乱码。
原因:VARIANT的类型是VT_NULL,但代码没判断,直接按 long 或 BSTR 解析。
解决:每次GetCollect之后先判断vt。如果是VT_NULL,给一个默认值或者跳过。对于字符串字段,VT_NULL和空字符串是两回事,业务逻辑要区分。
_variant_t v = rs->Fields->GetCollect("name"); if (v.vt == VT_NULL) { // 处理 NULL } else { _bstr_t name = v.bstrVal; }4.4 连接字符串里的密码包含特殊字符导致登录失败
现象:密码里有分号、引号或者空格,连接字符串解析出错,报“登录失败”。
原因:ADODB 的连接字符串用分号分隔键值对,密码里的分号会被当成分隔符。
解决:把密码用单引号或者双引号包起来,或者改用Connection对象的Properties集合单独设置Password。更稳妥的做法是用 Windows 身份验证,连接字符串里写Integrated Security=SSPI,完全避开密码问题。
4.5 多线程下 CoInitialize 没配对导致随机崩溃
现象:程序跑一段时间后随机崩溃,崩溃点常在 COM 相关的调用里,错误码是RPC_E_CHANGED_MODE或者CO_E_NOTINITIALIZED。
原因:每个线程调用 COM 之前都要CoInitialize或者CoInitializeEx,退出时要CoUninitialize。如果在一个线程里初始化了 STA,另一个线程又用 MTA 去调同一个对象,就会出问题。
解决:明确线程模型。ADODB 的对象默认是 STA,如果要在多线程里用,每个线程单独创建Connection,不要跨线程共享。CoInitializeEx(NULL, COINIT_MULTITHREADED)可以用,但 ADODB 的某些接口在 MTA 下行为不一样,得实测。最稳的办法是每个线程一套独立的 COM 初始化和 ADODB 对象,用完就释放。
5. 进阶:用 _com_ptr_t 的智能指针习惯和连接池调优
5.1 把 _com_ptr_t 用对,别让引用计数变成玄学
#import生成的_com_ptr_t本质上是一个智能指针,CreateInstance会AddRef,析构会Release。但如果你把裸指针传出去,或者用GetInterfacePtr()拿到接口指针后忘了释放,引用计数就乱了。
我一般遵守两条规则:第一,所有 ADODB 对象都用_com_ptr_t的Ptr类型声明,不手动new。第二,需要传接口指针给 ADO 方法时,用GetInterfacePtr(),但只在调用期间有效,不要存起来。
ADODB::_ConnectionPtr conn; conn.CreateInstance(__uuidof(ADODB::Connection)); // 正确:传接口指针给 Open 的 ActiveConnection 参数 rs->Open("SELECT * FROM t", conn.GetInterfacePtr(), ...); // 错误:把 conn.GetInterfacePtr() 存到全局变量,后面 conn 析构了指针就悬空另外,_com_ptr_t的==运算符比较的是接口指针,不是对象内容。判断两个_ConnectionPtr是否指向同一个连接,得比较GetInterfacePtr()的返回值。
5.2 连接池:OLE DB 的 Session Pooling 怎么开
ADODB 底层走 OLE DB,OLE DB 有 Session Pooling 机制,可以复用连接对象,减少CoCreateInstance和登录的开销。默认情况下,OLE DB 的 Session Pooling 是开的,但 ADODB 的Connection对象关闭时是否归还到池里,取决于提供者。
对于SQLOLEDB,连接字符串里加OLE DB Services=-1可以启用所有服务,包括池化。OLE DB Services=-4是禁用池化。如果你发现连接创建很慢,先检查这个参数。
conn->Open( "Provider=SQLOLEDB;Data Source=127.0.0.1;" "Initial Catalog=TestDB;User ID=sa;Password=pass;" "OLE DB Services=-1;", "", "", ADODB::adConnectUnspecified);但池化有个副作用:连接关闭后不会立刻断开,而是留在池里。如果你在数据库端看到很多休眠连接,别慌,那是池化的正常行为。可以通过OLE DB Services=-4关掉,但性能会下降。
5.3 一个验证连接是否真正可用的技巧
有时候conn->State显示adStateOpen,但实际网络已经断了,执行查询才报错。要提前发现,可以跑一个轻量查询:
bool IsConnectionAlive(ADODB::_ConnectionPtr conn) { try { ADODB::_RecordsetPtr rs = conn->Execute("SELECT 1", NULL, ADODB::adCmdText); return !rs->EndOfFile; } catch (_com_error&) { return false; } }这个函数在每次从连接池取连接后调一次,能过滤掉死连接。代价是多一次往返,但在长连接场景里值得。
5.4 我踩过的最大一个坑:忘了 CoUninitialize 导致进程退不干净
早期写 ADODB 代码的时候,我在main里CoInitialize了,但中间某个分支return提前退出,没走到CoUninitialize。结果进程退出时 COM 库没清理,偶尔会卡在某个析构里,表现为程序“假死”几秒才退出。
后来我养成了一个习惯:把CoInitialize和CoUninitialize包在一个 RAII 类里,构造时初始化,析构时清理。这样不管从哪个分支退出,都能保证配对。
class CComInit { public: CComInit() { CoInitialize(NULL); } ~CComInit() { CoUninitialize(); } };这个类不大,但省了很多后悔药。ADODB 这套东西,语法不复杂,复杂的是 COM 的生命周期管理。把引用计数和初始化配对这两件事做对,剩下的就是查文档和试参数了。希望帮到你。
本文还有配套的精品资源,点击获取