C/C++连接MySQL实战指南:从API到编译配置全解析

发布时间:2026/10/7 17:15:05
C/C++连接MySQL实战指南:从API到编译配置全解析 做服务端开发或者涉及底层系统集成的朋友对 C/C 链接 MySQL 这件事应该都不陌生。很多入门教程会把重点放在数据库本身教你怎么建表、怎么写 SQL可一旦你要在自研服务里直接读写 MySQL就绕不开 C/C 的 API。这篇东西我会把环境搭建、核心 API、增删改查、编译配置再到常见坑全部过一遍代码都是我实际跑过的你可以直接拿去改。适合看这篇的人刚接触 C/C 数据库编程的同学、需要在 Windows 或 Linux 下快速集成 MySQL 的开发者以及那些在 VS 或 VSCode 里被库路径折磨到想摔键盘的朋友。内容不追求花哨尽量把每个步骤讲透让你看完能一次性跑通。1. 连接方式怎么选C API、Connector/C 还是 ODBC1.1 官方 C API 是绕不开的基础MySQL 官方提供了一套 C 语言 API头文件叫mysql.h对应的库是libmysqlclient。这套 API 就是 MySQL 客户端与服务端通信的底层实现服务端本身也是用这套 API 的变体在和存储引擎打交道的。所以在 C 或 C 里直接调用它是最直接、最可控的方式。我们平时说的C/C 连接 MySQL默认就是指这套官方 C API。它的特点是纯 C 接口C 程序里也能直接用不需要额外封装。功能覆盖全连接管理、SQL 执行、结果集读取、预处理语句、事务控制、字符集设置都有对应的函数。网上资料最多遇到问题最容易搜到解决方案。很多人会用 Connector/C 或 ODBC但那些封装底层最终调用的还是 libmysqlclient。把 C API 搞明白了其他封装对你来说就是一层窗户纸。1.2 Connector/C 和 ODBC 的取舍Connector/C 是官方对 C API 的面向对象封装接口风格类似 JDBC代码写起来确实比裸 C API 舒服。比如连接可以用sql::mysql::MySQL_Connection查询可以用PreparedStatement不用手动管理MYSQL_RES*这类资源。ODBC 则是微软生态里更通用的一套标准不光是 MySQLSQL Server、Oracle、PostgreSQL 都能通过 ODBC 访问。如果你的程序以后可能要换数据库用 ODBC 有好处但代价是要额外装 ODBC 驱动配置数据源也麻烦。我的建议很直接纯 C 程序用 C APIC 项目如果团队成员都熟悉面向对象风格可以用 Connector/C但如果你是新手先老老实实把 C API 跑通一遍。因为 C API 一旦会了调试思路非常清晰出了问题你能定位到具体是连接、执行还是取数环节不会被封装层干扰。三种方式的对比如下方式语言要求依赖复杂度性能学习成本官方 C APIC/C低只需 libmysqlclient最高中等Connector/CC中需要额外的 Boost 依赖高中等偏高ODBCC/C高需要驱动管理器驱动高较高2. 环境准备编译器、数据库与连接库缺一不可2.1 数据库有了还不够开发包才是关键很多新手最大的误区是我装了 MySQL 客户端代码里 include mysql.h 应该就能用了。实际上那只是命令行工具mysql.h这个头文件和libmysqlclient这个库文件需要单独装。在 Linux 上装开发包很简单。以 Ubuntu/Debian 系为例sudo apt-get install libmysqlclient-dev装完之后头文件在/usr/include/mysql/mysql.h库文件在/usr/lib/x86_64-linux-gnu/libmysqlclient.so或者/usr/lib/.../libmysqlclient.a。不同发行版路径略有差异可以用下面命令确认mysql_config --cflags mysql_config --libs这个mysql_config脚本会告诉我编译器该加什么参数后面编译时直接用非常省心。CentOS/RHEL 系则用sudo yum install mysql-devel如果用的是 MariaDB开发包名字可能叫libmariadb-devel头文件路径和函数接口基本兼容。2.2 Windows 下安装时勾选 Development ComponentsWindows 这边更隐蔽。你从官网下载 MySQL Installer如果只默认安装了 MySQL Server那你的C:\Program Files\MySQL\MySQL Server 8.0\文件夹里是没有include和lib目录的。必须在安装时自定义安装组件勾选Development Components里面包含 C/C 的头文件和导入库。安装完之后关键路径长这样头文件C:\Program Files\MySQL\MySQL Server 8.0\include\mysql.h导入库C:\Program Files\MySQL\MySQL Server 8.0\lib\libmysql.lib动态库C:\Program Files\MySQL\MySQL Server 8.0\lib\libmysql.dll注意区分libmysql.lib和libmysql.dll.lib是编译链接期用的导入库代码里调用mysql_init等函数时链接器靠它知道函数在哪.dll是运行期用的动态库程序启动时必须能找到它。初学者经常链接通过了运行时报找不到libmysql.dll就是没搞清楚这对兄弟的职责。2.3 验证环境先把数据库跑起来写代码之前先确认 MySQL 服务本身是好的。Linux 上执行systemctl status mysql mysql -uroot -pWindows 上在服务管理器里确认MySQL80服务状态然后命令行执行mysql -uroot -p试试。如果密码都进不去后面写代码也白搭先把这台数据库调通。另外建议单独建一个测试库和测试用户别用 root 挂着跑养成好习惯CREATE DATABASE IF NOT EXISTS testdb DEFAULT CHARACTER SET utf8mb4; CREATE USER appuserlocalhost IDENTIFIED BY App123456; GRANT ALL PRIVILEGES ON testdb.* TO appuserlocalhost; FLUSH PRIVILEGES;这里特意指定utf8mb4字符集是为了后面少踩中文乱码的坑原因第 6 节会说。3. 第一个连接程序从初始化到读取结果3.1 核心流程就七个函数C API 的基本使用流程固定我总结成一句话初始化连接句柄、连接服务器、执行 SQL、取结果、释放资源。对应的函数调用顺序是mysql_init → mysql_real_connect → mysql_query → mysql_store_result → mysql_fetch_row → mysql_free_result → mysql_close别嫌函数多每个都有明确的职责顺着流程走一遍就记住了。下面这段是最小可运行示例功能是查询users表的所有数据#include stdio.h #include stdlib.h #include mysql.h int main() { MYSQL *conn mysql_init(NULL); if (conn NULL) { fprintf(stderr, mysql_init failed\n); return -1; } if (mysql_real_connect(conn, 127.0.0.1, appuser, App123456, testdb, 3306, NULL, 0) NULL) { fprintf(stderr, connect failed: %s\n, mysql_error(conn)); mysql_close(conn); return -1; } if (mysql_query(conn, SELECT id, name, age FROM users) ! 0) { fprintf(stderr, query failed: %s\n, mysql_error(conn)); mysql_close(conn); return -1; } MYSQL_RES *result mysql_store_result(conn); if (result NULL) { fprintf(stderr, store result failed: %s\n, mysql_error(conn)); mysql_close(conn); return -1; } MYSQL_ROW row; while ((row mysql_fetch_row(result)) ! NULL) { printf(id%s, name%s, age%s\n, row[0] ? row[0] : NULL, row[1] ? row[1] : NULL, row[2] ? row[2] : NULL); } mysql_free_result(result); mysql_close(conn); return 0; }这段代码看着简单但里面藏着很多值得注意的细节下面逐个拆开讲。3.2 mysql_real_connect 的每个参数都别乱填mysql_real_connect是这里最核心的函数参数一共八个MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd, const char *db, unsigned int port, const char *unix_socket, unsigned long clientflag);mysql就是mysql_init拿到的句柄连接信息会写进这个结构体。host服务器地址。这里有个经典坑Linux 下填localhost默认走 unix socket填127.0.0.1强制走 TCP。我之前在容器里部署程序连本机 MySQL 填localhost一直报找不到/var/run/mysqld/mysqld.sock改成127.0.0.1立刻就好。安全起见我建议一律填127.0.0.1。user和passwd数据库账号密码。db默认连接的数据库可以为 NULL但建议填上省得每条 SQL 都带库名前缀。portMySQL 服务端口默认 3306。unix_socket走 socket 连接时用不需要就填 NULL。clientflag连接标志位一般填 0 就够。如果需要一次执行多条 SQL可以填CLIENT_MULTI_STATEMENTS后面我会单独说。注意返回值成功返回传入的mysql指针失败返回 NULL。失败后必须用mysql_error(conn)拿错误信息这样你才能知道是密码错了、网络不通还是库不存在。3.3 mysql_query 和 mysql_real_query 怎么选代码里用的是mysql_query它接收一个以\0结尾的字符串。如果 SQL 文本里可能包含二进制数据比如图片内容、加密字段就得换mysql_real_query因为它是按长度执行的不会因为字符串里出现\0就截断int mysql_real_query(MYSQL *mysql, const char *stmt_str, unsigned long length);两种函数的返回值都一样0 表示成功非 0 表示出错。出错时用mysql_error查看原因用mysql_errno查看错误码。我通常习惯这么判断if (mysql_query(conn, sql) ! 0) { printf(error %d: %s\n, mysql_errno(conn), mysql_error(conn)); }这样日志里既有错误码又有错误信息排查起来更方便。3.4 结果集两种取法一次全拉 vs 逐行读取mysql_store_result会把整个查询结果从服务器拉到客户端内存好处是你可以反复遍历MYSQL_ROW也可以通过mysql_num_rows拿到总行数、通过mysql_num_fields拿到列数。坏处是如果结果集特别大内存会吃紧。另一种方式是mysql_use_result它不立刻拉数据而是逐行从服务器读取内存占用小但你不能回头再访问前面的行而且必须把结果全部取完否则后续查询会报 Commands out of sync。实际开发里查询结果一般不会特别大所以我默认用mysql_store_result只有在明确知道结果集可能上百万行时才考虑mysql_use_result。取值时的细节mysql_fetch_row返回的MYSQL_ROW是一个char**每一列都是字符串形式。如果某个字段是 NULL对应位置就是 NULL直接传给printf会崩溃所以代码里加了row[0] ? row[0] : NULL的保护。如果要拿数字类型需要自己atoi转换。4. 增删改查与防注入写真正的业务代码4.1 INSERT、UPDATE、DELETE 的常规写法查询只是第一步实际业务里增删改才是大头。这三种操作和 SELECT 的最大区别是执行成功后没有结果集但你要关注受影响行数。看下面这个例子const char *sql INSERT INTO users(name, age) VALUES(zhangsan, 25); if (mysql_query(conn, sql) ! 0) { fprintf(stderr, insert failed: %s\n, mysql_error(conn)); return -1; } if (mysql_affected_rows(conn) 1) { printf(insert ok, new id %llu\n, mysql_insert_id(conn)); }这里有两个常用函数mysql_affected_rows返回上一条 SQL 影响的行数。INSERT 是插入行数UPDATE 是实际修改的行数DELETE 是删除的行数。mysql_insert_id返回 INSERT 产生的自增 ID。如果你插入一条记录后要立刻拿这个 ID 去操作关联表就靠它。注意mysql_insert_id返回类型是unsigned long long打印用%llu别用%d否则大 ID 会显示成负数。4.2 字符串拼接的隐患SQL 注入不只是 Web 的事很多初学者为了省事直接用sprintf拼 SQLchar sql[1024]; sprintf(sql, SELECT * FROM users WHERE name %s, user_input); mysql_query(conn, sql);如果user_input是 OR 11这条 SQL 就变成了SELECT * FROM users WHERE name OR 11等于把整张表全查出来。更恶劣的是; DROP TABLE users; --可以直接删库。只要 SQL 里有字符串拼接注入漏洞就存在这跟语言无关。C 语言里有一点比脚本语言更危险越界拼接还可能造成缓冲区溢出影响程序稳定性。解决这个问题第一道防线是转义函数mysql_real_escape_stringchar escaped[1024]; mysql_real_escape_string(conn, escaped, user_input, strlen(user_input)); char sql[2048]; snprintf(sql, sizeof(sql), SELECT * FROM users WHERE name %s, escaped);这个函数会把单引号、双引号、反斜杠等特殊字符转义让用户输入变成一个安全的普通字符串。注意它的第一个参数要传连接句柄conn因为它要参考当前连接的字符集来决定转义方式传 NULL 在某些字符集下处理可能不对。第二道防线也是更推荐的方式是用预处理语句下面单独说。4.3 预处理语句防注入之王性能也更好预处理语句Prepared Statement的原理是先把 SQL 模板发给服务器预编译再把参数值单独传过去。参数是独立传输的不会被当作 SQL 代码解析所以天生免疫注入攻击。用 C API 写预处理语句步骤比普通查询多一点但逻辑清晰MYSQL_STMT *stmt mysql_stmt_init(conn); if (stmt NULL) { fprintf(stderr, mysql_stmt_init failed\n); return -1; } const char *sql INSERT INTO users(name, age) VALUES(?, ?); if (mysql_stmt_prepare(stmt, sql, strlen(sql)) ! 0) { fprintf(stderr, prepare failed: %s\n, mysql_stmt_error(stmt)); mysql_stmt_close(stmt); return -1; } MYSQL_BIND bind[2]; memset(bind, 0, sizeof(bind)); char name[64] lisi; int age 28; bind[0].buffer_type MYSQL_TYPE_STRING; bind[0].buffer name; bind[0].buffer_length strlen(name); bind[1].buffer_type MYSQL_TYPE_LONG; bind[1].buffer age; if (mysql_stmt_bind_param(stmt, bind) ! 0) { fprintf(stderr, bind failed: %s\n, mysql_stmt_error(stmt)); mysql_stmt_close(stmt); return -1; } if (mysql_stmt_execute(stmt) ! 0) { fprintf(stderr, execute failed: %s\n, mysql_stmt_error(stmt)); mysql_stmt_close(stmt); return -1; } printf(affected rows: %llu\n, mysql_stmt_affected_rows(stmt)); mysql_stmt_close(stmt);MYSQL_BIND结构体是参数绑定的核心几个关键字段buffer_type告诉服务器这个参数是什么类型字符串用MYSQL_TYPE_STRING整数用MYSQL_TYPE_LONG只要和表结构匹配就行。buffer指向你要传入的数据内存地址。buffer_length字符串类型必须给长度否则服务器不知道读多少字节。如果你要在一条 SQL 里传长文本、二进制数据预处理语句也支持流式传输不会像字符串拼接那样把内存搞得很大。可以这么说生产环境里凡是涉及外部输入的 SQL我都强制要求用预处理语句。转义函数是应急手段预处理才是正餐。4.4 事务处理两条 SQL 之间的原子性不能靠运气转账、下单这种场景必须保证多条 SQL 要么全部成功要么全部失败。C API 没有封装事务函数但你可以直接用 SQL 语句控制if (mysql_query(conn, START TRANSACTION) ! 0) { fprintf(stderr, start transaction failed\n); return -1; } int ok 1; ok (mysql_query(conn, UPDATE accounts SET balance balance - 100 WHERE id 1) 0); ok (mysql_query(conn, UPDATE accounts SET balance balance 100 WHERE id 2) 0); if (ok) { mysql_query(conn, COMMIT); } else { mysql_query(conn, ROLLBACK); fprintf(stderr, transaction rolled back\n); }这里有三个要点START TRANSACTION显式开启事务比SET autocommit0更灵活后者会影响整个连接后续的所有操作。每条 SQL 执行后都要检查返回值只要有一步失败就整体回滚。DDL 语句如CREATE TABLE、ALTER TABLE会隐式提交当前事务所以别把建表和 DML 混在同一个事务里。另外COMMIT和ROLLBACK本身也可能失败严谨一点也应该检查返回值。不过说实话我在实际项目里见过认真处理这两行返回值的人极少能执行到这步基本就成功了。5. 编译器与编辑器配置VS、VSCode、CMake 的完整配置5.1 Visual Studio 配置附加包含目录和附加库目录VS 里配置第三方库的标准套路就是修改项目属性三步走包含目录右键项目 → 属性 → C/C → 常规 → 附加包含目录填 MySQL 的 include 路径比如C:\Program Files\MySQL\MySQL Server 8.0\include。库目录链接器 → 常规 → 附加库目录填C:\Program Files\MySQL\MySQL Server 8.0\lib。依赖项链接器 → 输入 → 附加依赖项填libmysql.lib。配置完成后代码里#include mysql.h就能正常编译。如果链接时报LNK2019或者LNK2001找不到mysql_init之类的符号九成是附加依赖项没加或者加的库文件和你编译的架构不匹配。另外一个隐蔽问题Debug 模式要链接 debug 版库。MySQL 的 lib 目录下通常有debug子目录里面有libmysql.lib的 debug 版本。Release 程序链接了 debug 库或者反过来运行时就可能出现莫名其妙的崩溃。我在项目里一般这样处理Debug 配置链接lib\debug\libmysql.libRelease 配置链接lib\libmysql.lib。5.2 VSCode MinGW 配置手动写编译参数VSCode 不像 VS 有图形化属性页所有配置都在tasks.json里。假设你用 MinGW-w64 的 gcc一个能编译上面示例代码的任务配置长这样{ version: 2.0.0, tasks: [ { label: Build MySQL Demo, type: shell, command: gcc, args: [ -g, main.c, -o, demo.exe, -I, C:/Program Files/MySQL/MySQL Server 8.0/include, -L, C:/Program Files/MySQL/MySQL Server 8.0/lib, -lmysql ], group: { kind: build, isDefault: true } } ] }重点解释三个参数-I指定头文件搜索目录。-L指定库文件搜索目录。-lmysql告诉链接器链接libmysql.lib。注意 Windows 下的 MinGW 使用-lmysql而 Linux 下是-lmysqlclient别搞混了。运行的时候还有一道坎生成的demo.exe双击或者运行时系统提示找不到libmysql.dll。解决办法是把libmysql.dll拷贝到 exe 所在目录或者把 MySQL 的 lib 目录加到系统 PATH 环境变量。我一般选前者就跟把依赖的 DLL 和 exe 放在一起是一个道理干净利落。5.3 Linux 下编译mysql_config 是神器Linux 下用 gcc 编译连接 MySQL 的程序最简单的方法是用mysql_config自动获取路径gcc main.c -o demo $(mysql_config --cflags --libs)这条命令等价于在命令行里手动填入-I/usr/include/mysql -L/usr/lib/x86_64-linux-gnu -lmysqlclient。用mysql_config的好处是不同系统路径不一样它能自动适配写进 Makefile 里不会出现路径写死换台机器就编不过的尴尬。如果你用 CMake可以这样写find_path(MYSQL_INCLUDE_DIR mysql.h) find_library(MYSQL_LIBRARY NAMES mysqlclient libmysqlclient) add_executable(demo main.c) target_include_directories(demo PRIVATE ${MYSQL_INCLUDE_DIR}) target_link_libraries(demo PRIVATE ${MYSQL_LIBRARY})CMake 的find_path和find_library会在系统默认路径里搜索找不到再手动指定路径。注意头文件路径和库文件路径必须匹配同一套 MySQL 开发包别一个来自/usr/include/mysql另一个来自自己下载的旧版库ABI 不兼容会导致链接期报一堆undefined reference。5.4 架构匹配32 位与 64 位的恩怨这是新手最容易踩的隐形坑。如果你的编译器是 32 位的你去链接 64 位的libmysql.libVS 会直接报fatal error LNK1112: module machine type x64 conflicts with target machine type x86。反过来也一样。检查方式很简单MinGW 执行gcc -v看 Target 是x86_64还是i686。VS 里看解决方案平台的 Active solution platform。MySQL 库文件在lib目录下Server 8.0 默认提供 64 位库文件如果你的编译器是 32 位需要单独下载 32 位的 Connector/C 包。我之前遇到过一个朋友在 64 位 Windows 上装完 MySQL用 VS 默认的 x86 平台编译折腾了一个小时没搞明白为啥链接不过。其实把平台切到 x64 就解决了。记住一句话程序架构、编译器架构、MySQL 库架构三者必须一致。6. 常见问题与排查技巧实录6.1 中文乱码字符集设置必须在连接后立刻做中文乱码可能是 C/C 连 MySQL 最常见的坑症状就是查询出来的姓名显示为???或者插入的中文变成乱码。原因很简单客户端连接时的字符集和服务器、表结构的字符集不一致。MySQL 服务端默认字符集可能是latin1老版本或者utf8mb48.0 之后而你的 C 程序可能默认按latin1发送数据。解决办法在建立连接后、执行任何 SQL 之前调用mysql_set_character_setmysql_set_character_set(conn, utf8mb4);也可以直接执行 SQLmysql_query(conn, SET NAMES utf8mb4);两者效果一样。关键点有两个必须在第一次查询之前执行。连接建立后服务端用默认字符集和客户端通信你设置前发出的数据已经用错误字符集处理了。表和库的字符集也要是 utf8mb4。建库时最好显式指定就像第 2 节里写的CREATE DATABASE ... DEFAULT CHARACTER SET utf8mb4。关于utf8和utf8mb4MySQL 的utf8最多支持 3 字节而 emoji 表情和一些特殊汉字是 4 字节用utf8会插入失败。现在统一用utf8mb4不会错。6.2 libmysql.dll 找不到这个问题在第 5 节提过这里单独列出来是因为实在太常见。症状是编译成功了链接成功了一运行弹窗由于找不到 libmysql.dll无法继续执行代码。原因程序运行时需要加载libmysql.dll但系统搜索路径里没有它。Windows 加载 DLL 的顺序大致是exe 所在目录 → 系统目录 → PATH 环境变量目录。所以解决方式就三种把libmysql.dll拷贝到 exe 同目录最推荐。把 MySQL 的 lib 目录加到系统 PATH 并重启终端。把 DLL 放到 C:\Windows\System32不推荐污染系统目录。如果你用 CMake还可以在构建完成后自动拷贝 DLLadd_custom_command(TARGET demo POST_BUILD COMMAND cmake -E copy_if_different C:/Program Files/MySQL/MySQL Server 8.0/lib/libmysql.dll $TARGET_FILE_DIR:demo)6.3 MySQL 8.0 连接报 caching_sha2_password 错误MySQL 8.0 默认认证插件换成了caching_sha2_password老版本客户端或老驱动会报错Authentication plugin caching_sha2_password cannot be loaded。解决办法有三条路升级客户端库把你自己的 C API 库换到 8.x 版本这是最彻底的方式老库不认识新认证协议升级后问题自然消失。改用户认证插件执行下面 SQL把用户改回老协议ALTER USER appuserlocalhost IDENTIFIED WITH mysql_native_password BY App123456;使用 TCP SSLcaching_sha2_password完整认证需要加密通道如果你的连接是本地调试可以直接用 unix socket 或配置 SSL。如果采用第二种方案注意mysql_native_password本身也已经被 MySQL 官方标记为废弃以后的版本可能移除。能升库就升库别在旧协议上死磕。6.4 查询导致内存暴涨用 mysql_use_result 还是 mysql_store_result在第 3 节说过两种取结果方式这里再补一个生产案例。我第一次接一个报表需求要查几十万行数据直接mysql_store_result程序内存瞬间涨了 200 多 MB差点把服务搞崩了。后来改成mysql_use_result逐行处理MYSQL_RES *result mysql_use_result(conn); MYSQL_ROW row; while ((row mysql_fetch_row(result)) ! NULL) { // 逐行写入文件或直接内存复用 }内存占用立刻降到几 MB。所以结论是小结果集几千行无脑用mysql_store_result方便省事。大结果集几十万行以上必须用mysql_use_result逐行处理省内存。用mysql_use_result时必须一口气处理完所有行否则同连接上发新查询会报Commands out of sync。6.5 Access denied 和 Cant connect 的排查顺序连接不上数据库时别急着改代码按顺序排查Access denied for user xxxxxxxx账号密码错或者该账号不允许从当前主机连接。查看用户表确认 host 限制SELECT user, host FROM mysql.user;Cant connect to MySQL server on 127.0.0.1可能服务没启动、端口被占用、或者防火墙拦截。先确认服务状态再telnet 127.0.0.1 3306看端口通不通。Unknown database xxxx库名不存在检查连接参数里的 db 名。Table xxxx doesnt exist表名写错了注意大小写。Linux 下表名区分大小写Windows 不区分跨平台项目容易在这里翻车。我个人的习惯是打一排排查命令mysql -uappuser -p -h127.0.0.1 -P3306 testdb能用命令行连上说明数据库层面没问题那就是程序里的参数写错了命令行也连不上基本就是库没起或网络不通别浪费时间调试代码。7. 多线程与连接复用进阶避坑7.1 每个线程独立连接别共享 MYSQL*如果你的程序是多线程的千万注意多个线程不能共享同一个 MYSQL 连接句柄。MySQL C API 的官方文档明确写了连接对象不是线程安全的多个线程同时在一个连接上执行 SQL轻则报错重则数据库连接直接崩掉。正确姿势是每个线程mysql_init mysql_real_connect建立自己的连接。线程结束再关闭。连接数比较多时建议用一个简单的连接池放一堆空闲连接线程用的时候借用、用完归还。连接池实现不难一个互斥锁加一个空闲链表就够但效果立竿见影一次数据库连接建立的开销能有几十毫秒池化后每次取连接只是拿一个现成的业务接口的耗时能明显降下来。7.2 mysql_library_init 和 mysql_thread_init 的用法在正式使用 C API 前官方建议调用mysql_library_init做全局初始化#include mysql.h int main() { mysql_library_init(0, NULL, NULL); // 业务逻辑 mysql_library_end(); return 0; }这个函数会初始化库内部的全局状态在 Windows 多线程环境中尤其重要。如果你不调用也能跑那是因为mysql_init内部会隐式做一部分初始化但显式调用更规范。多线程环境下每个线程的连接建立后建议在mysql_real_connect成功后调用mysql_thread_init连接关闭前调用mysql_thread_end。这两个函数管理线程相关的内存池不调用在功能上通常没问题但在某些平台下会有内存泄漏隐患。7.3 连接超时与重连策略生产环境里数据库不会永远在线网络抖动、服务重启都可能让连接断掉。C API 里设置超时时间的方法是通过执行 SQLSET SESSION wait_timeout 28800; SET SESSION connect_timeout 10;或者在代码里设置 MYSQL_OPT_CONNECT_TIMEOUT 选项int timeout 10; mysql_options(conn, MYSQL_OPT_CONNECT_TIMEOUT, timeout);mysql_options必须在mysql_real_connect之前调用。断线检测则看两点mysql_query返回错误时检查mysql_errno是否为2006MySQL server has gone away是的话说明连接已断需要重新连接。我的处理套路是if (mysql_errno(conn) 2006) { // 重新建立连接 }别疯狂重连加个退避机制比如第一次失败等 1 秒、第二次等 2 秒最多等 30 秒防止数据库刚恢复时大量请求同时涌入。8. 性能优化与 SQL 执行细节8.1 批量插入比逐条插入快一个量级写业务代码时最常见的性能问题就是循环里逐条 INSERT。比如插入一万条数据用循环跑mysql_query一万次每次都有网络往返和 SQL 解析开销耗时可能十几秒。改成批量插入char sql[65536]; int offset 0; offset sprintf(sql offset, INSERT INTO users(name, age) VALUES (u1, 1)); for (int i 2; i 1000; i) { offset sprintf(sql offset, ,(u%d, %d), i, i); }一条 SQL 插入一千条记录再开个事务包起来速度提升不是一点半点。我在实际测试中一万条数据从 15 秒降到 0.3 秒以内差距就这么大。8.2 连接池不是万能的事务要短平快连接池解决了重复建连的开销但事务控制还是要小心。一个长事务会持有行锁、占用连接资源影响其他请求的处理速度。我的习惯是事务里只放必要的写操作读操作尽量放事务外。事务里的 SQL 数量越少越好能合并的合并。提交后马上处理结果别在事务里做慢速 IO 或等待用户输入。8.3 慎用 mysql_query 执行多条语句mysql_query默认只允许执行一条 SQL如果你想一次执行多条比如INSERT ...; UPDATE ...;必须在mysql_real_connect的 clientflag 里加上CLIENT_MULTI_STATEMENTSmysql_real_connect(conn, 127.0.0.1, appuser, App123456, testdb, 3306, NULL, CLIENT_MULTI_STATEMENTS);执行多条语句后结果处理也变了需要循环调用mysql_next_result来遍历每个结果集直到返回 -1。mysql_query(conn, SELECT 1; SELECT 2; SELECT 3); do { MYSQL_RES *result mysql_store_result(conn); // 处理每个结果 mysql_free_result(result); } while (mysql_next_result(conn) 0);不过说实话这种写法在业务代码里不太推荐SQL 的复杂度直线上升调试和维护都不方便。你完全可以用预处理语句多调几次逻辑清晰远胜于省一次网络往返。9. 日志与错误处理线上问题的定位命门9.1 mysql_errno 和 mysql_error 一定要同时记录排查问题最怕的就是日志里只有一句查询失败。我写代码时强制要求每个失败分支必须记录错误码和错误信息fprintf(stderr, [mysql] query failed, errno%d, error%s\n, mysql_errno(conn), mysql_error(conn));mysql_errno返回的是整数错误码mysql_error返回的是可读的错误描述。有了这两样线上报错了才能快速判断是语法问题、权限问题还是网络问题。我见过不少同学只错误信息不打错误码结果错误信息是Unknown error根本没法查。注意mysql_error返回的字符串是静态分配的下一次 API 调用就会覆盖它。如果你想保存这个错误信息供后续使用记得用strdup或snprintf拷贝出来。9.2 sql 语句本身打出来重建现场除脱敏数据外日志里最好把执行的 SQL 语句打印出来。这不只是方便自己调试更重要的是线上出问题后你能拿着 SQL 手动在命令行跑一遍秒确认是程序逻辑问题还是 SQL 本身有问题。fprintf(stderr, [sql] %s\n, sql);加这一行日志的成本几乎为零但排查效率的提升是成倍的。尤其是 SQL 是拼接出来的变量值不同可能导致不同的执行计划你把实际执行的完整 SQL 记录下来很多时好时坏的灵异问题就能一眼看穿。9.3 处理结果时也要防 NULLmysql_fetch_row返回的列如果是 NULL那项就是 NULL 指针不做判断直接 sprintf 或 printf 会触发段错误。我一直用一个小宏来处理#define SAFE_STR(v) ((v) ? (v) : )取值的时候用SAFE_STR(row[0])就能安全打印。如果要把 NULL 转成业务里的默认值就得在代码里显式判断不能把空字符串当 NULL两者语义不同做统计或者逻辑判断时要分清楚。10. 最后再分享一点经验C/C 连 MySQL 这条路我第一次走的时候也磕磕绊绊。当时卡得最久的不是 API 不会用而是环境问题在 Windows 上 VS 里折腾库路径在 Linux 上折腾字符集每次都觉得是代码问题最后发现都是环境没配对。所以这篇东西花了大篇幅讲环境和编译配置因为这些坑比 API 本身更容易劝退新手。现在回头总结核心就几点库版本和架构要对齐、字符集第一时间设置、SQL 操作尽量走预处理语句、多线程必须独立连接、日志里保留错误码和 SQL。这些规矩我后来所有项目都遵循线上问题排查的速度明显快了很多。如果你在自己的环境里遇到文档里没写到的报错先别急着搜把mysql_errno和mysql_error完整打出来再用 MySQL 命令行跑同样一段 SQL基本就能判断问题在数据库层还是程序层。工具用熟了数据库编程也就是一层窗户纸的事。