☰
MySQL基础篇之C语言访问MySQL
2026/10/22 12:52:45 网站建设 项目流程

一、环境配置与编译

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 会乱码

问题现象

连接建立后,客户端的默认字符集是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必须和客户端实际发送的字节编码一致。

新版MySQL在建立连接时就会自动设置三个关键编码,而不是以往的硬编码,因此出现乱码的可能性已经大幅降低!

三个核心字符集变量的设置时机与底层原理

1. 变量生命周期总时间线

三个变量均为会话级变量,每个数据库连接独立拥有、互不干扰,完整生命周期的设置节点如下:

  • 阶段 1:客户端初始化。调用 mysql_init 后,libmysqlclient 加载内置默认字符集(旧版固定为 latin1,MySQL 8.0 配套新版客户端默认 utf8mb4),该值仅保存在客户端本地。

  • 阶段 2:连接握手。调用 mysql_real_connect 建立连接时,客户端将本地字符集标识随握手包发送给服务器;服务器接收后,一次性将当前会话的三个变量全部设置为该值,这是默认值的核心来源。

  • 阶段 3:连接建立后。可通过 SQL 语句动态修改变量值,支持批量同步修改和单个变量单独修改。

  • 补充:服务器全局配置(my.cnf 中的 character-set-server 等)仅为兜底值,仅当客户端连接时未发送字符集标识时才生效;libmysqlclient 连接时一定会主动发送标识,因此全局默认对 C 语言连接通常不生效。

2. character_set_client(客户端声明编码)

  • 定义与作用:服务器端的入口解码规则,用于告知服务器 “客户端发来的 SQL 语句、参数数据是按什么编码的”,是所有输入数据的解码依据。

  • 设置时机:

    1. 默认值由连接握手时客户端发送的标识决定;

    2. 连接前可通过 C 语言 API mysql_options (mysql, MYSQL_SET_CHARSET_NAME, "编码名") 修改客户端本地配置,握手时同步给服务器;

    3. 连接后可通过 SQL 语句 SET character_set_client = xxx 单独修改。

  • 运行原理: 仅作用于服务器协议接入层,收到网络字节流后,严格按照该编码规则拆分字符边界、识别字符语义;只负责解码输入,不参与后续运算和存储,是乱码问题的核心发源地。

3. character_set_connection(中转运算编码)

  • 定义与作用:服务器 SQL 执行层的统一工作编码,是所有字符串运算的基准环境,又称中转编码。(比如:select * from xxx where xxx like xxx…… 为这类的sql语句执行提供环境)

  • 设置时机:

    1. 默认值在连接握手后由服务器自动同步为客户端编码,与 character_set_client 保持一致;

    2. 连接后可通过 SQL 语句 SET character_set_connection = xxx 单独修改。

  • 运行原理: 所有来源的字符串(客户端常量、表字段、系统变量、函数返回值、子查询结果)都必须先转换为该编码,才能参与比较、排序、截取、长度计算、索引匹配等所有运算;配套的校验规则(collation)也跟随该编码生效,直接决定字符串比较、排序的结果。

4. character_set_results(结果返回编码)

  • 定义与作用:服务器端的出口编码规则,用于将运算完成的字符序列化为字节流,返回给客户端。

  • 设置时机:

    1. 默认值同样在连接握手后同步为客户端编码;

    2. 连接后可通过 SQL 语句 SET character_set_results = xxx 单独修改。

  • 运行原理: 仅作用于服务器协议输出层,负责将逻辑字符编码为二进制字节;与 character_set_client 是完全对称的逆过程,只影响返回结果,不参与输入解析和内部运算。

5. 三者联动规则

  • 默认同步机制:连接建立时三者默认取值完全一致,这是 MySQL 的默认设计,目的是让主流单编码场景下,入口、运算、出口全程零转码开销,性能最优。

  • SET NAMES 原理:SQL 语句 SET NAMES xxx 的本质,就是一次性同步修改三个变量为同一个值,是官方推荐的标准修改方式,等价于同时执行三条 SET 语句。

  • 单独修改的影响:支持单独修改任意一个变量,但会导致读写链路编码不对称,增加转码开销,且极易引发显示乱码,非特殊场景不推荐使用。

二、读写全链路完整流程

1. 写入全流程(客户端 → 服务器磁盘)

  1. 本地字节生成:键盘输入或代码中的字符串,按终端 / 源码编码转换为二进制字节,存入 C 程序缓冲区,与 MySQL 配置无关。

  2. 网络发送:libmysqlclient 原封不动转发字节,同时携带客户端编码声明,全程不做任何转码。

  3. 入口解码:服务器按 character_set_client 规则,将字节流拆分为独立逻辑字符,确定字符语义;声明与实际不匹配时,字符含义从这一步彻底错误。

  4. 中转统一:字符转换为 character_set_connection 编码,进入运算内存,作为后续所有操作的统一基准;默认与客户端编码一致时,转码开销为 0。

  5. 存储转码:字符从中转编码转换为表 / 列定义的存储编码,仅改变字节表示,不改变字符语义。

  6. 落地存储:最终字节写入磁盘,数据正确性永久固化。

2. 查询全流程(服务器磁盘 → 客户端显示)

  1. 存储读取解码:从磁盘读取字节,按表存储编码还原为逻辑字符,只要表定义不变,这一步永远不会出错。

  2. 中转运算:字符转换为中转编码,在此完成条件匹配、排序、字符串函数、索引匹配等所有 SQL 运算。

  3. 返回序列化:运算结果按 character_set_results 编码,转换为二进制字节流。

  4. 网络传输:字节原封不动发送给客户端,网络层不做任何编码处理。

  5. 客户端接收:libmysqlclient 直接将字节返回给 C 程序,不做转码和解析,纯透传。

  6. 终端显示:终端按自身编码规则解析字节,渲染为可读文字;显示效果由返回字节编码与终端编码共同决定。


四、执行 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 等数值类型,也被转成了字符串;
  • 如果某列的值是 SQLNULL,对应的指针是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。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询