一、环境配置与编译
1.1 这是什么 API
这里使用的是 MySQL C API(libmysqlclient,也称 MySQL Connector/C),是 MySQL 官方提供的 C 语言客户端库。
1.2 安装依赖
sudo apt install libmysqlclient-dev
这个包会安装:
- 头文件:MySQL 所有头文件所在的
mysql/文件夹,安装到/usr/include/mysql/ - 动态库:
libmysqlclient.so安装到/usr/lib/x86_64-linux-gnu/
在 Ubuntu 20.04+ 或 MySQL 8.0 环境下,包名也可能是
default-libmysqlclient-dev,或从 MySQL 官方 APT 源安装mysql-community-devel。
1.3 头文件与编译链接
// 引入 MySQL C API 头文件
#include <mysql/mysql.h>
编译时动态链接 mysqlclient 库:
gcc -o myapp myapp.c -lmysqlclient
1.4 完整编译示例
# 单文件编译
gcc main.c -o main -lmysqlclient
# 如果头文件或库路径不在标准位置,手动指定
gcc main.c -o main -I/usr/include/mysql -L/usr/lib/x86_64-linux-gnu -lmysqlclient
二、MySQL C API 整体流程
使用 C 语言操作 MySQL 的标准流程:
① mysql_init() → 初始化 MYSQL 客户端结构体
② mysql_real_connect() → 连接 mysqld 服务端(TCP三次握手 + 认证)
③ mysql_set_character_set() → 设置字符集(防止乱码)
④ mysql_query() → 执行 SQL 语句
├─ 增删改:mysql_affected_rows() 获取受影响行数
└─ 查询:mysql_store_result() 获取结果集
⑤ 遍历结果集:mysql_fetch_row() 逐行读取
⑥ mysql_free_result() → 释放结果集
⑦ mysql_close() → 关闭连接,释放资源
三、初始化与连接
3.1 mysql_init:初始化客户端结构体
MYSQL *mysql_init(MYSQL *mysql);
作用
分配并初始化一个 MYSQL 结构体。这个结构体是后续所有操作的句柄,内部封装了:
- 网络连接信息(socket 描述符、对端地址)
- 字符集设置
- 错误信息(错误码、错误描述)
- 连接状态、协议版本等
- 通过内部的 VIO(Virtual IO)层统一管理网络收发
澄清:
mysql_init只初始化结构体,不会建立网络连接。真正的 TCP 三次握手和 MySQL 认证是在mysql_real_connect时完成的。
参数与返回值
| 参数 | 说明 |
|---|---|
mysql | 传入 NULL:函数内部动态分配一个 MYSQL 结构体并返回其地址;传入非 NULL 指针:初始化该结构体并返回该指针 |
注意:如果传入的是栈上变量的地址(
MYSQL m; mysql_init(&m);),需确保其在连接关闭前一直有效;通常直接传NULL,由库动态分配更安全。
示例
MYSQL *mysql = mysql_init(NULL);
if (mysql == NULL) {
fprintf(stderr, "mysql_init 失败,内存不足\n");
exit(1);
}
3.2 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 结构体指针 |
host | 服务器地址。传 "localhost" 时,Linux 下默认使用 Unix socket 连接而非 TCP/IP;传 IP 或主机名则使用 TCP/IP |
user | 登录用户名,如 "root" |
passwd | 登录密码 |
db | 连接后默认使用的数据库名,传 NULL 则不选择数据库(后续可用 USE 库名 切换) |
port | TCP 端口号,默认 3306。使用 Unix socket 时此参数被忽略 |
unix_socket | Unix socket 文件路径,见下方详解 |
clientflag | 客户端标志位,见下方详解 |
重点参数 1:unix_socket
当 host 为 "localhost" 时,MySQL 客户端在 Linux 上会优先使用 Unix domain socket 连接(不走 TCP/IP 协议栈,速度更快)。
unix_socket参数用于指定 socket 文件的路径;- 传
NULL则使用默认路径(Ubuntu 通常为/var/run/mysqld/mysqld.sock,CentOS 通常为/var/lib/mysql/mysql.sock); - 如果想强制使用 TCP/IP 连接本地,
host传"127.0.0.1"而不是"localhost"。
重点参数 2:clientflag
客户端标志位,用位或(|)组合多个选项。常用值:
| 标志 | 作用 |
|---|---|
0 | 默认,无特殊选项(最常用) |
CLIENT_MULTI_STATEMENTS | 允许一条 mysql_query 执行多条 SQL(用分号分隔) |
CLIENT_MULTI_RESULTS | 支持多结果集(多语句或存储过程返回多个结果集时需要) |
CLIENT_FOUND_ROWS | UPDATE 返回找到的行数而非实际修改的行数 |
CLIENT_IGNORE_SPACE | 允许函数名和括号之间有空格(如 COUNT (*)) |
一般开发传
0即可。需要执行多语句时才用CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS。
返回值
- 成功:返回传入的
mysql指针(连接已建立) - 失败:返回
NULL
通过返回值判断是否连接成功,失败时可用 mysql_error(mysql) 获取错误原因。
示例
if (mysql_real_connect(mysql, "127.0.0.1", "root", "123456",
"test_db", 3306, NULL, 0) == NULL) {
fprintf(stderr, "连接失败: %s\n", mysql_error(mysql));
mysql_close(mysql);
exit(1);
}
printf("连接成功!\n");
3.3 字符集问题:为什么默认 latin1 会乱码
这是原文提出的核心问题,也是 MySQL C API 开发中最常见的坑。
问题现象
连接建立后,客户端的默认字符集是 latin1。此时如果插入或查询中文,会出现乱码 —— 即使建表时已经指定了 utf8mb4。
为什么建表时指定了字符集还会乱码?
因为表的字符集只决定 "数据怎么存",而 "数据怎么传、怎么理解" 由连接字符集决定。一条 SQL 从键盘输入到最终存入磁盘,要经过完整的字符集转换链路,任何一环不匹配都会乱码。
完整的字符集转换链路
【客户端侧】
键盘输入中文
↓ 按终端编码(UTF-8 或 GBK)转为二进制字节
C 程序缓冲区中是 UTF-8 编码的字节流
↓ 通过网络发送到服务器
【服务器侧】
服务器接收二进制字节
↓ 按 character_set_client 解码,理解为字符
(⚠️ 这里默认是 latin1!服务器按 latin1 逐字节理解 UTF-8 字节,
一个汉字 3 字节被当成 3 个 latin1 字符,已经乱了)
↓ 转换为 character_set_connection(连接字符集)
↓ SQL 解析、执行
↓ 存储时转换为 列/表定义的字符集(如 utf8mb4),写入磁盘
【查询返回时】
从磁盘读取,按列字符集解码为字符
↓ 转换为 character_set_results,编码为字节
↓ 网络发送回客户端
客户端按终端编码解码,显示
乱码的根本原因
客户端实际发送的是 UTF-8 编码的字节,但服务器的 character_set_client = latin1,服务器按 latin1 去理解这些字节:
- 一个汉字在 UTF-8 中占 3 字节;
- 服务器按 latin1 把这 3 字节当成 3 个独立的 latin1 字符;
- 然后这 3 个 "latin1 字符" 被转换存储,存进去的就是乱码数据。
表的字符集是
utf8mb4只保证 "存储格式是 utf8mb4",但存进去的内容本身已经在character_set_client解码环节乱掉了,所以最终存的还是乱码。
解决方法:连接后立即设置字符集
// 方法一(推荐):使用 API 直接设置
mysql_set_character_set(mysql, "utf8mb4");
// 方法二:执行 SQL 设置
mysql_query(mysql, "SET NAMES utf8mb4");
SET NAMES utf8mb4 会同时设置三个会话变量:
character_set_client = utf8mb4(服务器按 utf8mb4 理解客户端发送的字节)character_set_connection = utf8mb4(连接字符集)character_set_results = utf8mb4(返回结果按 utf8mb4 编码)
补充:如果终端是 GBK 编码(如中文 Windows 的 cmd),则应设置为
gbk,而不是utf8mb4。原则是:character_set_client必须和客户端实际发送的字节编码一致。
原文表述修正
原文中 "存储的时候通过负载均衡判断后端的存储服务器忙闲状态,进行存储" 是错误的—— 这是分布式数据库(如 TiDB、分库分表中间件)的概念。单机 MySQL 不存在 "负载均衡判断后端存储服务器",存储时直接按表 / 列定义的字符集编码后写入 InnoDB 数据文件(.ibd)。
四、执行 SQL 语句
4.1 mysql_query
int mysql_query(MYSQL *mysql, const char *q);
作用
向服务器发送一条 SQL 语句并执行。SQL 语句不需要加分号结尾(加了也能执行,但多语句模式下分号有特殊含义)。
返回值
0:执行成功- 非
0:执行失败,可用mysql_error(mysql)获取错误信息
示例
if (mysql_query(mysql, "INSERT INTO student(name, age) VALUES('张三', 20)") != 0) {
fprintf(stderr, "插入失败: %s\n", mysql_error(mysql));
}
4.2 增删改操作:获取受影响行数
对于 INSERT / UPDATE / DELETE,不需要处理结果集,但可以获取受影响的行数:
my_ulonglong mysql_affected_rows(MYSQL *mysql);
示例:
mysql_query(mysql, "UPDATE student SET age = 21 WHERE id = 1");
printf("受影响行数: %llu\n", mysql_affected_rows(mysql));
注意:返回类型是
my_ulonglong(无符号长整型),用%llu格式化输出。
4.3 获取自增 ID
插入数据后,如果表有自增主键,可以获取刚插入的 ID:
my_ulonglong mysql_insert_id(MYSQL *mysql);
示例:
mysql_query(mysql, "INSERT INTO student(name, age) VALUES('李四', 22)");
printf("新插入的自增ID: %llu\n", mysql_insert_id(mysql));
五、查询结果集处理
对于 SELECT(或 SHOW、DESC 等返回数据的语句),执行 mysql_query 成功后,需要从服务器获取结果集。
5.1 获取结果集:mysql_store_result
MYSQL_RES *mysql_store_result(MYSQL *mysql);
作用
mysql_store_result的作用是:将服务器返回的完整结果集一次性全部读取到客户端内存中,并返回一个MYSQL_RES结构体指针。
MYSQL_RES 内部包含:
- 所有行的数据
- 行数、列数
- 每列的元信息(MYSQL_FIELD 数组)
- 当前遍历位置
返回值
- 成功:返回
MYSQL_RES*结果集指针 - 失败:返回
NULL(可能是 SQL 不返回结果集,或出错)
mysql_store_result vs mysql_use_result
| mysql_store_result | mysql_use_result | |
|---|---|---|
| 数据获取方式 | 一次性将全部结果拉到客户端内存 | 逐行从服务器读取,不一次性拉取 |
| 内存占用 | 结果集大时占用较多内存 | 内存占用小,每次只存一行 |
| 可操作功能 | 可获取行数、可随机访问、可重复遍历 | 只能顺序逐行读取,不能回头 |
| 服务器占用 | 读取完后服务器立即释放资源 | 读取期间服务器一直持有结果,不能释放 |
| 适用场景 | 结果集不大、需要行数、需要多次遍历 | 结果集极大、只需顺序处理一次 |
绝大多数场景用
mysql_store_result,简单方便。
5.2 获取结果集信息
// 获取结果集的行数
my_ulonglong mysql_num_rows(MYSQL_RES *res);
// 获取结果集的列数
unsigned int mysql_num_fields(MYSQL_RES *res);
示例:
MYSQL_RES *res = mysql_store_result(mysql);
my_ulonglong rows = mysql_num_rows(res);
unsigned int fields = mysql_num_fields(res);
printf("共 %llu 行,%u 列\n", rows, fields);
5.3 获取列信息:mysql_fetch_fields 与 MYSQL_FIELD
MYSQL_FIELD *mysql_fetch_fields(MYSQL_RES *res);
作用
返回一个 MYSQL_FIELD 结构体数组,每个元素描述结果集中一列的元信息。数组长度等于列数(mysql_num_fields)。
MYSQL_FIELD 常用成员
| 成员 | 类型 | 说明 |
|---|---|---|
name | char * | 列名(如果有别名,这是别名) |
org_name | char * | 原始列名(表中真实的列名) |
table | char * | 表名(如果有别名,这是别名) |
org_table | char * | 原始表名 |
db | char * | 数据库名 |
def | char * | 该列的默认值 |
length | unsigned long | 列的定义长度(如 VARCHAR (50) 则为 50) |
max_length | unsigned long | 结果集中该列实际最大长度 |
flags | unsigned int | 列标志位,见下方 |
decimals | unsigned int | 小数位数 |
type | enum enum_field_types | 列的数据类型,见下方 |
flags 常用标志位
| 标志 | 含义 |
|---|---|
NOT_NULL_FLAG | 非空 |
PRI_KEY_FLAG | 主键 |
UNIQUE_KEY_FLAG | 唯一键 |
MULTIPLE_KEY_FLAG | 普通索引 |
AUTO_INCREMENT_FLAG | 自增 |
UNSIGNED_FLAG | 无符号 |
ZEROFILL_FLAG | 零填充 |
type 常用值
| 枚举值 | 对应 SQL 类型 |
|---|---|
MYSQL_TYPE_TINY | TINYINT |
MYSQL_TYPE_SHORT | SMALLINT |
MYSQL_TYPE_LONG | INT |
MYSQL_TYPE_LONGLONG | BIGINT |
MYSQL_TYPE_FLOAT | FLOAT |
MYSQL_TYPE_DOUBLE | DOUBLE |
MYSQL_TYPE_DECIMAL | DECIMAL |
MYSQL_TYPE_VAR_STRING | VARCHAR |
MYSQL_TYPE_STRING | CHAR |
MYSQL_TYPE_BLOB | TEXT / BLOB |
MYSQL_TYPE_DATE | DATE |
MYSQL_TYPE_DATETIME | DATETIME |
MYSQL_TYPE_TIMESTAMP | TIMESTAMP |
打印所有列名示例
MYSQL_FIELD *fields = mysql_fetch_fields(res);
unsigned int num_fields = mysql_num_fields(res);
for (unsigned int i = 0; i < num_fields; i++) {
printf("%s\t", fields[i].name);
}
printf("\n");
5.4 获取行数据:mysql_fetch_row 与 MYSQL_ROW
MYSQL_ROW mysql_fetch_row(MYSQL_RES *result);
作用
mysql_fetch_row每次调用只返回结果集中的下一行,返回MYSQL_ROW类型。遍历完所有行后返回NULL。
MYSQL_ROW 的本质
typedef char **MYSQL_ROW;
MYSQL_ROW 本质是一个字符串指针数组(二级指针),每个元素指向一列的值。
- 所有值都以字符串形式存储,即使是 INT、FLOAT 等数值类型,也被转成了字符串;
- 如果某列的值是 SQL
NULL,对应的指针是NULL(不是空字符串""),使用前需要判断。
使用方式
可以把 MYSQL_ROW 想象成一维字符串数组,row[0] 是第一列,row[1] 是第二列…… 多次调用 mysql_fetch_row 会自动依次返回下一行(类似 strtok 的迭代器模式)。
5.5 完整遍历结果集示例
方式一:用行数控制 for 循环(原文方式,需先获取行数)
MYSQL_RES *res = mysql_store_result(mysql);
my_ulonglong num_rows = mysql_num_rows(res);
unsigned int num_fields = mysql_num_fields(res);
// 打印列名
MYSQL_FIELD *fields = mysql_fetch_fields(res);
for (unsigned int j = 0; j < num_fields; j++) {
printf("%-15s", fields[j].name);
}
printf("\n");
// 打印数据
for (my_ulonglong i = 0; i < num_rows; i++) {
MYSQL_ROW row = mysql_fetch_row(res);
for (unsigned int j = 0; j < num_fields; j++) {
if (row[j] == NULL) {
printf("%-15s", "NULL");
} else {
printf("%-15s", row[j]);
}
}
printf("\n");
}
方式二:while 循环(更常用,不需要预先知道行数)
MYSQL_RES *res = mysql_store_result(mysql);
unsigned int num_fields = mysql_num_fields(res);
MYSQL_ROW row;
while ((row = mysql_fetch_row(res)) != NULL) {
for (unsigned int j = 0; j < num_fields; j++) {
printf("%s\t", row[j] ? row[j] : "NULL");
}
printf("\n");
}
⚠️ 注意:
row[j]可能为NULL(对应 SQL 的 NULL 值),直接printf("%s", row[j])传入 NULL 指针是未定义行为,必须先判断。
六、错误处理
MySQL C API 提供了统一的错误信息获取函数:
// 获取最近一次错误的描述字符串
const char *mysql_error(MYSQL *mysql);
// 获取最近一次错误的错误码
unsigned int mysql_errno(MYSQL *mysql);
示例
if (mysql_query(mysql, "SELECT * FROM not_exist_table") != 0) {
fprintf(stderr, "错误码: %u\n", mysql_errno(mysql));
fprintf(stderr, "错误信息: %s\n", mysql_error(mysql));
}
七、资源释放
7.1 释放结果集
void mysql_free_result(MYSQL_RES *res);
每次调用 mysql_store_result(或 mysql_use_result)后,处理完结果必须释放,否则内存泄漏。
7.2 关闭连接
void mysql_close(MYSQL *mysql);
关闭与服务器的连接,释放 mysql_init 分配的 MYSQL 结构体(如果是 mysql_init(NULL) 动态分配的)。
注意:
mysql_close会自动关闭连接并释放 MYSQL 结构体,但不会自动释放未释放的MYSQL_RES,所以要先mysql_free_result再mysql_close。
转载自 CSDN-专业IT技术社区




