简介:这份资源面向在 C++ 环境下进行数据库开发的程序员与学习者,围绕 ADODB(ActiveX Data Objects for Database)这一 COM 接口,提供了一套可运行的数据库操作示例工程。内容涵盖 Connection、Command、Recordset 等核心对象的封装,涉及连接字符串配置、SQL 执行、结果集遍历、游标类型与记录锁定策略,以及类型库注册、异常捕获、连接池与多数据库兼容性等实践要点,适合希望掌握 C++ 访问 SQL Server、Oracle、MySQL 等数据库的中级开发者参考。压缩包共 79 个文件,约 38.43MB,以 h 头文件、cpp 源文件、obj 编译产物、dll 动态库、lib 导入库及 sln 解决方案为主,另含 tlog、pdb、tli 等工程中间文件,完整保留了 Visual Studio 工程结构。目前已有 110 人学习下载。通过研读 ADODatabase.h、ADORecordset.h 等模块,读者可理解连接建立、SQL 执行与记录集处理的核心流程,并借鉴错误处理与工程组织方式,快速搭建自己的数据库访问框架。
1. 从 AdoDB.rar 说起:C++ 里怎么把 ADODB 这层数据库访问壳用起来
如果你手里拿到一个叫AdoDB.rar的包,里面混着ADODB、regtlibv12、C++ 源码和数据库访问相关的东西,第一反应大概率是懵的:这到底是封装库、示例工程,还是某个老项目里扒出来的数据库访问层?我先把结论摆出来——这类包的核心,通常是用 C++ 通过 COM 去调用微软的 ADODB 组件,把Connection、Recordset、Command这些对象包一层,让 C++ 代码能像写脚本一样操作数据库。regtlibv12则是注册类型库的工具,用来把 ADODB 的类型库信息注册进系统,让编译器能通过#import生成智能指针包装类。这套东西在维护老系统、做内部工具、对接 SQL Server 或 Access 时依然能跑,适合需要快速在 C++ 里接数据库、又不想引入重型 ORM 的从业者。下面我按“先搞懂它是什么、再动手跑通、最后避开坑”的顺序,把这条路讲透。
2. ADODB 在 C++ 里的真实角色:COM 壳、类型库与 regtlibv12
2.1 为什么 C++ 调数据库会绕到 ADODB 上
C++ 本身没有标准数据库访问接口,早期 Windows 平台上常见做法是走 ODBC、OLE DB 或者直接调 ADO。ADO 是微软把 OLE DB 包了一层之后给出的自动化接口,用 COM 暴露出来,脚本语言和 VB 用起来很顺手。C++ 要用,就得通过 COM 的IDispatch去调,或者用#import让编译器根据类型库生成包装类。AdoDB.rar这类包,往往就是有人把#import生成的.tlh、.tli文件,加上自己的封装类,一起打了个压缩包。你拿到之后,核心工作不是重写 ADO,而是把类型库注册好、把包装类引进来、把连接字符串和记录集操作写对。
这里有个容易混淆的点:ADODB 是组件名,ADO 是技术名,msado15.dll是常见的实现库。C++ 里通过#import "C:\Program Files\Common Files\System\ado\msado15.dll" no_namespace rename("EOF", "EndOfFile")这种写法,让编译器读取类型库并生成智能指针。regtlibv12出现的原因,是有些环境里类型库没注册,或者版本对不上,需要手动把.tlb注册进去。它通常和regtlib.exe、regsvr32属于同一类工具,只是版本号带 v12,常见于较老的 Visual Studio 或 Office 附带组件里。
2.2 包内文件类型与各自用途
拿到AdoDB.rar后,先别急着编译。解压后按扩展名分类,能省很多时间。下面这张表是我一般会先扫一遍的对照关系:
| 文件/目录 | 典型用途 | 处理方式 |
|---|---|---|
.dsp/.vcproj/.vcxproj | 老版本工程文件 | 用对应 VS 版本打开或升级 |
.h/.cpp | 封装类与调用示例 | 看类名、连接字符串、SQL 拼接 |
.tlh/.tli | #import生成的类型库包装 | 不要手改,随编译重新生成 |
.tlb | 类型库文件 | 用regtlibv12或regsvr32注册 |
.dll/.ocx | 可能包含 ADO 相关组件 | 确认位数,注册前先备份 |
.sql/.mdb/.accdb | 示例数据库 | 用于本地复现 |
.rar内嵌说明 | 作者备注 | 只作参考,不当作官方文档 |
这里要提醒一句:如果包里有.dll,先确认它是 32 位还是 64 位。C++ 工程的目标平台必须和 DLL 位数一致,否则会出现“模块加载失败”或者“类未注册”的报错。很多老包是 32 位的,你拿 VS2022 默认 x64 去编,必然翻车。
2.3 regtlibv12 到底做了什么
regtlibv12的作用是把类型库注册到系统注册表里,让 COM 能根据 ProgID 或 CLSID 找到对应的类型信息。常见命令形式是:
regtlibv12.exe "C:\path\to\msado15.dll"或者对.tlb文件:
regtlibv12.exe "C:\path\to\yourlib.tlb"执行后,注册表里会写入类型库相关键值。判断是否成功,可以看命令有没有报错,也可以用oleview这类工具查。注意:注册类型库通常需要管理员权限,普通命令行会静默失败或者提示拒绝访问。另一个坑是,如果系统里已经注册了更高版本的 ADO,你再注册旧版可能覆盖掉,影响其他程序。我一般会先记下当前msado15.dll的版本,再决定要不要动。
2.4 用 #import 生成 C++ 可用的智能指针
真正让 C++ 能写 ADODB 代码的关键一步,是#import。它会在编译时读取类型库,生成.tlh和.tli两个文件,里面包含_ConnectionPtr、_RecordsetPtr、_CommandPtr等智能指针类型。典型写法如下:
// 引入 ADODB 类型库,生成智能指针包装 #import "C:\Program Files\Common Files\System\ado\msado15.dll" \ no_namespace \ rename("EOF", "EndOfFile") \ rename("BOF", "BeginOfFile") // 初始化 COM 库,线程模型按单线程套间即可 ::CoInitialize(NULL); try { // 创建连接对象 _ConnectionPtr pConn(__uuidof(Connection)); // 打开 SQL Server 示例连接,实际替换为你的连接字符串 pConn->Open( "Provider=SQLOLEDB;Data Source=127.0.0.1;Initial Catalog=TestDB;User ID=sa;Password=yourpwd;", "", "", adConnectUnspecified ); // 执行查询并取回记录集 _RecordsetPtr pRs = pConn->Execute("SELECT id, name FROM users", NULL, adCmdText); while (!pRs->EndOfFile) { long id = pRs->Fields->Item["id"]->Value; _bstr_t name = pRs->Fields->Item["name"]->Value; // 这里按业务处理,示例只做输出 printf("id=%ld name=%s\n", id, (const char*)name); pRs->MoveNext(); } pRs->Close(); pConn->Close(); } catch (_com_error& e) { // 打印 COM 错误描述,便于定位连接或 SQL 问题 printf("COM error: %s\n", (const char*)e.Description()); } ::CoUninitialize();这段代码的逻辑说明:#import负责把类型库变成 C++ 可识别的接口;CoInitialize初始化 COM 环境;_ConnectionPtr和_RecordsetPtr是智能指针,析构时会自动释放;Open的第一个参数是连接字符串,Provider 决定用哪个数据源驱动;Execute返回记录集,遍历时用EndOfFile判断结束。参数方面,adConnectUnspecified、adCmdText这些枚举值来自 ADO 类型库,写错会编译不过。连接字符串里的Data Source可以是 IP 或实例名,Initial Catalog是数据库名,认证方式可以用 SQL 账号,也可以改成Integrated Security=SSPI走 Windows 认证。
提示:
#import路径在不同机器上可能不同,建议用环境变量或相对路径,避免硬编码导致换机编译失败。
3. 从解压到跑通:AdoDB.rar 的最小复现路径
3.1 环境准备与工程配置
先确定工具链。老包常见于 VS2008、VS2010 时代,如果你用 VS2019/2022,打开旧工程会提示升级,升级后重点检查字符集和平台工具集。字符集建议统一成“使用多字节字符集”或“使用 Unicode 字符集”,但 ADO 的_bstr_t在两种下都能用,关键是别混用char*和LPCWSTR。平台工具集如果报错,可以装对应版本的生成工具,或者把工程改成当前工具集后重新编译。
工程配置里需要确认几项:附加包含目录是否包含 ADO 头文件路径;链接器是否需要comsuppw.lib或comsupp.lib;C++ 语言标准不要开太高,老代码在 C++17 下可能因为throw规格或auto_ptr报错。我一般会先把工程属性里的“SDL 检查”关掉,减少老代码的编译干扰,跑通后再按需开启。
3.2 注册类型库与验证 COM 可用
如果#import报“无法打开类型库”或“找不到 msado15.dll”,先确认文件是否存在。常见路径是C:\Program Files\Common Files\System\ado\msado15.dll,64 位系统上 32 位程序可能走SysWOW64下的版本。确认存在后,用管理员权限运行:
regtlibv12.exe "C:\Program Files\Common Files\System\ado\msado15.dll"如果regtlibv12不在手边,也可以用regsvr32注册 DLL,但注意regsvr32注册的是 COM 服务器,不是类型库,两者作用不同。验证 COM 是否可用,可以写一个最小程序只做CoInitialize和创建_ConnectionPtr,不连数据库,看是否抛异常。如果创建对象就失败,说明类型库或注册表有问题;如果能创建但Open失败,问题在连接字符串或数据库服务。
3.3 连接字符串的写法与常见参数
连接字符串是 ADO 里最容易写错的部分。下面这张表列出 SQL Server 和 Access 两种常见场景的参数:
| 参数 | SQL Server 示例 | Access 示例 | 说明 |
|---|---|---|---|
| Provider | SQLOLEDB 或 MSOLEDBSQL | Microsoft.ACE.OLEDB.12.0 | 决定驱动 |
| Data Source | 127.0.0.1 或 .\SQLEXPRESS | 文件路径 | 服务器或文件 |
| Initial Catalog | TestDB | 不适用 | 数据库名 |
| User ID / Password | sa / 密码 | 不适用 | SQL 认证 |
| Integrated Security | SSPI | 不适用 | Windows 认证 |
| Jet OLEDB:Database Password | 不适用 | 密码 | Access 密码 |
写连接字符串时,分号分隔,值里如果有分号需要转义或加引号。我一般会先在.udl文件里测试连接,确认能通后再把字符串抄进代码。.udl文件可以通过右键新建文本文档改扩展名得到,双击打开就是数据链接属性界面,测通后用记事本打开就能看到连接字符串。
3.4 记录集遍历与字段取值
记录集遍历看起来简单,但字段取值有坑。Fields->Item["name"]->Value返回的是VARIANT,直接赋给_bstr_t或long时,如果字段为 NULL,会抛异常。稳妥做法是先判断vt类型,或者用Fields->Item["name"]->Value之前检查IsNull。下面是一个带空值处理的片段:
// 遍历记录集,处理可能为 NULL 的字段 while (!pRs->EndOfFile) { // 取 id 字段,假设为整型 long id = 0; if (pRs->Fields->Item["id"]->Value.vt != VT_NULL) { id = pRs->Fields->Item["id"]->Value; } // 取 name 字段,假设为字符串 _bstr_t name = L""; if (pRs->Fields->Item["name"]->Value.vt != VT_NULL) { name = pRs->Fields->Item["name"]->Value; } // 输出或业务处理 printf("id=%ld name=%s\n", id, (const char*)name); pRs->MoveNext(); }参数说明:vt是 VARIANT 的类型标记,VT_NULL表示空值;_bstr_t可以直接从 VARIANT 构造,但空值时会抛_com_error。如果字段是日期或 decimal,取值方式又不同,需要先转换。我一般会在封装层里写几个辅助函数,按字段类型分别处理,避免在业务代码里到处判断。
3.5 用完后释放顺序与 COM 卸载
释放顺序不对,可能导致程序退出时崩溃或数据库连接未关闭。正确顺序是:先关闭记录集,再关闭连接,然后让智能指针析构,最后CoUninitialize。如果用了_CommandPtr,也要先释放。下面是一个典型的清理片段:
// 按顺序释放资源,避免 COM 对象残留 if (pRs != nullptr && pRs->State != adStateClosed) { pRs->Close(); } if (pConn != nullptr && pConn->State != adStateClosed) { pConn->Close(); } pRs = nullptr; pConn = nullptr; ::CoUninitialize();注意:State属性返回对象状态,adStateClosed表示已关闭。不要重复关闭,否则会抛异常。如果程序是多线程的,每个线程都要单独CoInitialize,并且不要跨线程传递 COM 指针,除非做了封送处理。老代码里经常在主线程初始化 COM,然后在工作线程直接用连接对象,这是典型的翻车点。
4. 避坑与排查:AdoDB 在 C++ 里最容易翻车的 5 个地方
4.1 报错“类未注册”或“无法打开类型库”
现象:编译时#import失败,提示找不到类型库;或者运行时创建_ConnectionPtr抛_com_error,描述里带“类未注册”。原因通常是 ADO 组件没注册、位数不匹配、或者类型库路径不对。解决:先用管理员权限运行regtlibv12注册msado15.dll,确认工程目标平台和 DLL 位数一致,检查#import路径是否存在。如果系统是 64 位,32 位程序要引SysWOW64下的 ADO 组件,而不是System32下的。
4.2 连接字符串写错导致“未找到数据源”
现象:Open时抛异常,描述里带“未找到数据源名称”或“登录失败”。原因可能是 Provider 写错、服务器名解析不了、认证方式不对。解决:先用.udl文件测试连接,确认能通后再抄字符串;SQL Server 要确认 TCP/IP 协议已启用,端口没被防火墙挡;Access 要确认 ACE 驱动已安装,且文件路径没有中文或空格问题。如果用的是SQLOLEDB,在新系统上可能被弃用,可以换MSOLEDBSQL。
4.3 字段取值抛异常或得到乱码
现象:遍历记录集时,取某个字段突然抛_com_error,或者字符串显示乱码。原因通常是字段为 NULL、字符集不匹配、或者 VARIANT 类型没转换。解决:取值前判断vt != VT_NULL;字符串统一用_bstr_t接收,输出时再转const char*;如果数据库是 UTF-8 而 ADO 按 ANSI 处理,需要在连接字符串里加CharacterSet=UTF8或改用宽字符输出。乱码问题有时也跟控制台代码页有关,可以先用wprintf验证。
4.4 多线程下 COM 初始化失败或崩溃
现象:主线程能跑,工作线程一调 ADO 就崩,或者报“尚未调用 CoInitialize”。原因:每个线程都要单独初始化 COM,且线程模型要匹配。解决:在工作线程入口调用CoInitializeEx(NULL, COINIT_MULTITHREADED)或COINIT_APARTMENTTHREADED,退出前CoUninitialize。如果连接对象要跨线程用,必须做封送,或者每个线程自己创建连接。我一般建议每个线程独立连接,避免共享 COM 指针。
4.5 程序退出时崩溃或数据库连接未释放
现象:程序关闭时弹异常,或者数据库里看到连接一直挂着。原因:释放顺序不对,或者智能指针在CoUninitialize之后才析构。解决:确保pRs、pConn先置空,再CoUninitialize;不要在全局对象里持有 COM 指针,因为全局析构顺序不可控。如果用了连接池,确认连接字符串里没有禁用池化,必要时手动调用Close。另外,_com_error捕获后要打印ErrorMessage和Source,这两个信息比Description更具体。
5. 进阶:把 ADODB 封装成可复用的 C++ 数据库层
5.1 封装类的接口设计与异常处理
直接在每个业务函数里写_ConnectionPtr和_RecordsetPtr,代码会散得到处都是。我一般会封一个AdoDatabase类,提供Connect、Execute、Query、Close四个方法,内部持有连接指针,记录集用局部变量。异常处理统一在封装层捕获_com_error,转成自定义错误码或抛出std::runtime_error,这样上层不用关心 COM 细节。下面是一个简化接口:
// 封装 ADODB 的数据库类,隐藏 COM 细节 class AdoDatabase { public: AdoDatabase() { ::CoInitialize(NULL); } ~AdoDatabase() { Close(); ::CoUninitialize(); } // 连接数据库,传入连接字符串 bool Connect(const std::string& connStr) { try { m_conn.CreateInstance(__uuidof(Connection)); m_conn->Open(connStr.c_str(), "", "", adConnectUnspecified); return true; } catch (_com_error& e) { m_lastError = (const char*)e.Description(); return false; } } // 执行查询,返回记录集指针由调用方遍历 _RecordsetPtr Query(const std::string& sql) { try { return m_conn->Execute(sql.c_str(), NULL, adCmdText); } catch (_com_error& e) { m_lastError = (const char*)e.Description(); return nullptr; } } // 关闭连接 void Close() { if (m_conn != nullptr && m_conn->State != adStateClosed) { m_conn->Close(); } m_conn = nullptr; } std::string GetLastError() const { return m_lastError; } private: _ConnectionPtr m_conn; std::string m_lastError; };参数说明:CreateInstance用于延迟创建 COM 对象,比直接构造更可控;Open的第三个参数是用户 ID 和密码,如果连接字符串里已经带了,这里传空;Execute返回的_RecordsetPtr由调用方负责关闭。这个封装没有处理多线程,如果要在多线程用,需要把CoInitialize移到线程入口,或者加锁。
5.2 参数化查询与 SQL 注入防护
拼接 SQL 字符串是另一个血泪坑。用户输入里带单引号,轻则查询失败,重则数据被改。ADO 的Command对象支持参数化查询,写法如下:
// 使用 Command 对象做参数化查询,避免 SQL 注入 _CommandPtr pCmd(__uuidof(Command)); pCmd->ActiveConnection = pConn; pCmd->CommandText = "SELECT id, name FROM users WHERE name = ?"; pCmd->Parameters->Append(pCmd->CreateParameter( "name", adVarWChar, adParamInput, 50, _variant_t("张三"))); _RecordsetPtr pRs = pCmd->Execute(NULL, NULL, adCmdText);参数说明:CreateParameter的五个参数分别是名称、类型、方向、大小、值;adVarWChar表示宽字符串,adParamInput表示输入参数。如果字段是整型,用adInteger。参数化查询不仅防注入,还能让数据库复用执行计划,性能更好。老代码里大量用sprintf拼 SQL 的,建议逐步替换。
5.3 用 regtlibv12 处理类型库版本冲突
如果机器上装了多个版本的 ADO,或者 Office 带来的类型库和系统自带的不一致,#import可能引到旧版本。这时可以用regtlibv12显式注册你需要的那个.tlb,然后在#import里写完整路径,避免编译器去注册表里找默认版本。验证方法:编译后看生成的.tlh里LIBID是否和你注册的一致。如果还是不对,可以在#import后面加version或libid限定。注意,注册类型库会影响全局,测试完最好记录原始状态,必要时恢复。
5.4 一个可复用的连接池思路
ADO 本身有连接池,但 C++ 里通过 COM 调用时,池化行为取决于 Provider 和连接字符串。如果频繁开关连接,性能会差。我一般会在封装层维护一个连接数组,按线程 ID 分配,每个线程复用自己的连接,空闲时不断开。实现时注意:连接对象不是线程安全的,不要跨线程共享;连接池大小要限制,避免数据库端连接数爆掉。如果不想自己写,也可以用Sessions或Connections的池化参数,但老版本 ADO 支持有限。更稳妥的做法是每个线程一个连接,用完不关,程序退出时统一清理。
5.5 验证封装是否可靠的三个小测试
写完封装后,我一般会跑三个测试:第一,连续查询 1000 次,看内存和句柄是否增长;第二,传入带单引号和分号的字符串,确认参数化查询不报错;第三,在工作线程里调用查询,确认 COM 初始化正确。如果这三个都过,基本可以投入内部使用。最后一个习惯:每次改完连接字符串或 SQL,先用.udl和数据库客户端验证一遍,再进代码调试。这个习惯帮我省了很多“以为是代码问题,其实是环境问题”的时间。希望帮到你。
本文还有配套的精品资源,点击获取