MFC和MySQL这个组合,听起来确实有点“复古”,但直到今天,我身边仍然有一大批内部工具、工装配套软件、实验室记录程序是用MFC对话框写的,后端数据清一色落在MySQL里。我在VS2019下把一个MySQL 8.0.25数据库接进现成的MFC工程,并从零实现了“点按钮、把编辑框里的数据写进数据库表”这个最基础也最高频的需求。整个过程不算复杂,但踩了几个不大不小的坑,包括环境依赖、DLL位数、字符集乱码、超时断连这些。这篇文章就把从环境配置到代码落地的完整链路写出来,给同样在MFC和MySQL之间反复折腾的人一条可以直接照抄的路。1. 为什么绕开ODBC和ADO,直接选MySQL C API这条路很多人一听到MFC连数据库,第一反应就是ODBC,因为老教程里全是CDatabase、CRecordset那套东西。但真把ODBC用到MySQL上,麻烦事一点不少。MySQL 8.0默认的认证插件是caching_sha2_password,如果ODBC驱动版本不够新,或者MFC程序加载的ODBC驱动和系统里装的不一致,连接时会直接报2059错误。再加上ODBC数据源本身还有32位和64位之分,控制面板里配的DSN和程序里实际加载的DSN很可能不是同一份文件,排查起来很痛苦。1.1 三种连接方式的现实对比我简单整理一下几种常见方案的实际体验:方案上手难度维护成本常见坑ODBC中等偏高驱动版本、DSN位数、认证插件兼容性ADO中等偏高依赖OLE DB Provider,官方维护力度弱MySQL C API偏低低DLL位数、字符集,但报错信息直接ADO我也试过,连接字符串写起来简单,但本质上它需要一个可用的OLE DB Provider,而MySQL官方的OLE DB提供程序更新节奏很慢。对于MFC这种老技术栈来说,能用稳定方案就不去赌冷门依托。1.2 直接链接libmysql库的好处我最终的落地方案是直接调用MySQL官方提供的C API,也就是链接libmysql.lib,运行时加载libmysql.dll。这套接口MySQL自己一直在维护,8.0.25的Server配套的库文件能完整实现新版握手协议,不需要额外装驱动。代码量看起来比ODBC多一点点,因为每一步都摆在明面上:初始化、设置参数、连接、执行、关闭,但这也意味着出问题时,错误码直接来自MySQL服务器,排查链路变得非常透明。本文后面所有的示例,都是围绕一个实际的MFC对话框程序来说的:界面上有“姓名”“电话”“备注”三个编辑框,还有一个“保存到数据库”按钮,按钮点了之后把内容插入到一张客户登记表里。先建好数据库和表:CREATE DATABASE my_app_db DEFAULT CHARACTER SET utf8mb4; USE my_app_db; CREATE TABLE customer_info ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, phone VARCHAR(20), remark TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );之所以把表字符集定为utf8mb4,是因为目前MySQL 8.0的默认字符集就是它,能完整存下中文和emoji表情。后面代码里涉及字符编码的部分,也都是围绕这个前提处理的。2. VS2019工程里把MySQL的依赖配置妥当这一步看起来不起眼,实际上翻车率极高。很多人的情况是:代码写得非常顺利,结果编译阶段报了一堆无法解析的外部符号,或者一运行就弹0xc000007b。问题根源几乎都集中在依赖没配好,或者位数对不上。2.1 安装MySQL时要包含开发组件如果你装MySQL Server时一路Next到底,很可能在安装目录里根本找不到include和lib文件夹。默认安装路径一般是C:\Program Files\MySQL\MySQL Server 8.0\,只有在安装器选择组件时勾选了“C/C++头文件和库”(Development Components),才会生成include和lib目录。如果当时没勾选,也不用重装整个数据库。打开MySQL Installer,选择“Add”把Development Components补装上就行,这一步只是往磁盘里补文件,不会动现有的数据。2.2 属性页三件套:包含目录、库目录、附加依赖项打开工程属性,重点检查三个位置:第一个,C/C++ -> 常规 -> 附加包含目录,添加:C:\Program Files\MySQL\MySQL Server 8.0\include第二个,链接器 -> 常规 -> 附加库目录,添加:C:\Program Files\MySQL\MySQL Server 8.0\lib第三个,链接器 -> 输入 -> 附加依赖项,填写:libmysql.lib这里有一个特别容易忽略的地方:MySQL 8.0的Server本身只有64位版本,所以官方提供的libmysql.lib和libmysql.dll也都是64位的。VS2019新建MFC工程时,默认的活动解决方案平台其实是Win32,如果你直接用默认平台去链接64位的库,会报一堆机器类型不匹配的错误。解决办法是在配置管理器里新建一个x64平台,然后整个工程用x64编译。这个操作必须在配置依赖之前做,否则后面所有路径都白配了。2.3 把libmysql.dll放到exe能看见的地方静态链接解决的是编译期问题,运行期还需要libmysql.dll。MySQL安装到系统后,这个DLL是不在C:\Windows\System32里的,需要你自己把它放到生成的exe同目录,或者复制到工程输出目录。我比较推荐在VS工程里配置一个“生成后事件”,每次都自动把DLL拷到输出目录。打开生成事件 -> 生成后事件 -> 命令行,加一行:xcopy /Y "C:\Program Files\MySQL\MySQL Server 8.0\lib\libmysql.dll" "$(OutDir)"这样每次编译完,DLL都会自动跟着刷新,省去手动拷贝的麻烦,也避免exe更新了但DLL还是旧版本的诡异问题。3. 从mysql_init到mysql_real_connect:把连接代码写进MFC按钮里基础依赖配好之后,接下来就是真刀真枪写连接代码。我在对话框上放了一个“测试连接”按钮,一个“保存数据”按钮,还有一个MYSQL*成员变量挂在对话框类上:// MainDlg.h class CMainDlg : public CDialogEx { public: MYSQL* m_pConn; void CloseConnection(); ... };构造函数里把m_pConn初始化成nullptr,接下来在“测试连接”按钮的响应函数里完成整个连接流程。3.1 初始化、设置连接选项、真正建立连接void CMainDlg::OnBnClickedBtnConnect() { // 防止上一次没关干净 if (m_pConn != nullptr) { mysql_close(m_pConn); m_pConn = nullptr; } m_pConn = mysql_init(nullptr); if (m_pConn == nullptr) { AfxMessageBox(_T("mysql_init 返回 NULL,初始化失败")); return; } // 统一走 utf8mb4,避免乱码 mysql_options(m_pConn, MYSQL_SET_CHARSET_NAME, "utf8mb4"); // 连接超时设短一点,免得服务器没启时界面卡死在那 unsigned int timeout = 5; mysql_options(m_pConn, MYSQL_OPT_CONNECT_TIMEOUT, &timeout); MYSQL* ret = mysql_real_connect( m_pConn, "localhost", // 服务器地址 "root", // 用户名 "your_password", // 密码,开发阶段先写死 "my_app_db", // 数据库名 3306, // 端口 nullptr, // Unix socket,Windows下传nullptr 0 // client_flag,这里用0即可 ); if (ret == nullptr) { CString msg; msg.Format(_T("连接失败,错误码:%d(%s)"), mysql_errno(m_pConn), CA2T(mysql_error(m_pConn))); AfxMessageBox(msg); mysql_close(m_pConn); m_pConn = nullptr; return; } AfxMessageBox(_T("连接成功")); }几个参数说明一下。mysql_init(nullptr)会分配并初始化一个MYSQL对象,返回值是nullptr说明连内存分配都失败了,这种极端情况也得处理。mysql_real_connect的最后一个参数client_flag平时传0就够,没必要去翻后面的位标志,除非你明确要开多语句、长连接之类的特性。3.2 为什么把连接放在按钮回调而不是OnInitDialog里我最初也习惯在对话框初始化时连接数据库,后来发现体验很差:如果MySQL服务没启动,程序一打开就弹错误框,用户连主界面都看不着。而且mysql_real_connect如果碰到网络超时,可能阻塞好几秒,这段时间窗口卡死,移动不了,体验非常糟。现在我的习惯是:启动时不主动连接,等用户点按钮时再连;或者程序刚启动时只创建连接对象,真正用到数据库时再建立连接。对一个小工具来说,这种“懒连接”策略更符合日常使用习惯。3.3 关闭连接的统一出口任何地方关闭连接,我都只走同一个函数:void CMainDlg::CloseConnection() { if (m_pConn != nullptr) { mysql_close(m_pConn); m_pConn = nullptr; } }在OnDestroy里也记得调用一下,防止用户直接关窗口时连接还开着,导致句柄泄漏。4. 添加数据:一条INSERT背后的四个细节连接建立了,接下来就是核心的“添加数据”功能。表面上就是拼一条SQL然后执行,但实际操作中有四个细节不处理到位,后面会有各种匪夷所思的报错。4.1 从CString到UTF-8字符串的转换MFC工程如果用的是Unicode字符集,CString里存的是UTF-16宽字符。而MySQL的C API接口全部是char*窄字符,所以需要把编辑框内容转换成UTF-8编码的char*才能传给MySQL。CString strName; GetDlgItemText(IDC_EDIT_NAME, strName); // CW2A 把宽字符转成指定代码页的窄字符 CStringA utf8Name = CW2A(strName, CP_UTF8);这一步是中文乱码的根源之一。很多人直接把CString塞给MySQL,结果存进去的中文变成一堆问号或乱码,就是因为在宽窄字符转换这一步漏了代码页指定。MySQL那边是utf8mb4,这边的字符串也必须是UTF-8,两边对齐了才不会有编码冲突。4.2 mysql_real_escape_string:用户输入不能直接拼SQL界面上用户输的内容是什么,完全不可控。如果直接拼进SQL字符串,一个单引号就能让你的SQL提前结束,严重的话会把整条SQL的语义改掉。这就是经典的SQL注入风险。正确做法是先转义再拼接:char escapedName[256] = {0}; char escapedPhone[64] = {0}; char escapedRemark[1024] = {0}; unsigned long utf8NameLen = (unsigned long)strlen(utf8Name); mysql_real_escape_string(m_pConn, escapedName, utf8Name, utf8NameLen); // phone、remark 同理,先把 CString 转成 UTF-8,再调用 mysql_real_escape_stringmysql_real_escape_string会把单引号、双引号、反斜杠、换行等特殊字符转义成安全形式,目标缓冲区大小按源字符串长度的两倍再加1来分配就够了。注意它的返回值是转义后的字符串长度,如果后面要用这个长度,记得存下来。4.3 执行INSERT并处理错误码转义完成后,拼SQL并执行:char sql[1024] = {0}; sprintf_s(sql, "INSERT INTO customer_info(name, phone, remark) VALUES('%s', '%s', '%s')", escapedName, escapedPhone, escapedRemark); if (mysql_query(m_pConn, sql) != 0) { CString msg; msg.Format(_T("插入失败,错误码:%d(%s)"), mysql_errno(m_pConn), CA2T(mysql_error(m_pConn))); AfxMessageBox(msg); } else { int newId = (int)mysql_insert_id(m_pConn); CString msg; msg.Format(_T("保存成功,新记录ID:%d"), newId); AfxMessageBox(msg); }mysql_query返回0表示成功,非0时需要靠mysql_errno和mysql_error去查具体原因。常见的几个错误码:错误码含义常见原因1064SQL语法错误拼串时遗漏引号、字段名拼错1054字段不存在表结构改了但SQL没同步1146表不存在操作了错误的数据库1048字段不能为NULL插入语句漏了必填字段2003无法连接服务器服务没启、端口不对、防火墙拦截拿到错误信息之后,不要去猜,直接看错误文本,定位速度会快很多。4.4 多条写入时用事务包起来如果“保存”按钮背后不只是插一条记录,而是一写就要写好几张表,那最好把操作包在一个事务里。比如一次登记就要同时插入客户表、关联联系人表,其中任何一条失败,都得把前面已插入的数据回滚掉,否则数据就半截了。mysql_query(m_pConn, "START TRANSACTION"); if (mysql_query(m_pConn, sql1) != 0) { mysql_query(m_pConn, "ROLLBACK"); return; } if (mysql_query(m_pConn, sql2) != 0) { mysql_query(m_pConn, "ROLLBACK"); return; } mysql_query(m_pConn, "COMMIT");这里有个前提:表引擎必须是InnoDB,事务才生效。MySQL 8.0默认引擎就是InnoDB,自己建的普通表基本没问题。用事务之后,还要记住一点:COMMIT之后再去读mysql_insert_id拿自增ID,顺序不能反。5. 跑通之后最容易踩的四个运行期坑代码基本写完,编译也过了,但真正运行起来才是考验的开始。下面这几个坑是我实际遇到过,并且都花了不少时间排查的,直接列出来,帮你省掉那段弯路。5.1 一运行就报0xc000007b0xc000007b这个错误码在Windows下极其常见,本质是应用程序启动时加载的DLL和你期望的层级对不上,最常见的原因就是exe位数和DLL位数不一致。排查路径是这样的:先在VS里确认活动解决方案平台是x64,编译出来的exe是64位的;再确认你放到exe目录下的libmysql.dll也是64位版本。MySQL 8.0官方安装包里没有32位版本的libmysql.dll,网上能找到的32位版本基本都是很老的MySQL版本,协议上兼容性反而有问题。如果确认exe和DLL都是64位但还是报错,那就用工具看一眼exe的导入表,确认它到底加载的哪个路径下的libmysql.dll。有时候系统PATH里存在一个旧DLL,程序优先加载了它,也会触发这个问题。5.2 中文乱码的完整链路排查中文乱码可能是整个项目里最闹心的问题,因为每一层都可能出问题。我的排查顺序是:第一层,确认表和数据连接字符集。进入MySQL命令行和程序连接后,分别执行:SHOW VARIABLES LIKE 'character_set_%';重点看character_set_client和character_set_connection是不是utf8mb4。程序端可以在mysql_real_connect之后显式执行:mysql_query(m_pConn, "SET NAMES utf8mb4");这一句能一次性把客户端、连接、结果集的字符集全部设置为utf8mb4。第二层,确认代码里的字符串转换。MFC端必须用CW2A(strName, CP_UTF8)转成UTF-8再传给MySQL,不能用系统默认代码页(中文Windows下是GBK)。如果两边字符集不一致,一个汉字在传输过程中就会变成乱码序列。第三层,确认表本身是utf8mb4。如果建表时用的是latin1或gbk,就算连接层全对,存储层也会把数据弄乱。一张已经建成旧字符集的表,可以用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4来转换。5.3 长时间不用后报“MySQL server has gone away”工具类程序经常会出现这种情况:程序开着不动,过半小时再去点“保存”,直接报错误码2006——MySQL server has gone away。原因是MySQL服务器的wait_timeout默认值较大(通常28800秒),但本地网络环境里也可能因为各种原因导致会话被断开,这种情况下客户端并不知道连接已经失效。最简单可靠的方案是执行关键操作前先做一次健康检查:if (m_pConn != nullptr) { int res = mysql_ping(m_pConn); if (res != 0) { // 连接已经断了,关掉重新连 mysql_close(m_pConn); m_pConn = nullptr; } } if (m_pConn == nullptr) { // 重新走一遍连接流程 ConnectToDatabase(); }也可以设置自动重连选项:bool reconnect = true; mysql_options(m_pConn, MYSQL_OPT_RECONNECT, &reconnect);注意这个选项要在启动连接之前设置,之后设置不一定生效。我建议两种方式结合:启动时设好自动重连,关键操作前再mysql_ping确认一次,双保险。5.4 远程数据库连不上时,按这个顺序查如果MySQL不在本机,而是在另一台服务器上,连接失败的排查顺序很重要。我从后往前倒着查:第一步,看3306端口是否通。Windows下直接:telnet 192.168.1.100 3306端口不通,看后面几步;端口通,问题基本在账号授权层。第二步,看MySQL服务是否允许远程连接。配置文件my.ini里如果设置了:bind-address = 127.0.0.1那即使是有效账号也会被拒绝。改成0.0.0.0之后重启MySQL服务才生效。第三步,看账号的host限制。MySQL授权里常见的写法是:CREATE USER 'app_user'@'192.168.1.%' IDENTIFIED BY 'secure_pwd'; GRANT SELECT, INSERT ON my_app_db.* TO 'app_user'@'192.168.1.%'; FLUSH PRIVILEGES;'app_user'@'192.168.1.%'表示只允许来自192.168.1网段的机器用这个账号连接。如果程序所在机器的IP不在授权网段里,就会报1045错误。第四步,看Windows防火墙或云安全组是否放行3306。这一步没配好,前面全是白搭,端口怎么都ping不通。6. 从能跑到跑好:用预处理语句和封装类让代码更稳把上面的内容全部跑通,你的MFC程序已经能正常连MySQL并添加数据了。但作为一个经历过前人代码教训的人,我还想多说一步:想真正让代码稳定、可维护,尽量把数据库操作封装起来,并且用预处理语句替代字符串拼接。6.1 为什么建议封装一个小的数据库工具类MFC程序里,如果每个按钮响应函数都直接写mysql_init、mysql_real_connect、mysql_query这一大套,代码会越堆越乱。我后来把所有MySQL操作收敛到一个单独的类里,对话框只保留一个成员变量,调用时看见的只有bool Connect()、int InsertCustomer(...)这样的方法。封装的好处不仅仅是代码整洁,更重要的是把“连接生命周期”的维护责任集中在一个类里管理,避免各个按钮函数各自为政,有的忘关连接,有的重复开关连了十几次,到了最后连接数被耗尽。6.2 用预处理语句替代字符串拼接字符串拼接即使做了转义,代码写起来仍然有心理负担:每次都要记得为目标缓冲区分配足够空间,否则溢出直接提高崩溃率。预处理语句是更彻底的解决方案:const char* sql = "INSERT INTO customer_info(name, phone, remark) VALUES(?, ?, ?)"; MYSQL_STMT* stmt = mysql_stmt_init(m_pConn); if (stmt == nullptr) { // 错误处理 return; } if (mysql_stmt_prepare(stmt, sql, (unsigned long)strlen(sql)) != 0) { mysql_stmt_close(stmt); return; } char nameBuf[128] = {0}; char phoneBuf[32] = {0}; char remarkBuf[512] = {0}; unsigned long nameLen = 0, phoneLen = 0, remarkLen = 0; MYSQL_BIND bind[3]; memset(bind, 0, sizeof(bind)); bind[0].buffer_type = MYSQL_TYPE_STRING; bind[0].buffer = nameBuf; bind[0].buffer_length = sizeof(nameBuf); bind[0].length = &nameLen; bind[1].buffer_type = MYSQL_TYPE_STRING; bind[1].buffer = phoneBuf; bind[1].buffer_length = sizeof(phoneBuf); bind[1].length = &phoneLen; bind[2].buffer_type = MYSQL_TYPE_STRING; bind[2].buffer = remarkBuf; bind[2].buffer_length = sizeof(remarkBuf); bind[2].length = &remarkLen; if (mysql_stmt_bind_param(stmt, bind) != 0) { mysql_stmt_close(stmt); return; } // 每次赋值后更新长度 strncpy_s(nameBuf, utf8Name, _TRUNCATE); nameLen = (unsigned long)strlen(nameBuf); mysql_stmt_execute(stmt); mysql_stmt_close(stmt);预处理语句和SQL拼接最大的区别是:参数和SQL语句彻底分开了,SQL只负责模板,参数值通过MYSQL_BIND结构体传给服务器。这样做之后,用户输入的值只是一个普通的字符串参数,不可能改变SQL结构,注入风险从根上被消除了。6.3 预处理语句实操中容易忽略的点用预处理语句时有几个坑比较隐蔽。MYSQL_BIND里的buffer指针指向的缓冲区,在mysql_stmt_execute执行完之后才能释放。如果你在栈上定义字符数组,那没问题;但如果你在循环里复用同一个缓冲区,每次执行前都要重新设置length指向的变量值,否则MySQL可能用上一次的旧长度读取数据。字段类型方面,MySQL的VARCHAR对应MYSQL_TYPE_STRING,整数对应MYSQL_TYPE_LONG,日期对应MYSQL_TYPE_DATETIME,这个映射不能搞错,类型不匹配时服务器会报转换错误,而且不好排查。最后顺手提一个我自己的习惯:不管代码里哪个分支用到了连接,对话框销毁时都会统一走CloseConnection()把连接关掉,并且把m_pConn置空。这样即使操作中途用户直接关窗口,也不会留下悬空句柄,更不会出现下一次启动时报“连接数已满”这种莫名其妙的问题。开发阶段密码写死在代码里图方便可以理解,但交付前一定要改成配置文件读取或者加密存储,这一步不能省。
MFC和MySQL这个组合,听起来确实有点“复古”,但直到今天,我身边仍然有一大批内部工具、工装配套软件、实验室记录程序是用MFC对话框写的,后端数据清一色落在MySQL里。我在VS2019下把一个MySQL 8.0.25数据库接进现成的MFC工程,并从零实现了“点按钮、把编辑框里的数据写进数据库表”这个最基础也最高频的需求。整个过程不算复杂,但踩了几个不大不小的坑,包括环境依赖、DLL位数、字符集乱码、超时断连这些。这篇文章就把从环境配置到代码落地的完整链路写出来,给同样在MFC和MySQL之间反复折腾的人一条可以直接照抄的路。1. 为什么绕开ODBC和ADO,直接选MySQL C API这条路很多人一听到MFC连数据库,第一反应就是ODBC,因为老教程里全是CDatabase、CRecordset那套东西。但真把ODBC用到MySQL上,麻烦事一点不少。MySQL 8.0默认的认证插件是caching_sha2_password,如果ODBC驱动版本不够新,或者MFC程序加载的ODBC驱动和系统里装的不一致,连接时会直接报2059错误。再加上ODBC数据源本身还有32位和64位之分,控制面板里配的DSN和程序里实际加载的DSN很可能不是同一份文件,排查起来很痛苦。1.1 三种连接方式的现实对比我简单整理一下几种常见方案的实际体验:方案上手难度维护成本常见坑ODBC中等偏高驱动版本、DSN位数、认证插件兼容性ADO中等偏高依赖OLE DB Provider,官方维护力度弱MySQL C API偏低低DLL位数、字符集,但报错信息直接ADO我也试过,连接字符串写起来简单,但本质上它需要一个可用的OLE DB Provider,而MySQL官方的OLE DB提供程序更新节奏很慢。对于MFC这种老技术栈来说,能用稳定方案就不去赌冷门依托。1.2 直接链接libmysql库的好处我最终的落地方案是直接调用MySQL官方提供的C API,也就是链接libmysql.lib,运行时加载libmysql.dll。这套接口MySQL自己一直在维护,8.0.25的Server配套的库文件能完整实现新版握手协议,不需要额外装驱动。代码量看起来比ODBC多一点点,因为每一步都摆在明面上:初始化、设置参数、连接、执行、关闭,但这也意味着出问题时,错误码直接来自MySQL服务器,排查链路变得非常透明。本文后面所有的示例,都是围绕一个实际的MFC对话框程序来说的:界面上有“姓名”“电话”“备注”三个编辑框,还有一个“保存到数据库”按钮,按钮点了之后把内容插入到一张客户登记表里。先建好数据库和表:CREATE DATABASE my_app_db DEFAULT CHARACTER SET utf8mb4; USE my_app_db; CREATE TABLE customer_info ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, phone VARCHAR(20), remark TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );之所以把表字符集定为utf8mb4,是因为目前MySQL 8.0的默认字符集就是它,能完整存下中文和emoji表情。后面代码里涉及字符编码的部分,也都是围绕这个前提处理的。2. VS2019工程里把MySQL的依赖配置妥当这一步看起来不起眼,实际上翻车率极高。很多人的情况是:代码写得非常顺利,结果编译阶段报了一堆无法解析的外部符号,或者一运行就弹0xc000007b。问题根源几乎都集中在依赖没配好,或者位数对不上。2.1 安装MySQL时要包含开发组件如果你装MySQL Server时一路Next到底,很可能在安装目录里根本找不到include和lib文件夹。默认安装路径一般是C:\Program Files\MySQL\MySQL Server 8.0\,只有在安装器选择组件时勾选了“C/C++头文件和库”(Development Components),才会生成include和lib目录。如果当时没勾选,也不用重装整个数据库。打开MySQL Installer,选择“Add”把Development Components补装上就行,这一步只是往磁盘里补文件,不会动现有的数据。2.2 属性页三件套:包含目录、库目录、附加依赖项打开工程属性,重点检查三个位置:第一个,C/C++ -> 常规 -> 附加包含目录,添加:C:\Program Files\MySQL\MySQL Server 8.0\include第二个,链接器 -> 常规 -> 附加库目录,添加:C:\Program Files\MySQL\MySQL Server 8.0\lib第三个,链接器 -> 输入 -> 附加依赖项,填写:libmysql.lib这里有一个特别容易忽略的地方:MySQL 8.0的Server本身只有64位版本,所以官方提供的libmysql.lib和libmysql.dll也都是64位的。VS2019新建MFC工程时,默认的活动解决方案平台其实是Win32,如果你直接用默认平台去链接64位的库,会报一堆机器类型不匹配的错误。解决办法是在配置管理器里新建一个x64平台,然后整个工程用x64编译。这个操作必须在配置依赖之前做,否则后面所有路径都白配了。2.3 把libmysql.dll放到exe能看见的地方静态链接解决的是编译期问题,运行期还需要libmysql.dll。MySQL安装到系统后,这个DLL是不在C:\Windows\System32里的,需要你自己把它放到生成的exe同目录,或者复制到工程输出目录。我比较推荐在VS工程里配置一个“生成后事件”,每次都自动把DLL拷到输出目录。打开生成事件 -> 生成后事件 -> 命令行,加一行:xcopy /Y "C:\Program Files\MySQL\MySQL Server 8.0\lib\libmysql.dll" "$(OutDir)"这样每次编译完,DLL都会自动跟着刷新,省去手动拷贝的麻烦,也避免exe更新了但DLL还是旧版本的诡异问题。3. 从mysql_init到mysql_real_connect:把连接代码写进MFC按钮里基础依赖配好之后,接下来就是真刀真枪写连接代码。我在对话框上放了一个“测试连接”按钮,一个“保存数据”按钮,还有一个MYSQL*成员变量挂在对话框类上:// MainDlg.h class CMainDlg : public CDialogEx { public: MYSQL* m_pConn; void CloseConnection(); ... };构造函数里把m_pConn初始化成nullptr,接下来在“测试连接”按钮的响应函数里完成整个连接流程。3.1 初始化、设置连接选项、真正建立连接void CMainDlg::OnBnClickedBtnConnect() { // 防止上一次没关干净 if (m_pConn != nullptr) { mysql_close(m_pConn); m_pConn = nullptr; } m_pConn = mysql_init(nullptr); if (m_pConn == nullptr) { AfxMessageBox(_T("mysql_init 返回 NULL,初始化失败")); return; } // 统一走 utf8mb4,避免乱码 mysql_options(m_pConn, MYSQL_SET_CHARSET_NAME, "utf8mb4"); // 连接超时设短一点,免得服务器没启时界面卡死在那 unsigned int timeout = 5; mysql_options(m_pConn, MYSQL_OPT_CONNECT_TIMEOUT, &timeout); MYSQL* ret = mysql_real_connect( m_pConn, "localhost", // 服务器地址 "root", // 用户名 "your_password", // 密码,开发阶段先写死 "my_app_db", // 数据库名 3306, // 端口 nullptr, // Unix socket,Windows下传nullptr 0 // client_flag,这里用0即可 ); if (ret == nullptr) { CString msg; msg.Format(_T("连接失败,错误码:%d(%s)"), mysql_errno(m_pConn), CA2T(mysql_error(m_pConn))); AfxMessageBox(msg); mysql_close(m_pConn); m_pConn = nullptr; return; } AfxMessageBox(_T("连接成功")); }几个参数说明一下。mysql_init(nullptr)会分配并初始化一个MYSQL对象,返回值是nullptr说明连内存分配都失败了,这种极端情况也得处理。mysql_real_connect的最后一个参数client_flag平时传0就够,没必要去翻后面的位标志,除非你明确要开多语句、长连接之类的特性。3.2 为什么把连接放在按钮回调而不是OnInitDialog里我最初也习惯在对话框初始化时连接数据库,后来发现体验很差:如果MySQL服务没启动,程序一打开就弹错误框,用户连主界面都看不着。而且mysql_real_connect如果碰到网络超时,可能阻塞好几秒,这段时间窗口卡死,移动不了,体验非常糟。现在我的习惯是:启动时不主动连接,等用户点按钮时再连;或者程序刚启动时只创建连接对象,真正用到数据库时再建立连接。对一个小工具来说,这种“懒连接”策略更符合日常使用习惯。3.3 关闭连接的统一出口任何地方关闭连接,我都只走同一个函数:void CMainDlg::CloseConnection() { if (m_pConn != nullptr) { mysql_close(m_pConn); m_pConn = nullptr; } }在OnDestroy里也记得调用一下,防止用户直接关窗口时连接还开着,导致句柄泄漏。4. 添加数据:一条INSERT背后的四个细节连接建立了,接下来就是核心的“添加数据”功能。表面上就是拼一条SQL然后执行,但实际操作中有四个细节不处理到位,后面会有各种匪夷所思的报错。4.1 从CString到UTF-8字符串的转换MFC工程如果用的是Unicode字符集,CString里存的是UTF-16宽字符。而MySQL的C API接口全部是char*窄字符,所以需要把编辑框内容转换成UTF-8编码的char*才能传给MySQL。CString strName; GetDlgItemText(IDC_EDIT_NAME, strName); // CW2A 把宽字符转成指定代码页的窄字符 CStringA utf8Name = CW2A(strName, CP_UTF8);这一步是中文乱码的根源之一。很多人直接把CString塞给MySQL,结果存进去的中文变成一堆问号或乱码,就是因为在宽窄字符转换这一步漏了代码页指定。MySQL那边是utf8mb4,这边的字符串也必须是UTF-8,两边对齐了才不会有编码冲突。4.2 mysql_real_escape_string:用户输入不能直接拼SQL界面上用户输的内容是什么,完全不可控。如果直接拼进SQL字符串,一个单引号就能让你的SQL提前结束,严重的话会把整条SQL的语义改掉。这就是经典的SQL注入风险。正确做法是先转义再拼接:char escapedName[256] = {0}; char escapedPhone[64] = {0}; char escapedRemark[1024] = {0}; unsigned long utf8NameLen = (unsigned long)strlen(utf8Name); mysql_real_escape_string(m_pConn, escapedName, utf8Name, utf8NameLen); // phone、remark 同理,先把 CString 转成 UTF-8,再调用 mysql_real_escape_stringmysql_real_escape_string会把单引号、双引号、反斜杠、换行等特殊字符转义成安全形式,目标缓冲区大小按源字符串长度的两倍再加1来分配就够了。注意它的返回值是转义后的字符串长度,如果后面要用这个长度,记得存下来。4.3 执行INSERT并处理错误码转义完成后,拼SQL并执行:char sql[1024] = {0}; sprintf_s(sql, "INSERT INTO customer_info(name, phone, remark) VALUES('%s', '%s', '%s')", escapedName, escapedPhone, escapedRemark); if (mysql_query(m_pConn, sql) != 0) { CString msg; msg.Format(_T("插入失败,错误码:%d(%s)"), mysql_errno(m_pConn), CA2T(mysql_error(m_pConn))); AfxMessageBox(msg); } else { int newId = (int)mysql_insert_id(m_pConn); CString msg; msg.Format(_T("保存成功,新记录ID:%d"), newId); AfxMessageBox(msg); }mysql_query返回0表示成功,非0时需要靠mysql_errno和mysql_error去查具体原因。常见的几个错误码:错误码含义常见原因1064SQL语法错误拼串时遗漏引号、字段名拼错1054字段不存在表结构改了但SQL没同步1146表不存在操作了错误的数据库1048字段不能为NULL插入语句漏了必填字段2003无法连接服务器服务没启、端口不对、防火墙拦截拿到错误信息之后,不要去猜,直接看错误文本,定位速度会快很多。4.4 多条写入时用事务包起来如果“保存”按钮背后不只是插一条记录,而是一写就要写好几张表,那最好把操作包在一个事务里。比如一次登记就要同时插入客户表、关联联系人表,其中任何一条失败,都得把前面已插入的数据回滚掉,否则数据就半截了。mysql_query(m_pConn, "START TRANSACTION"); if (mysql_query(m_pConn, sql1) != 0) { mysql_query(m_pConn, "ROLLBACK"); return; } if (mysql_query(m_pConn, sql2) != 0) { mysql_query(m_pConn, "ROLLBACK"); return; } mysql_query(m_pConn, "COMMIT");这里有个前提:表引擎必须是InnoDB,事务才生效。MySQL 8.0默认引擎就是InnoDB,自己建的普通表基本没问题。用事务之后,还要记住一点:COMMIT之后再去读mysql_insert_id拿自增ID,顺序不能反。5. 跑通之后最容易踩的四个运行期坑代码基本写完,编译也过了,但真正运行起来才是考验的开始。下面这几个坑是我实际遇到过,并且都花了不少时间排查的,直接列出来,帮你省掉那段弯路。5.1 一运行就报0xc000007b0xc000007b这个错误码在Windows下极其常见,本质是应用程序启动时加载的DLL和你期望的层级对不上,最常见的原因就是exe位数和DLL位数不一致。排查路径是这样的:先在VS里确认活动解决方案平台是x64,编译出来的exe是64位的;再确认你放到exe目录下的libmysql.dll也是64位版本。MySQL 8.0官方安装包里没有32位版本的libmysql.dll,网上能找到的32位版本基本都是很老的MySQL版本,协议上兼容性反而有问题。如果确认exe和DLL都是64位但还是报错,那就用工具看一眼exe的导入表,确认它到底加载的哪个路径下的libmysql.dll。有时候系统PATH里存在一个旧DLL,程序优先加载了它,也会触发这个问题。5.2 中文乱码的完整链路排查中文乱码可能是整个项目里最闹心的问题,因为每一层都可能出问题。我的排查顺序是:第一层,确认表和数据连接字符集。进入MySQL命令行和程序连接后,分别执行:SHOW VARIABLES LIKE 'character_set_%';重点看character_set_client和character_set_connection是不是utf8mb4。程序端可以在mysql_real_connect之后显式执行:mysql_query(m_pConn, "SET NAMES utf8mb4");这一句能一次性把客户端、连接、结果集的字符集全部设置为utf8mb4。第二层,确认代码里的字符串转换。MFC端必须用CW2A(strName, CP_UTF8)转成UTF-8再传给MySQL,不能用系统默认代码页(中文Windows下是GBK)。如果两边字符集不一致,一个汉字在传输过程中就会变成乱码序列。第三层,确认表本身是utf8mb4。如果建表时用的是latin1或gbk,就算连接层全对,存储层也会把数据弄乱。一张已经建成旧字符集的表,可以用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4来转换。5.3 长时间不用后报“MySQL server has gone away”工具类程序经常会出现这种情况:程序开着不动,过半小时再去点“保存”,直接报错误码2006——MySQL server has gone away。原因是MySQL服务器的wait_timeout默认值较大(通常28800秒),但本地网络环境里也可能因为各种原因导致会话被断开,这种情况下客户端并不知道连接已经失效。最简单可靠的方案是执行关键操作前先做一次健康检查:if (m_pConn != nullptr) { int res = mysql_ping(m_pConn); if (res != 0) { // 连接已经断了,关掉重新连 mysql_close(m_pConn); m_pConn = nullptr; } } if (m_pConn == nullptr) { // 重新走一遍连接流程 ConnectToDatabase(); }也可以设置自动重连选项:bool reconnect = true; mysql_options(m_pConn, MYSQL_OPT_RECONNECT, &reconnect);注意这个选项要在启动连接之前设置,之后设置不一定生效。我建议两种方式结合:启动时设好自动重连,关键操作前再mysql_ping确认一次,双保险。5.4 远程数据库连不上时,按这个顺序查如果MySQL不在本机,而是在另一台服务器上,连接失败的排查顺序很重要。我从后往前倒着查:第一步,看3306端口是否通。Windows下直接:telnet 192.168.1.100 3306端口不通,看后面几步;端口通,问题基本在账号授权层。第二步,看MySQL服务是否允许远程连接。配置文件my.ini里如果设置了:bind-address = 127.0.0.1那即使是有效账号也会被拒绝。改成0.0.0.0之后重启MySQL服务才生效。第三步,看账号的host限制。MySQL授权里常见的写法是:CREATE USER 'app_user'@'192.168.1.%' IDENTIFIED BY 'secure_pwd'; GRANT SELECT, INSERT ON my_app_db.* TO 'app_user'@'192.168.1.%'; FLUSH PRIVILEGES;'app_user'@'192.168.1.%'表示只允许来自192.168.1网段的机器用这个账号连接。如果程序所在机器的IP不在授权网段里,就会报1045错误。第四步,看Windows防火墙或云安全组是否放行3306。这一步没配好,前面全是白搭,端口怎么都ping不通。6. 从能跑到跑好:用预处理语句和封装类让代码更稳把上面的内容全部跑通,你的MFC程序已经能正常连MySQL并添加数据了。但作为一个经历过前人代码教训的人,我还想多说一步:想真正让代码稳定、可维护,尽量把数据库操作封装起来,并且用预处理语句替代字符串拼接。6.1 为什么建议封装一个小的数据库工具类MFC程序里,如果每个按钮响应函数都直接写mysql_init、mysql_real_connect、mysql_query这一大套,代码会越堆越乱。我后来把所有MySQL操作收敛到一个单独的类里,对话框只保留一个成员变量,调用时看见的只有bool Connect()、int InsertCustomer(...)这样的方法。封装的好处不仅仅是代码整洁,更重要的是把“连接生命周期”的维护责任集中在一个类里管理,避免各个按钮函数各自为政,有的忘关连接,有的重复开关连了十几次,到了最后连接数被耗尽。6.2 用预处理语句替代字符串拼接字符串拼接即使做了转义,代码写起来仍然有心理负担:每次都要记得为目标缓冲区分配足够空间,否则溢出直接提高崩溃率。预处理语句是更彻底的解决方案:const char* sql = "INSERT INTO customer_info(name, phone, remark) VALUES(?, ?, ?)"; MYSQL_STMT* stmt = mysql_stmt_init(m_pConn); if (stmt == nullptr) { // 错误处理 return; } if (mysql_stmt_prepare(stmt, sql, (unsigned long)strlen(sql)) != 0) { mysql_stmt_close(stmt); return; } char nameBuf[128] = {0}; char phoneBuf[32] = {0}; char remarkBuf[512] = {0}; unsigned long nameLen = 0, phoneLen = 0, remarkLen = 0; MYSQL_BIND bind[3]; memset(bind, 0, sizeof(bind)); bind[0].buffer_type = MYSQL_TYPE_STRING; bind[0].buffer = nameBuf; bind[0].buffer_length = sizeof(nameBuf); bind[0].length = &nameLen; bind[1].buffer_type = MYSQL_TYPE_STRING; bind[1].buffer = phoneBuf; bind[1].buffer_length = sizeof(phoneBuf); bind[1].length = &phoneLen; bind[2].buffer_type = MYSQL_TYPE_STRING; bind[2].buffer = remarkBuf; bind[2].buffer_length = sizeof(remarkBuf); bind[2].length = &remarkLen; if (mysql_stmt_bind_param(stmt, bind) != 0) { mysql_stmt_close(stmt); return; } // 每次赋值后更新长度 strncpy_s(nameBuf, utf8Name, _TRUNCATE); nameLen = (unsigned long)strlen(nameBuf); mysql_stmt_execute(stmt); mysql_stmt_close(stmt);预处理语句和SQL拼接最大的区别是:参数和SQL语句彻底分开了,SQL只负责模板,参数值通过MYSQL_BIND结构体传给服务器。这样做之后,用户输入的值只是一个普通的字符串参数,不可能改变SQL结构,注入风险从根上被消除了。6.3 预处理语句实操中容易忽略的点用预处理语句时有几个坑比较隐蔽。MYSQL_BIND里的buffer指针指向的缓冲区,在mysql_stmt_execute执行完之后才能释放。如果你在栈上定义字符数组,那没问题;但如果你在循环里复用同一个缓冲区,每次执行前都要重新设置length指向的变量值,否则MySQL可能用上一次的旧长度读取数据。字段类型方面,MySQL的VARCHAR对应MYSQL_TYPE_STRING,整数对应MYSQL_TYPE_LONG,日期对应MYSQL_TYPE_DATETIME,这个映射不能搞错,类型不匹配时服务器会报转换错误,而且不好排查。最后顺手提一个我自己的习惯:不管代码里哪个分支用到了连接,对话框销毁时都会统一走CloseConnection()把连接关掉,并且把m_pConn置空。这样即使操作中途用户直接关窗口,也不会留下悬空句柄,更不会出现下一次启动时报“连接数已满”这种莫名其妙的问题。开发阶段密码写死在代码里图方便可以理解,但交付前一定要改成配置文件读取或者加密存储,这一步不能省。