简介:一套基于Visual C++环境、通过ODBC接口操作Access MDB数据库的通讯录示例源码,适合初学Windows桌面数据库开发的C++开发者。压缩包共32个文件,以12个h头文件、11个cpp源文件为主,附带rc资源文件、ico图标、dsp/dsw工程文件及一个示例mdb数据库,整体仅42KB,结构精简完整。已有381人浏览学习。源码完整演示了使用MFC的CDatabase与CRecordset连接并查询Access数据库的流程,覆盖登录验证、联系人信息增改查、密码修改等常用功能模块,代码注释清晰,便于直接编译运行和二次扩展。通过研读该项目,可掌握ODBC连接配置、SQL语句执行、记录集遍历以及数据库操作异常处理的实用方法,对开发涉及Access数据交互的Windows桌面应用具有直接参考价值。
1. 为什么还在用 Visual C++ 源码对接 Access MDB 数据库
MDB 是 90 年代到 2010 年前后最常见的桌面数据库格式,今天仍有大量工控、教务、医疗数据躺在 .mdb 文件里。新版 Windows 上要读出这些数据,绕不开 Visual C++、ODBC、Access 这条老链路。
这条链路是 .NET 成熟前 Windows 桌面应用的标准方案,很多存量工程至今用 VC++ 6.0 或 VS2008 编译,源码里写死连接串,换机器后先崩的往往是环境而不是业务逻辑。
读者分两类:一类接手老源码,把 MDB 数据迁出去;另一类在新机器上重编老代码,遇到 DSN 看不到、记录集打不开、0xc0000005 崩溃。下面按驱动选型、连接串、取数、增删改、排错的顺序讲。
2. ODBC 与 Access MDB 的驱动选型:先解决 32/64 位和连接串
2.1 老 Jet 驱动和 ACE 驱动怎么选
访问 .mdb 文件面对两个 ODBC 驱动:老驱动 "Microsoft Access Driver (.mdb)" 基于 Jet 4.0,文件格式支持到 Access 2003;新驱动 "Microsoft Access Driver (.mdb, *.accdb)" 基于 ACE 引擎,由 AccessDatabaseEngine 安装包提供,同时认 .mdb 和 .accdb。选型的关键不在格式,而在进程位数:Jet 4.0 驱动只有 32 位实现,VC++ 工程平台如果是 x64,这个驱动在 64 位进程里根本加载不出来,ODBC 管理器里也看不到它。
反过来,ACE 驱动有 x86 和 x64 两个版本,64 位工程必须装 AccessDatabaseEngine_x64.exe,32 位工程装 x86 版。同一个 64 位系统里两个版本可以共存,注册表视图不同,互不干扰。老项目源码里如果写死Driver={Microsoft Access Driver (*.mdb)},工程又是 x64,要么把平台改回 Win32,要么把连接串改成 ACE 驱动名,没有第三条路。
2.2 32 位和 64 位 ODBC 管理器是两个入口
odbcad32.exe 在 System32 和 SysWOW64 里各有一份,分别是 64 位和 32 位的数据源管理器。很多人只打开开始菜单里的 ODBC 数据源,在 64 位界面建了 DSN,结果 32 位程序仍然报"找不到数据源"。判断机器上能用哪个驱动,先看 VC++ 工程配置管理器的平台,再看对应位数的 odbcad32。
| VC++ 工程平台 | 打开的管理器 | 可用的驱动名 | 对应安装包 |
|---|---|---|---|
| Win32 (x86) | C:\Windows\SysWOW64\odbcad32.exe | Microsoft Access Driver (.mdb) 或 (.mdb, *.accdb) | AccessDatabaseEngine_x86.exe |
| x64 | C:\Windows\System32\odbcad32.exe | Microsoft Access Driver (*.mdb, *.accdb) | AccessDatabaseEngine_x64.exe |
提示:SysWOW64 里的 odbcad32 是 32 位,路径反直觉,最容易搞混;64 位驱动不会出现在 32 位管理器里,反之亦然。
2.3 用无 DSN 连接串绕过每台机器的配置
DSN 要在每台目标机器预先创建,而且 32 位和 64 位各有各的 DSN 列表,部署时漏一台就报错。我一般直接在源码里写无 DSN 连接串,程序自己带着路径去连,目标机器只要装对驱动即可。
// Visual C++ 中的无 DSN 连接串构造(Unicode 字符集) CString strConn; strConn.Format( _T("Driver={Microsoft Access Driver (*.mdb, *.accdb)};") _T("Dbq=C:\\data\\school.mdb;") _T("Pwd="));Driver 花括号里的名字必须和系统实际注册的驱动名逐字一致,多一个空格都会在打开时报"未找到数据源名称"。Dbq 是 MDB 文件全路径,反斜杠要双写;路径含中文时,Unicode 字符集下直接写即可,ANSI 工程里要自己做转换。Pwd 是打开数据库的密码,没密码就留空,结尾分号不能省。
2.4 装机后用注册表确认驱动是否注册
程序跑之前,先在命令行确认驱动注册到位,避免把时间浪费在排查环境上。
:: 64 位系统的驱动视图 reg query "HKLM\SOFTWARE\ODBC\ODBCINST.INI\ODBC Drivers" :: 32 位系统的驱动视图 reg query "HKLM\SOFTWARE\WOW6432Node\ODBC\ODBCINST.INI\ODBC Drivers"输出的驱动列表里能看到 "Microsoft Access Driver (*.mdb, *.accdb)" 才算装好。如果装了 AccessDatabaseEngine 但列表里没有,多半是装错位数,或者被 Office 的 Click-to-Run 版本覆盖了注册项,卸载后重装对应位数的 ACE 引擎即可。
3. Visual C++ 源码骨架:用 CDatabase 打开 MDB 并读取记录集
3.1 为什么选 MFC 封装而不是裸 ODBC API
裸 ODBC API 的调用链是 SQLAllocHandle、SQLSetEnvAttr、SQLDriverConnect、SQLExecDirect、SQLFetch,再加上手写列绑定和类型转换,一个 SELECT 就要几十行。MFC 的 CDatabase 和 CRecordset 把这套流程封装成三个对象,异常统一成 CDBException。老项目的 Visual C++ 源码大多基于 MFC 对话框框架,直接沿用封装维护成本最低。如果工程是纯 Win32 控制台、没引 MFC,那就用裸 API 更轻,但连接串和驱动选型的原则完全一样,把下面代码里的 OpenEx 换成 SQLDriverConnect 即可。
3.2 打开数据库的最小代码
#include <afxdb.h> // CDatabase、CRecordset、CDBException void OpenMdb(CDatabase& db, LPCTSTR szMdbPath) { CString strConn; strConn.Format( _T("Driver={Microsoft Access Driver (*.mdb, *.accdb)};") _T("Dbq=%s;"), szMdbPath); // noOdbcDialog:不弹 DSN 选择框,解析失败直接抛异常 db.OpenEx(strConn, CDatabase::noOdbcDialog); }OpenEx 失败时抛 CDBException 而不是返回 FALSE,调用方必须包 try/catch。第二个参数 noOdbcDialog 是必须的,否则驱动解析连接串失败时可能弹出系统对话框,无人值守的服务器程序会卡在那里。连接串里 Dbq 指向的路径在调用前最好用 PathFileExists 确认一次,把"文件不存在"和"驱动不对"两类错误分开。
3.3 用 CRecordset 取数:按列序号还是按列名
打开记录集最省事的方式是直接传 SQL,然后循环 MoveNext。取字段值用 GetFieldValue 有两种重载:按列序号从 0 开始,或按列名字符串。列序号在 SELECT * 时不可控,我习惯在 SQL 里显式写列名,可读性更好。
CRecordset rs(&db); rs.Open(CRecordset::forwardOnly, _T("SELECT [学号], [姓名], [成绩] FROM score")); while (!rs.IsEOF()) { CString strNo, strName; double dScore = 0.0; rs.GetFieldValue((short)0, strNo); rs.GetFieldValue((short)1, strName); rs.GetFieldValue((short)2, dScore); TRACE(_T("%s %s %.1f\n"), strNo, strName, dScore); rs.MoveNext(); } rs.Close();按列名版本rs.GetFieldValue(_T("成绩"), dScore)在列很多时可读性更好,但每次调用都要查列映射,循环里性能略差。字段为 NULL 时数值类型得到 0、字符串得到空串,要区分 NULL 和 0 得先用 IsFieldNull。类型不匹配会抛 CDBException,比如把备注字段取成 double,异常里的错误码是 SQL_ERROR。
3.4 记录集类型:forwardOnly、dynaset 和 snapshot 怎么挑
| 记录集类型 | 打开参数 | 适用场景 | 注意点 |
|---|---|---|---|
| forwardOnly | CRecordset::forwardOnly | 大批量只读导出 | 只能向后,不能 MovePrev |
| dynaset | CRecordset::dynaset | 中等数据量、要编辑 | 要求 MDB 文件非只读 |
| snapshot | CRecordset::snapshot | 报表、重复读取固定视图 | 其他程序改的数据不可见 |
3.5 更稳的字段绑定:DoFieldExchange 和 RFX
GetFieldValue 运行时按类型取数,表结构变了就要改代码。更稳的做法是在派生 CRecordset 里用 DoFieldExchange 声明列和 C++ 成员的映射,编译期把模型固定下来:
class CScoreSet : public CRecordset { public: CString m_strNo; // 学号 CString m_strName; // 姓名 double m_dScore; // 成绩 CScoreSet(CDatabase* pDB) : CRecordset(pDB) {} virtual CString GetDefaultSQL() { return _T("score"); } virtual void DoFieldExchange(CFieldExchange* pFX) { pFX->SetFieldType(CFieldExchange::outputColumn); RFX_Text(pFX, _T("学号"), m_strNo); RFX_Text(pFX, _T("姓名"), m_strName, 64); RFX_Double(pFX, _T("成绩"), m_dScore); } };RFX_Text 不传长度时默认按 255 字节缓冲绑定,表里字段是"备注"且内容超长时要显式传长度,比如RFX_Text(pFX, _T("备注"), m_strMemo, 2048)。RFX 函数要和 Access 字段类型对应:数字字段分 Byte、Integer、Long、Single、Double,绑错类型轻则取数异常,重则缓冲区越界,这是后面 0xc0000005 崩溃的高发原因。派生类打开后直接用成员变量即可:
CScoreSet rs(&db); rs.Open(CRecordset::dynaset); while (!rs.IsEOF()) { // rs.m_strNo / rs.m_strName / rs.m_dScore 已自动填充 rs.MoveNext(); }4. 增删改查落地:参数化、只读陷阱和高频错误码排查
4.1 ExecuteSQL 执行 INSERT/UPDATE/DELETE
不查询结果的语句用CDatabase::ExecuteSQL,它内部用 SQLExecDirect 一次性执行,适合批量初始化和维护脚本。ExecuteSQL 不支持 MFC 参数绑定,语句里出现用户输入时要自行转义单引号(把'换成''),所以涉及输入内容的写入我一般不走这条,而是用 4.2 的记录集参数化。
try { db.ExecuteSQL(_T("UPDATE score SET [成绩] = 95 WHERE [学号] = '001'")); } catch (CDBException* e) { // e->m_strError 里是驱动返回的完整错误文本 e->Delete(); }列名若是 Access 保留字,SQL 里必须加方括号;RFX 绑定里仍写去掉括号的列名,因为驱动返回的字段元数据不带括号。ExecuteSQL 执行不返回行集,判断是否成功只能靠有没有抛异常。
4.2 用 ? 参数和 RFX 绑定,别拼 SQL
MFC CRecordset 支持参数化查询:在派生类里加 param 类型的 RFX 绑定,配合 m_strFilter 使用。参数以?占位符出现在 WHERE 里,驱动按绑定顺序填值,单引号、中文都不用手工转义。
class CScoreSet : public CRecordset { public: CString m_strNo; // 输出列:学号 CString m_strParamNo; // 查询参数 CScoreSet(CDatabase* pDB) : CRecordset(pDB) {} virtual CString GetDefaultSQL() { return _T("score"); } virtual void DoFieldExchange(CFieldExchange* pFX) { pFX->SetFieldType(CFieldExchange::outputColumn); RFX_Text(pFX, _T("学号"), m_strNo, 32); pFX->SetFieldType(CFieldExchange::param); RFX_Text(pFX, _T("param1"), m_strParamNo); } }; CScoreSet rs(&db); rs.m_strFilter = _T("[学号] = ?"); // ? 的个数要和 param 绑定数一致 rs.m_strParamNo = _T("001"); rs.Open(CRecordset::dynaset); if (!rs.IsEOF()) { // 找到一条,rs.m_strNo 就是它的学号 } rs.Close();参数绑定顺序由 DoFieldExchange 里 param 段落的声明顺序决定,和 WHERE 中?的出现顺序一一对应。LIKE 查询也能参数化:m_strFilter = _T("[姓名] LIKE ?"),参数值传_T("张%"),通配符写在参数值里。
注意:列名拼写错误也会报"参数不足,期待是 1",Jet 会把无法解析的标识符当成查询参数。先拼写核对再查绑定数量。
4.3 中文路径和只读文件的坑
Jet 驱动解析 Dbq 路径时按系统 ANSI 代码页处理,中文 Windows 上通常没问题;但工程是 ANSI 字符集又改了系统区域设置时,中文目录下的 MDB 可能打不开,对策是把工程字符集改成 Unicode。另一个高频坑是 MDB 文件在只读目录,比如被拷进 Program Files 或勾了只读属性,打开记录集时报"不能更新的查询"或直接抛异常。先右键文件属性去掉只读,再确认进程对目录有写权限。Access 是文件级锁定,记录集没 Close 之前外部改文件也会失败,脚本跑完记得rs.Close()。
4.4 高频错误码对照表
| 错误/退出表现 | 最常见原因 | 第一步处理 |
|---|---|---|
| 未找到数据源名称且未指定默认驱动程序 | 连接串 Driver 名写错或位数不匹配 | reg query 核对驱动注册,见 2.4 |
| 参数不足,期待是 1 | WHERE 里 ? 和 param 绑定数不一致,或列名拼错 | 数 ? 个数,检查 DoFieldExchange 顺序 |
| 不能更新的查询 | 记录集只读打开或 MDB 文件只读 | 改 dynaset,去只读属性 |
| 退出码 0xc0000005 | 字段类型绑错、缓冲长度不足、CDatabase 先于记录集析构 | 先关 rs 再关 db,核对 RFX 类型 |
4.5 用 CDBException 拿到真正的原因
退出码只能说明进程崩了,驱动为什么拒绝 SQL 还得靠异常对象。CDBException 的 m_strError 是 ODBC 驱动返回的完整错误文本,m_nRetCode 是底层 SQLxxx 调用的返回码(-1 即 SQL_ERROR)。调试期把这两项和 m_strStateNativeOrigin 一起打到日志,很多问题一眼定位。
catch (CDBException* e) { CString strLog; strLog.Format(_T("ret=%d, err=%s\n"), e->m_nRetCode, (LPCTSTR)e->m_strError); OutputDebugString(strLog); e->Delete(); }5. 收尾技巧:连接复用、ODBC 跟踪和 0xc0000005 的取证习惯
5.1 一个 CDatabase 多处用,别每条语句都 Open
Jet/ACE 引擎启动和文件锁初始化比想象中慢,对话框里每条 SQL 都重新 OpenEx 会明显卡顿。我一般把 CDatabase 作为窗口类的成员变量,初始化时打开一次,程序退出前再 Close。多个线程共享同一个 CDatabase 会随机崩溃,必须每个线程单独一个连接。UPDATE 一直挂起时,先看有没有 CRecordset 没 Close——Access 是文件级锁,未关闭的记录集可能把整库锁住。
5.2 用 ODBC 跟踪确认 SQL 到底发没发出去
"数据没写进去"和"SQL 压根没执行"是两回事。开启 ODBC 跟踪后,驱动把每条 SQLPrepare、SQLExecute 和参数值写入日志:
reg add "HKLM\SOFTWARE\ODBC\ODBC.INI\ODBC" /v Trace /t REG_SZ /d "1" /f reg add "HKLM\SOFTWARE\ODBC\ODBC.INI\ODBC" /v TraceFile /t REG_SZ /d "C:\odbc_trace.log" /f复现问题后把 Trace 改回 "0" 并删除日志,避免持续写盘。日志里能看到执行到哪一步出错,比单步调试更快。
5.3 0xc0000005 先看栈,再查 RFX 绑定
进程以 0xc0000005(十进制 -1073741819)退出时,用 Visual Studio 的本地 Windows 调试器跑一遍直接看调用栈。栈停在 CRecordset::MoveNext 或 RFX 内部,优先核对 DoFieldExchange 里字段类型和 Access 表结构,数字字段的 Byte/Integer/Long/Single/Double 宽度不同,绑错会越界。另一个常见现场是 CRecordset 成员比 CDatabase 后析构,访问了已关闭的连接对象。先确认目标机装了匹配编译工具集的 Visual C++ Redistributable,缺运行库的进程会在入口直接退出,栈里没有你的任何代码,和 ODBC 崩溃是两类问题。Debug 版打开_CrtSetDbgFlag的检测,配合 ODBC 跟踪,通常能把崩溃缩到某一次具体 SQLExecute 上。
本文还有配套的精品资源,点击获取