简介这是给C与微软基础类库MFC开发者的数据库连接实践资料围绕在Windows应用中通过开放数据库连接ODBC访问MySQL数据库这一核心场景系统讲解驱动安装、数据源建立、连接管理、结果集操作、SQL执行、异常处理与事务控制等关键知识点帮助读者从环境准备到代码实现建立完整认知。资源包为完整的DatabaseTest工程共38个文件压缩包大小3.54MB。包内包括头文件与实现文件、对话框与图标资源、可执行程序以及工程配置与调试辅助文件类型覆盖源码、界面资源、构建产物与配置信息目录结构清晰便于按模块查阅和复用。目前已有1664人学习内容适合MFC初级至中级水平的开发者。通过学习该工程可以掌握MFC中CDatabase与CRecordset的实际用法了解数据库增删改查功能的完整编码流程并借鉴异常捕获、事务回滚、连接关闭等健壮性设计经验。对于正在学习MFC数据库编程或计划在项目中使用MySQL的读者是一份可以对照实践的参考案例。1. MFC 连 MySQL先想清楚走哪条路再动手MFC连接MySQL数据库指在微软基础类库的Windows桌面程序里通过客户端库或中间驱动把界面操作落到远端的MySQL服务上。做这个标题的人大多是手里有个维护中的老工程要加数据能力或者正在选型C桌面端的数据层方案。这件事的原理并不深真正费功夫的是库的位数、字符集、运行库一致性这些外围环节。常见的路线有两条用ADO或ODBC走微软数据访问栈或者直接用官方C API。我更倾向后者。它能让你清楚每个调用做了什么出错时也能直接拿到MySQL侧的错误码排查起来更直接。2. 开工前先备料选型理由与 MFC 工程的三个配置点2.1 为什么我优先选 MySQL C API而不是 ADO 或 ODBC在MFC工程里连MySQL绕不开两条主流路线。第一条是ADO通过OLE DB Provider for MySQL连接优点是不用额外装客户端开发库工程里加个#import引用就行但微软早已不再维护这个Provider新版MySQL驱动对老接口的兼容全靠社区维护一旦遇到认证插件变化排查会很痛苦。第二条是ODBCMySQL提供了官方Connector/ODBC但ODBC的接口模型和C之间隔着一层取结果集、绑定参数都绕不开SQLHANDLE那一套代码量不小。我一般会直接用官方C API也就是Connector/C头文件mysql.h链接libmysql.lib。理由有三条。第一它没有额外的驱动服务进程只是一个库跟着exe部署就行。第二错误信息完整mysql_errno和mysql_error能直接拿到MySQL服务端的错误码和文本比ODBC的SQLGetDiagRec直观得多。第三它天然支持预处理语句、MySQL专属类型和函数不需要通过SQL方言兼容层绕。如果你的项目未来还要在Linux上用同一个数据层逻辑C API的代码可以直接跨平台编译ADO那套就被绑死在Windows上了。2.2 头文件、lib 与 dll版本、位数与字符集必须对齐选了C API之后第一件事不是在代码里写include而是把开发环境配对。常见的翻车现场是能编过但链接失败或者编译链接都过了程序一启动就报缺少DLL。这些问题的根源都是版本、位数、字符集三个维度没有对齐。先说版本。现在常用的MySQL 8.0之后客户端库按大版本区分头文件和库的命名基本都是mysql.h和libmysql.lib。如果你手里的工程是更早的5.x时代代码建议还是装对应版本的Connector不要拿8.0的头文件去编译老代码因为认证握手协议变了客户端与服务端版本跨度太大时mysql_real_connect会报认证插件无法加载一类的错误。我遇到过一台服务器只支持旧密码协议新客户端连上去直接握手失败最后只能换旧版Connector绕过。再说位数。Connector的库分32位和64位这个必须和你的exe目标平台一致。很多人在Visual Studio里默认用Win32平台却装了64位的Connector链接时必然报LNK2019。这在项目属性里的配置管理器看平台就清楚了别等到链接报错了才反应。然后是字符集MFC在Unicode编译下CString是UTF-16MySQL默认是UTF-8这个转换链路后文会专门说但环境配置阶段就确认好编码方向能省掉后面大量乱码问题。然后是工程配置。在Visual Studio里右键项目属性定位到VC目录包含目录添加MySQL安装目录下的include文件夹例如C:\Program Files\MySQL\MySQL Server 8.0\include库目录添加lib文件夹例如C:\Program Files\MySQL\MySQL Server 8.0\lib再到链接器输入里追加libmysql.lib。这样配置后编译和链接基本都能过。但这里的坑在于程序运行时还需要一个libmysql.dll。Visual Studio不会自动把DLL复制到输出目录最简单的方式是把DLL复制到exe同目录或者放到系统PATH能找到的位置。我一般会设置一个生成后事件把dll从MySQL的lib目录复制到$(OutDir)这样每次重新生成都不用手动去拖文件。注意Debug和Release要分别确认这三个配置因为VC目录和附加依赖项是按配置分开保存的改了一个配置另一个还是老样子这种问题在接手别人的工程时最容易碰到。2.3 用一段空工程验证配置编译通过才算迈出第一步配置完之后先别急着写业务代码在对话框工程里放一个最简单的调用目标是编译链接通过。#include stdafx.h #include mysql.h void CMainDlg::TestConnection() { MYSQL* pConn mysql_init(nullptr); if (pConn) { mysql_close(pConn); } }这段代码本身不连库只测试头文件、库文件、DLL三个环节是否就位。如果编译报错找不到mysql.h回去检查包含目录如果链接报LNK2019无法解析mysql_init回去检查附加依赖项如果编译链接都过了但启动时弹窗说找不到libmysql.dll那就是DLL路径问题。通过这一步你才算把MFC连接MySQL的环境地基打好后面所有代码都在这个基础上加。3. 用 C API 在 MFC 里跑通第一个真实连接代码与参数说明3.1 最小连接代码初始化、连接、错误处理一个都不能少环境就绪后接下来写真正建立连接的代码。下面这段取自对话框类的一个按钮处理函数是最小的能跑通流程的版本。我用_T()宏处理字符串兼顾Unicode和ANSI编译但传给MySQL的SQL和参数必须显式转成UTF-8这点在后面的字符集部分再展开。void CMainDlg::OnBnClickedBtnConnect() { MYSQL* pConn mysql_init(nullptr); if (!pConn) { AfxMessageBox(_T(mysql_init 分配句柄失败)); return; } // 设置连接超时避免网络不通时界面卡死 unsigned int nTimeout 3; mysql_options(pConn, MYSQL_OPT_CONNECT_TIMEOUT, nTimeout); if (!mysql_real_connect(pConn, 127.0.0.1, root, 123456, demo_db, 3306, nullptr, 0)) { CString strErr; strErr.Format(_T(连接失败[%u]: %s), mysql_errno(pConn), CA2T(mysql_error(pConn))); AfxMessageBox(strErr); mysql_close(pConn); return; } // 连接成功先做字符集设置后面所有SQL都按UTF-8处理 mysql_set_character_set(pConn, utf8mb4); // 业务操作在这里展开 mysql_close(pConn); }这段代码的逻辑顺序很重要。先用mysql_init创建句柄然后用mysql_options设置连接超时再用mysql_real_connect真正建立TCP连接失败时用mysql_errno与mysql_error取错误信息。我特别建议在连接之前就设置MYSQL_OPT_CONNECT_TIMEOUT不设的话默认行为是让系统网络栈决定在某些不通的网段里能卡十几秒界面直接无响应用户会以为程序死了。3.2 六个参数的填法与两个标志位的取舍mysql_real_connect一共八个参数日常用到最多的前六个值得逐一说明参数含义我一般怎么填host服务端IP或主机名内网固定IP比主机名稳避免DNS解析干扰user用户名用最小权限账号别拿root跑业务password密码建议从配置文件读取不写死在代码db默认数据库可为nullptr通过USE语句切换portTCP端口默认3306unix_socketUnix域套接字Windows下固定填nullptr第七个参数是client_flag类型是unsigned long用来开启协议层面的行为。最常见的是CLIENT_MULTI_STATEMENTS它允许一条SQL里用分号分隔多条语句但不是每个场景都需要因为它同时增加了SQL注入的风险面如果你的代码里没有批量执行多条语句的需求保持0即可。unsigned long nFlags CLIENT_MULTI_STATEMENTS; mysql_real_connect(pConn, 127.0.0.1, user, pass, db, 3306, nullptr, nFlags);区分一下CLIENT_MULTI_STATEMENTS影响的是mysql_query执行的语义CLIENT_MULTI_RESULTS则用于存储过程返回多个结果集。MFC的对话框程序如果只需要单表查询这两个标志都可以不打开把复杂度留在外面。3.3 连接后的三个检查ping、字符集与状态确认连接建立不等于万事大吉。很多线上问题出在“连接成功过后来断了但代码不知道”。MFC的界面线程往往阻塞在等待用户操作上MySQL服务端默认的wait_timeout是8小时一个连接挂在那里超过8小时不动服务端会主动断开。下一次界面发SQLmysql_query可能直接返回错误2006MySQL server has gone away。所以我的习惯是每次执行SQL之前先调用mysql_ping。if (mysql_ping(pConn) ! 0) { AfxMessageBox(_T(数据库连接已断开请重新连接)); return; }mysql_ping会自动做一次轻量往返确认连接状态如果服务端断开了它不会自动重连。如果你期望自动重连可以选择在代码里检测到ping失败后重建连接。我更推荐显式重建连接因为自动重连时事务状态和临时表都会丢失业务结果不可预期。另外一个容易忽略的是mysql_set_character_set。它设置的是客户端连接层面的字符集等于在握手后发送一条SET NAMES utf8mb4。如果跳过这步客户端默认字符集可能是latin1中文写入再读回来就全乱了。后面避坑章节讲中文乱码时会再回到这里。4. 在 MFC 对话框中落地增删改查封装、列表映射与事务边界4.1 先把连接和 SQL 封装成工具类别让界面代码裸奔第一次连上MySQL后开发者最容易犯的错误是把所有数据库操作直接写进按钮的OnBnClicked函数。一个真实的MFC业务窗口可能同时有查询列表、保存、删除、刷新十几个消息处理函数每个都写一遍mysql_query和错误处理代码会迅速膨胀到没法维护。我建议在最开始就用一个C类封装连接和SQL执行。class CDbHelper { public: CDbHelper(); ~CDbHelper(); BOOL Connect(const CString strHost, int nPort, const CString strUser, const CString strPwd, const CString strDb); void Close(); BOOL ExecuteSQL(const CString strSql); MYSQL_RES* Query(const CString strSql); BOOL EnsureConnected(); private: MYSQL m_mysql; BOOL m_bConnected; };这里有个细节值得说MYSQL结构体可以用栈变量mysql_init(m_mysql)就地初始化而不是每次new一个。这样做的好处是析构函数里只要判断是否已连接再mysql_close就行生命周期和对象绑定不会内存泄漏。有人习惯用MYSQL*指针加new这时要记得在析构里判断非空再关闭。构造函数里初始化时我会显式关闭自动重连因为MYSQL_OPT_RECONNECT在8.0里默认是关的但有些打包的老版本默认开行为不可预期。CDbHelper::CDbHelper() { m_bConnected FALSE; mysql_init(m_mysql); bool bReconnect false; mysql_options(m_mysql, MYSQL_OPT_RECONNECT, bReconnect); } CDbHelper::~CDbHelper() { if (m_bConnected) { mysql_close(m_mysql); m_bConnected FALSE; } }4.2 查询结果映射到 CListCtrl类型转换与内存释放MFC最常用的是CListCtrl。从MySQL取出来的结果集是MYSQL_ROW每一列都是char*。要把它们填进列表控件必须先做字符集转换和控件操作。下面是一个把users表查出来塞进列表的函数。BOOL CDbHelper::LoadUsers(CListCtrl listCtrl, const CString strWhere) { if (!EnsureConnected()) return FALSE; // 拼SQL时注意防注入简单示例用拼接实际建议用预处理语句 CString strSql; strSql.Format(_T(SELECT id, user_name, age, create_time FROM users WHERE %s), strWhere); // CString转UTF-8注意用GetLength取的是字节数而不是字符数 CStringA strSqlUtf8 CT2A(strSql, CP_UTF8); if (mysql_real_query(m_mysql, strSqlUtf8, (unsigned long)strSqlUtf8.GetLength()) ! 0) { // 记录mysql_error这里省略日志代码 return FALSE; } MYSQL_RES* pResult mysql_store_result(m_mysql); if (!pResult) { if (mysql_field_count(m_mysql) 0) { // 说明不是SELECT语句无结果集按成功处理 return TRUE; } return FALSE; } listCtrl.DeleteAllItems(); MYSQL_ROW row; while ((row mysql_fetch_row(pResult)) ! nullptr) { int nRow listCtrl.InsertItem(listCtrl.GetItemCount(), CA2T(row[0])); listCtrl.SetItemText(nRow, 1, CA2T(row[1])); listCtrl.SetItemText(nRow, 2, CA2T(row[2])); listCtrl.SetItemText(nRow, 3, CA2T(row[3])); } mysql_free_result(pResult); return TRUE; }这代码里有几个关键点。第一是mysql_real_query与mysql_query的区别mysql_real_query带长度参数SQL里含二进制数据时更安全MFC工程里我习惯用前者因为CStringA的GetLength()能拿到准确的UTF-8字节数。很多人第一次写会直接传strSql.GetLength()在Unicode工程里这个值是宽字符数中文SQL一多就截断这是典型的字符集翻车点。第二是mysql_store_result与mysql_use_result的取舍。store_result是一次性把所有结果拉到客户端内存占用大但取数快且可以随时用mysql_num_rows知道行数。use_result是流式逐行取内存省但处理期间不能对同一连接执行其他SQL容易踩“结果没取完就发新查询”的坑。在MFC的普通列表加载场景数据量几千行store_result是最稳的选择。第三是结果集用完之后必须mysql_free_result。这个API对应的内存由客户端库管理如果你在循环里提前return而忘了释放每次查询都会泄漏。我习惯把查询结果和释放写在同一个函数里不要在多个函数之间传递MYSQL_RES*指针减少遗忘。4.3 写操作与事务边界autocommit 的取舍MFC程序里的写操作比查询更容易出问题因为它涉及事务边界。MySQL默认autocommit1每条语句立刻提交这对单条INSERT是没问题的但遇到批量更新或先删后插这种需要原子性的场景就必须自己开事务。BOOL CDbHelper::ReplaceUser(const CString strOldName, const CString strNewName) { if (!EnsureConnected()) return FALSE; // 关闭自动提交开启事务 if (mysql_autocommit(m_mysql, 0) ! 0) { return FALSE; } CString strSql; strSql.Format(_T(UPDATE users SET user_name%s WHERE user_name%s), strNewName, strOldName); CStringA strSqlUtf8 CT2A(strSql, CP_UTF8); BOOL bOK (mysql_real_query(m_mysql, strSqlUtf8, (unsigned long)strSqlUtf8.GetLength()) 0); if (bOK) { if (mysql_commit(m_mysql) ! 0) { bOK FALSE; } } if (!bOK) { mysql_rollback(m_mysql); } // 恢复自动提交避免影响后续连接上的其他逻辑 mysql_autocommit(m_mysql, 1); return bOK; }事务处理的三个关键点是mysql_autocommit把模式关掉所有失败分支都走到rollback无论成功失败最后都要恢复autocommit默认值。如果连接是长连接、被多个窗口共用忘记恢复会导致后续单条INSERT不生效。这是MFC长生命周期窗口特有的坑代码里稍微绕一下就很容易漏。5. MFC 连接 MySQL 的避坑清单五个典型问题的现象、原因与处置5.1 现象Debug 能编译 Release 报链接错这个问题我接过不止一次。现象是Debug配置下编译链接一路畅通切到Release就报LNK2019或者反过来。根本原因往往是Release配置里没有附加依赖项或者引用库的路径和Debug不一致。MFC工程的属性页里Debug和Release的附加依赖项是独立保存的很多人只在Debug里配置过一次Release那边就空白了。解决方式把第2章里讲到的包含目录、库目录、附加依赖项在Release配置里完整重复一遍。也可以把这份配置导出成PropertySheet文件两个配置都导入以后换机器重配环境不用重新手敲。这类问题的排查路径最快是打开项目属性逐个配置项对照先把配置下拉框从Debug改成Release不需要去看代码。5.2 现象中文写入数据库变成问号读回来也是乱码这是MFC连接MySQL最普遍的中文乱码问题。本质是字符集链路某一环没有对齐。MFC的CString在Unicode编译下是UTF-16MySQL服务端通常是utf8mb4客户端连接默认可能是latin1。数据从CString到MySQL要经过UTF-16转UTF-8再转服务端存储任何一步不对都会乱码。解决的完整链路是连接后立刻调用mysql_set_character_set(pConn, utf8mb4)所有传进mysql_real_query的SQL字符串都先用CT2A转成UTF-8建表时指定DEFAULT CHARSETutf8mb4。有一个我踩过的细节mysql_set_character_set的第二个参数是const char*在MFC工程里如果你直接写_T(utf8mb4)编译出来的可能是宽字符指针函数内部按char读取就会得到错误字符集名。正确写法是传窄字符字面量utf8mb4。5.3 现象程序启动报 0xc000007b无法正常进入界面这个错误码常见于32位exe配了64位DLL或者反过来。现象是编译链接都正常双击exe直接弹“应用程序无法正常启动”。这是因为Windows加载器加载libmysql.dll时发现PE格式的位数和exe不一致映射失败。解决最直接的是把exe目标平台切到x64在配置管理器里新建x64平台然后把包含目录、库目录、附加依赖项重新配置一份。如果项目里依赖的第三方库只有32位版本那就反过来装32位的Connector下载时选Win32版本。这个现象最坑的地方是它不报具体错误打开事件查看器看到的是模块加载失败排查方向很容易跑偏到运行库上。5.4 现象mysql_init 返回空指针但系统内存资源明明充足mysql_init返回nullptr最直接的原因通常是内存分配失败但在MFC工程里还有一种特定情况初始化发生在DLL的模块边界之外或者工程同时链接了不同版本的运行时导致mysql_init内部的内存分配器和你工程的堆不一致。多见于把数据库操作封装在某个自己写的DLL里而该DLL与主exe使用不同运行库模式。解决思路是让封装数据库的模块和主exe使用相同的运行库模式。项目属性里代码生成的运行库选项Debug用多线程调试DLLRelease用多线程DLL关键是两端一致。另一个稳妥方案是mysql_init(nullptr)让库内部自己分配但释放时也要对应mysql_close(ptr)不要混用。5.5 现象运行几小时后报 2006 MySQL server has gone away这个错误码经典且高频。原因是服务端或网络设备把空闲连接断开了MySQL默认wait_timeout是8小时如果MFC程序界面挂在后台不动长连接超过阈值就被服务端回收。另一个常见触发条件是网络环境里防火墙对空闲TCP连接做了清理。解决业务层每次使用前调用mysql_ping检查发现断开就重建连接。想省事的话可以在连接时打开MYSQL_OPT_RECONNECT但重连后临时表、用户变量、事务状态全部丢失我一般不用。更推荐在工具类里做惰性重连执行SQL前ping失败则自动调用Connect重新建连然后重试一次SQL。注意重试次数要有限制否则在数据库宕机时会拖住界面线程。6. 最后一步把连接工具类改成带自动重连的 RAII 封装前面所有代码已经能让MFC程序跑通连接和增删改查但离“能用几个月不出问题”还差一层。我把CDbHelper在持续维护中慢慢改成了下面这个风格构造时不连接显式调Connect每次执行SQL前自动检查连接状态析构时无论处于什么状态都能安全关闭。这样把前面说的2006问题、内存泄漏问题都压进了类内部。BOOL CDbHelper::EnsureConnected() { if (m_bConnected mysql_ping(m_mysql) 0) { return TRUE; } return Reconnect(); } BOOL CDbHelper::Reconnect() { if (m_bConnected) { mysql_close(m_mysql); m_bConnected FALSE; } mysql_init(m_mysql); // 重新设置超时 unsigned int nTimeout 3; mysql_options(m_mysql, MYSQL_OPT_CONNECT_TIMEOUT, nTimeout); // 这里用成员变量保存的连接参数 if (!mysql_real_connect(m_mysql, m_strHost, m_strUser, m_strPwd, m_strDb, m_nPort, nullptr, 0)) { return FALSE; } m_bConnected TRUE; mysql_set_character_set(m_mysql, utf8mb4); return TRUE; }用这个类的时候查询函数的第一步是EnsureConnected失败就让界面提示数据库连接失效、检查网络或服务状态不要自动无限重试。我自己的习惯是每个数据库操作函数开头一行EnsureConnected中间是SQL逻辑结尾确保结果集释放。一开始我也觉得每次操作都检查多余的直到某次演示时直接弹出服务器没开的提示才意识到这种防御是必要的。做MFC的老人常说数据库这块最怕的不是复杂SQL而是连接状态没人管。这个工具类跑顺了后面接手的人也能很快上手。希望帮到你。本文还有配套的精品资源点击获取