简介Visual C通过ODBC访问Access MDB数据库的完整示例工程面向需要开发Windows桌面数据库应用的C开发者适用于课程设计、毕业设计及小型信息管理系统场景演示了从环境配置、ODBC数据源连接到数据库增删改查的完整链路。压缩包共32个文件包含12个头文件(.h)、11个实现文件(.cpp)、工程文件(.dsp/.dsw)及示例数据库(.mdb)另有图标资源、对话框资源脚本和说明文档整体仅42KB可直接编译运行非常适合对照学习。已有381人学习下载。源码以通讯录管理项目为载体核心模块包括AddressDlg界面交互、PersonSet记录集操作、PswdSet密码设置、AddInfoDlg/ModifyDlg信息维护等覆盖CDatabase/CRecordset封装、ODBC SQL执行、事务与错误处理等关键编程点。同时涉及Access MDB的Jet引擎特性与安全连接字符串使用可帮助初学者系统掌握VC操作Access数据库的常用套路也可为实际MFC数据库项目提供可复用的基础框架。1. 老系统的 MDB 文件还在手工导数据却把人逼疯这就是 visual c 源码 ODBC 存在的理由手头有一套十年前的业务系统数据库是 Access 的 MDB 文件客户不要换库只想要自动把分站数据合并到总部。管理工具点一下动一下导出还要装 Office更别提跨机器部署。转了一圈最可靠的反而是最底层那条路visual c 源码里直接调 ODBC API把 MDB 当成普通数据源读写连接字符串一行剩下全是 C/C 代码的事。标题里的核心词就是这么回事——用 Visual C 写 ODBC 程序访问 Access MDB 数据库解决的是机器上不一定有 Access但程序必须能读写数据的现实问题。适合谁手头有 Win32 老项目要维护的人、需要写一次性数据迁移工具的人、想彻底摆脱管理工具手工操作的工程师。下面从环境准备、最小可运行代码、写入与参数绑定一路到最常见的坑和合库技巧按我能直接复现的顺序写。2. 开工前的环境准备驱动、编译选项与连接字符串2.1 驱动选型Jet 4.0 与 ACE 的差别以及 64 位这个最大的坑访问 MDB 文件Windows 上实际干活的是 ODBC 驱动。常见的有两个代际老的 Jet 4.0驱动名叫Microsoft Access Driver (*.mdb)随 Windows 系统自带但只有 32 位版本新的 ACE全称 Microsoft Access Database Engine驱动名是Microsoft Access Driver (*.mdb, *.accdb)微软官网有 x86 和 x64 两个安装包同时支持老 MDB 和新 ACCDB。这里有个绝大多数人第一次都会翻车的点64 位 Windows 上控制面板里打开ODBC 数据源管理器默认是 64 位视图C:\Windows\SysWOW64\odbcad32.exe才是 32 位视图。你在 64 位视图里看不到 Jet 驱动不代表它没装而是你看错了地方。反过来你写 64 位程序去连接一个只注册在 32 位视图里的 Jet 驱动ODBC 管理器直接报找不到数据源。驱动选型建议直接按下表决定不用纠结驱动位数支持是否系统自带典型连接串驱动名Jet 4.0仅 x86Windows 自带{Microsoft Access Driver (*.mdb)}ACEx86 / x64需下载安装{Microsoft Access Driver (*.mdb, *.accdb)}我一般这样定程序编译成 x86、数据文件确定是老 MDB那就直接用系统自带的 Jet 4.0部署最省事如果必须用 x64 编译比如主程序是 64 位、还要同时连 64 位 SQL Server那就去装 ACE 对应 x64 版本连接串里的驱动名换成带accdb那个。2.2 工程配置头文件、导入库与字符集Unicode 与 ANSI 怎么选VC 工程里用 ODBC 原生 API只需要三样东西头文件sql.h、sqlext.h导入库odbc32.lib。新建 Win32 控制台项目后在源文件里直接#include sql.h和#include sqlext.h链接器设置里加上odbc32.lib这是所有 ODBC 程序的地基不需要装任何 SDK 包。真正决定你能不能跑通的是字符集设置。Visual C 6.0 时代的工程默认 ANSIVS2008 之后的工程默认 Unicode。ODBC API 按字符集分了两套SQLExecDirectAANSI 版本和SQLExecDirectWUnicode 版本。MDB 的文本字段在 Jet 内部按 Unicode 存储读出来再转成 ANSI一旦代码页不匹配中文就是问号。我的建议是不管工程默认字符集是什么操作 MDB 统一走 W 版本 API。即SQLDriverConnectW、SQLExecDirectW、SQLGetDataW。理由就一条——少一次编码转换就少一类乱码问题。老项目升级工程文件后如果发现中文全乱先去项目属性 → 配置属性 → 常规 → 字符集看看是不是从使用多字节字符集被悄悄改成了 Unicode。2.3 连接方式DSN 与 DSN-Less 连接字符串的取舍ODBC 连 MDB 有两种方式。DSN 方式先在数据源管理器里注册一个数据源名程序里用SQLConnect按名字连DSN-Less 方式不注册任何东西直接通过连接字符串指定驱动和文件路径。两者的对比很清晰对比项DSN 方式DSN-Less 方式配置位置每台机器 ODBC 管理器注册写在代码里或配置文件里多机部署每台机器都要配烦改一行字符串即可换数据库文件路径改注册表或重配 DSN改连接串程序代码SQLConnectSQLDriverConnect部署角度我强烈建议 DSN-Less。给一个最常用的连接字符串模板Driver{Microsoft Access Driver (*.mdb)};DBQD:\data\test.mdb;ReadOnly0;Driver指定驱动名必须和系统里装的那个完全一致多一个空格都不行DBQ是数据库文件绝对路径注意反斜杠ReadOnly0表示可读写如果你只需要读数据改成ReadOnly1能减少文件锁冲突后面避坑章节会细说。3. 最小可运行代码连接、查询、释放一条龙3.1 连接初始化从环境句柄到语句句柄ODBC 的操作是分层句柄环境句柄 → 连接句柄 → 语句句柄。先分配环境设置 ODBC 版本再分配连接用连接字符串连上最后分配语句执行 SQL。下面这个例子是能直接编译的最小框架连上D:\data\test.mdb并执行一条 SELECT#include windows.h #include sql.h #include sqlext.h #include tchar.h int main() { SQLHENV hEnv SQL_NULL_HENV; SQLHDBC hDbc SQL_NULL_HDBC; SQLHSTMT hStmt SQL_NULL_HSTMT; SQLRETURN rc; // 1. 分配环境句柄并声明使用 ODBC 3.x 规范 rc SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, hEnv); rc SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0); // 2. 分配连接句柄 rc SQLAllocHandle(SQL_HANDLE_DBC, hEnv, hDbc); // 3. 用连接字符串连接 DSN-Less 数据源 SQLWCHAR szConn[] LDriver{Microsoft Access Driver (*.mdb)};DBQD:\\data\\test.mdb;ReadOnly0;; rc SQLDriverConnectW(hDbc, NULL, szConn, SQL_NTS, NULL, 0, NULL, SQL_DRIVER_NOPROMPT); // 4. 分配语句句柄 rc SQLAllocHandle(SQL_HANDLE_STMT, hDbc, hStmt); // 5. 执行查询Unicode 版本 SQLWCHAR szSql[] LSELECT id, name, amount FROM orders WHERE amount 100; rc SQLExecDirectW(hStmt, szSql, SQL_NTS); // 6. 释放语句句柄查询结果的读取在 3.2 节展开 if (hStmt ! SQL_NULL_HSTMT) SQLFreeHandle(SQL_HANDLE_STMT, hStmt); if (hDbc ! SQL_NULL_HDBC) SQLDisconnect(hDbc), SQLFreeHandle(SQL_HANDLE_DBC, hDbc); if (hEnv ! SQL_NULL_HENV) SQLFreeHandle(SQL_HANDLE_ENV, hEnv); return 0; }逻辑说明步骤 1 到 2 是固定套路SQL_ATTR_ODBC_VERSION必须设置否则后续行为不可预期步骤 3 用SQLDriverConnectW配合 DSN-Less 连接串SQL_DRIVER_NOPROMPT表示禁止弹出 ODBC 配置对话框程序跑在客户机上不会卡住等人点按钮步骤 4 的语句句柄是后续所有 SQL 操作的载体。释放顺序必须是 语句 → 连接 → 环境这个顺序来自 ODBC 规范逆序释放轻则返回错误重则在部分驱动下崩溃。3.2 执行 SELECT 并循环取数SQLFetch 与 SQLGetData 的配合上一节的代码执行完 SELECT 就停了没取数据等于白干。取数的套路是SQLFetch逐行移动游标SQLGetData按列取值// 接上面的 hStmt SQLWCHAR szName[256]; SQLLEN nLen1, nLen2; SQLINTEGER nId; SQLDOUBLE dAmount; // 循环取每一行 while (SQLFetch(hStmt) SQL_SUCCESS || SQLFetch(hStmt) SQL_SUCCESS_WITH_INFO) { // 第 1 列整型 id第 2 列文本 name第 3 列浮点 amount SQLGetData(hStmt, 1, SQL_C_SLONG, nId, 0, NULL); SQLGetData(hStmt, 2, SQL_C_WCHAR, szName, sizeof(szName), nLen1); SQLGetData(hStmt, 3, SQL_C_DOUBLE, dAmount, 0, NULL); // 刚取出来的数据直接用 wprintf(Lid%d name%s amount%.2f\n, nId, szName, dAmount); }参数说明SQLGetData的第二参数是列序号从 1 开始不是 0第三参数SQL_C_WCHAR表示按 Unicode 文本接收对应 C 语言的wchar_t第四参数是接收缓冲区地址第五参数是缓冲区字节数这里sizeof(szName)是 256 个 wchar 的字节数即 512 字节第六参数nLen1输出实际数据长度。两个容易踩的地方。第一SQLFetch的循环条件里我写了两次调用因为SQL_SUCCESS_WITH_INFO也是正常返回很多新手只判断 SQL_SUCCESS结果某些字段有截断警告时循环提前退出最后的行丢失。第二文本字段缓冲区给多大——MDB 的文本字段最大 255 字符给 256 个 wchar 正好但如果你读的是备注类型MEMO长度可能远超 255SQLGetData支持对一个列分多次取先给个 255 的缓冲区取第一部分再根据nLen1判断是否还有剩余。3.3 句柄释放顺序与常见资源泄漏资源泄漏的表现不是报错而是程序退出后 MDB 文件还被占用——下一个程序打开文件时提示文件正由另一用户使用。原因多半是语句句柄没释放导致连接没有真正关闭。释放顺序有讲究先SQLFreeHandle(SQL_HANDLE_STMT, hStmt)再SQLDisconnect(hDbc)断连接着SQLFreeHandle(SQL_HANDLE_DBC, hDbc)释放连接句柄最后SQLFreeHandle(SQL_HANDLE_ENV, hEnv)。每一步都判断返回值至少用assert(rc SQL_SUCCESS)兜底别图省事只释放环境句柄——ODBC 驱动管理器不保证这会连带释放下面的连接和语句。另外如果你的程序要连续打开多个 MDB 文件比如后面要讲的多库合并建议写一个CloseDB()函数对称封装连接逻辑。打开哪个文件就一定要关上哪个文件这是 MDB 操作里最土但最有效的防翻车手段。4. 写入与参数绑定从 INSERT 到防注入4.1 直接执行 UPDATE/INSERT/DELETE 的注意点连接和查询框架搭好之后写入操作就简单了——把 SQL 文本换掉即可。但 MDB 的 SQL 方言有三个坑直接拼字符串时务必注意。日期字面量必须用#包裹不是单引号。Access 的 SQL 里#2024-05-01#才是合法日期写成2024-05-01会被当成文本比较类型不匹配报错。布尔值在 Access 里存储为 -1真和 0假不是 C 语言里的 1 和 0。文本里的单引号要替换成两个单引号变成不是反斜杠转义——那是 MySQL 的写法在 Jet 引擎里无效。下面是一条带这些细节的 INSERTSQLWCHAR szSql[] LINSERT INTO users(name, birth, active) LVALUES(张三, #1985-03-12#, -1); rc SQLExecDirectW(hStmt, szSql, SQL_NTS); if (rc SQL_SUCCESS || rc SQL_SUCCESS_WITH_INFO) { SQLINTEGER nRow 0; SQLRowCount(hStmt, nRow); wprintf(L影响行数: %d\n, nRow); }SQLRowCount返回受影响行数DELETE 和 UPDATE 之后记得用这招确认有没有真的改到数据。Access 对并发锁比较敏感一条 UPDATE 如果影响了上千行文件可能被锁住几秒钟这种场景放到事务里处理见 4.3。4.2 用 SQLBindParameter 绑参数顺便解决 Access 注入手工拼字符串只要遇到用户输入就头皮发麻——用户名字里带个单引号整条 SQL 直接语法错误运气差点还能把整表数据带沟里。ODBC 处理这个的正解是参数绑定SQL 文本里写?占位符用SQLBindParameter把程序变量绑到占位符上数据由驱动负责转义。// 准备带占位符的 SQL SQLWCHAR szSql[] LINSERT INTO users(name, birth, active) VALUES(?, ?, ?); rc SQLPrepareW(hStmt, szSql, SQL_NTS); // 绑定参数名字、日期、布尔值 SQLWCHAR szName[64] LOBrien; SQLWCHAR szBirth[] L#1990-07-01#; SQLINTEGER nActive -1; SQLLEN nLenName SQL_NTS, nLenBirth SQL_NTS, nLenActive 0; SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_WCHAR, SQL_WCHAR, 64, 0, szName, sizeof(szName), nLenName); SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_WCHAR, SQL_WCHAR, 32, 0, szBirth, sizeof(szBirth), nLenBirth); SQLBindParameter(hStmt, 3, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, nActive, 0, nLenActive); rc SQLExecute(hStmt);参数说明SQLBindParameter的第四参数是参数缓冲区类型第五参数是 SQL 类型第六参数ColumnSize对文本类型表示最大字符数第七参数是小数位固定传 0。SQL_NTS表示字符串以空字符结尾让驱动自己算长度避免自己数错。这样写的好处有两个。第一用户输入OBrien这种含单引号的文本驱动会按正确规则转义不会破坏 SQL 语法Access 注入这条路被堵死大半第二参数类型由驱动自己转换日期和布尔值不用我手工拼#和-1传SQL_C_WCHAR的日期字符串进去即可。如果嫌SQLBindParameter参数太多记不住可以包一层宏但不要因此退回字符串拼接。4.3 关闭自动提交用事务保护批量写入ODBC 默认每执行一条 SQL 自动提交。如果程序要连续插入 500 条订单走到第 250 条时失败前面 250 条已经落库——这种半截数据在 Access 里清理起来非常痛苦。事务的作用就是让这批写入要么全成功要么全回滚。// 关闭自动提交进入手动提交模式 SQLSetConnectAttr(hDbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, 0); // 执行一批 INSERT ... // 全部成功后提交 SQLSetConnectAttr(hDbc, SQL_ATTR_COMMIT, (SQLPOINTER)SQL_COMMIT, 0); // 中途出错则回滚 // SQLSetConnectAttr(hDbc, SQL_ATTR_ROLLBACK, (SQLPOINTER)SQL_ROLLBACK, 0); // 恢复自动提交 SQLSetConnectAttr(hDbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, 0);注意 Access 的 Jet 引擎支持事务但锁粒度是页级长事务会让其他进程打不开文件。所以一批不要太大200 到 500 条提交一次即可。批处理完记得恢复自动提交否则下次连接复用同一个hDbc时所有 SQL 都在事务里忘了提交数据就丢了。5. 读 MDB 最容易翻车的 5 个点现象、原因、排查5.1 找不到驱动或数据源现象SQLDriverConnectW返回IM002或IM003错误程序报未找到数据源名称且未指定默认驱动程序。原因最常见是位数不匹配。64 位程序去找 32 位 Jet 驱动或者反过来其次是驱动名写错比如驱动名写成Microsoft Access Driver (*.mdb) Driver多了一个词。排查先确认自己编译出来的 exe 是 x86 还是 x64任务管理器里能看到再开对应位数的 odbcad32 看有没有对应驱动。C:\Windows\SysWOW64\odbcad32.exe是 32 位视图普通控制面板里的是 64 位视图。如果驱动名对着文档抄还不行把连接串里驱动名两边的大括号原样保留别删。5.2 MDB 文件被占用报文件正由另一用户使用现象连接串没问题驱动也对但SQLDriverConnect返回错误错误文本类似磁盘或网络错误同一时刻用 Access 打开这个文件却能成功。原因MDB 同时只允许一个进程以可写方式独占打开。Access、WPS、甚至上一个没释放句柄的 ODBC 程序都会占住文件。目录里如果出现同名.ldb文件就说明当前有进程锁着它。解决先改连接串加ReadOnly1;试试只读模式不容易锁文件如果必须写就把 MDB 先复制到临时目录再操作——反正你也要备份复制一份不亏。用 Process Explorer 搜文件句柄找到占用的进程 kill 掉这是最后的排查手段。5.3 中文读出来全是问号现象同一张表别人用管理工具看是正常中文你的程序读出来要么???要么乱码。原因字符集三处没对上。第一处是 ODBC API 版本SQLExecDirectA读 Unicode 字段必然有风险第二处是工程字符集设置VS 默认 Unicode 时SQLCHAR是单字节TCHAR是宽字符混用就乱第三处是源代码文件的编码UTF-8 源码被 ANSI 编译器按 GBK 解析字符串字面量早就是错的。解决统一走 W 版本 API即SQLExecDirectW、SQLGetDataW、SQLPrepareW。源代码文件用 VS 的文件 → 高级保存选项存成 UTF-8 with BOM避免编译器误解码。输出调试时用wprintf而不是printf。这三处对齐后中文基本不会再翻车。5.4 程序直接崩溃异常码 0xC0000005现象运行到SQLGetData附近进程崩溃Windows 事件查看器里的异常码是0xC0000005Access Violation也就是访问了不该访问的内存。原因八成是缓冲区问题。缓冲区分配得比数据短SQLGetData写越界或者SQLGetData的第六参数StrLen_or_IndPtr传了NULL驱动把长度写到空指针上还有一种常见情况是读到的字段是数值类型但按文本类型接收驱动解释数据时内部越界。解决每个SQLGetData检查返回码出现SQL_SUCCESS_WITH_INFO说明有截断要么加大缓冲区要么分多次取。StrLen_or_IndPtr参数别偷懒传NULL定义一个SQLLEN nLen传地址过去。文本缓冲区按字节数分配读 wchar 时sizeof算的是字节别按字符数填。5.5 SQL 拼接报语法错误特殊字符把语句毁了现象程序里用户名合法但拼进 SQL 后执行报-3100附近的关键字符号错误。原因文本里有单引号。Access 的 SQL 解析器把单引号当成字符串定界符用户输入OBrien拼进去后变成OBrien解析器看到O就认为字符串结束了后面全是非法语法。解决首选 4.2 节的SQLBindParameter参数绑定一劳永逸。如果临时改代码必须把单引号替换成两个单引号str.Replace(_T(), _T())。注意别用反斜杠转义Jet 引擎不认这是从 MySQL 转过来的人最容易犯的习惯。6. 进阶玩法把多个 MDB 合并成一个并验证数据没丢6.1 用 Jet 的跨库 IN 子句快速抽数Jet 引擎支持一种不常见的跨库语法SELECT ... INTO ... FROM ... IN 文件路径。不需要预先连接源库直接在 SQL 文本里写路径适合把分散在各分店的 MDB 快速抽到总库里。比如把D:\data\2023.mdb里的orders表原样复制到当前连接的库中SELECT * INTO orders_2023 FROM orders IN D:\data\2023.mdb这条语句在目标库当前连接创建新表orders_2023数据从源文件读入。注意两点源表里的自动编号字段自增主键复制后不会保留自增属性只是普通整型如果你还指望它继续自增合并后要手工重建新旧表字段名和类型不对齐时SELECT *可能会报错稳妥做法是显式列出字段。6.2 多 MDB 合并的脚本框架跨库IN子句适合单表同步要合并整个目录下所有 MDB 的每张表还得靠程序循环。框架用第 3 章的连接逻辑封装两个函数OpenDB(path)和CloseDB()对每个文件先连接遍历表结构目标库建表再逐行拷贝。核心骨架如下// 伪代码级别的循环框架 WIN32_FIND_DATAA fd; HANDLE hFind FindFirstFileA(D:\\data\\*.mdb, fd); do { char szSrc[MAX_PATH]; sprintf_s(szSrc, D:\\data\\%s, fd.cFileName); OpenDB(szSrc); // 连接源库只读方式 LoadAllTables(); // 读取表名清单 for (each table) { CreateTargetTable(); // 目标库建表用 SELECT * INTO CopyRows(); // 逐行或分批 INSERT } CloseDB(); } while (FindNextFileA(hFind, fd));逻辑说明源库一律ReadOnly1连接避免拷贝时被源文件锁困扰建表用SELECT * INTO tmp_xxx FROM tbl最快但字段类型最省事逐行拷贝时为每个字段按序号做SQLGetData到变体缓冲再SQLBindParameter插进目标库文本字段缓冲区统一给 1024 字节备注字段分两段取。6.3 收尾验证与发布运行库合库之后立刻验证别急着把源库删了。每张表在源库和目标库分别SELECT COUNT(*)对比行数随机抽出 20 行逐字段比对重点看中文文本、日期、浮点精度验证通过后把目标库复制一份用管理工具或另一个 ODBC 程序重新打开一遍确认文件没被写坏。发布给客户时记得带上你编译用的那个 VC 运行库——VS2015 到 2022 编译的程序目标机器上缺vcruntime140.dll就起不来客户会以为你的程序写坏了。要么装对应的 Visual C Redistributable 包要么把 DLL 和 exe 放一起。我自己维护导数据工具几年最深的教训就是每次合库前先复制一份.bakMDB 一旦写坏没有后悔药。希望帮到你。本文还有配套的精品资源点击获取