bksczm头像
关注
MySQL基础篇之C语言访问MySQL封面图

MySQL基础篇之C语言访问MySQL

一、环境配置与编译

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 库名 切换)
portTCP 端口号,默认 3306。使用 Unix socket 时此参数被忽略
unix_socketUnix 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_ROWSUPDATE 返回找到的行数而非实际修改的行数
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_resultmysql_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 常用成员
成员类型说明
namechar *列名(如果有别名,这是别名)
org_namechar *原始列名(表中真实的列名)
tablechar *表名(如果有别名,这是别名)
org_tablechar *原始表名
dbchar *数据库名
defchar *该列的默认值
lengthunsigned long列的定义长度(如 VARCHAR (50) 则为 50)
max_lengthunsigned long结果集中该列实际最大长度
flagsunsigned int列标志位,见下方
decimalsunsigned int小数位数
typeenum 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_TINYTINYINT
MYSQL_TYPE_SHORTSMALLINT
MYSQL_TYPE_LONGINT
MYSQL_TYPE_LONGLONGBIGINT
MYSQL_TYPE_FLOATFLOAT
MYSQL_TYPE_DOUBLEDOUBLE
MYSQL_TYPE_DECIMALDECIMAL
MYSQL_TYPE_VAR_STRINGVARCHAR
MYSQL_TYPE_STRINGCHAR
MYSQL_TYPE_BLOBTEXT / BLOB
MYSQL_TYPE_DATEDATE
MYSQL_TYPE_DATETIMEDATETIME
MYSQL_TYPE_TIMESTAMPTIMESTAMP
打印所有列名示例
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_resultmysql_close。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/bksczm/article/details/165889417

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--