目录一、C/C引入MySQL客户端库mysql connect引入MySQL第三方库mysql接口介绍初始化 mysql_init() :连接数据库 mysql_real_connect() :下达mysql命令 mysql_query() :关闭 mysql_close() :总结:示例:C/C查询的处理细节mysql_store_result()二、总结平时我们操作 MySQL 数据库都是直接打开终端用 mysql 命令行客户端输入 SQL 完成增删改查。但真实的后端开发场景里业务程序不会人工手动敲命令操作数据库。比如我们用 C/C 写后台服务、数据采集程序以及高性能工具程序时程序需要自动不间断地读写数据库这就必须让代码直接和 MySQL 服务建立网络通信自动发送 SQL、接收数据结果。因此 MySQL 官方专门提供了一套 C 语言客户端开发库C 和 C 代码都可以直接调用这套库封装好的接口不用我们自己手写底层 Socket、不用解析 MySQL 私有通信协议就能完成数据库交互。所以今天我们完整学习从零搭建环境、配置数据库账号、编写 C/C 代码、编译运行实现程序自动连接并操作 MySQL 服务。一、C/C引入MySQL客户端库程序想要登录 MySQL 服务不能直接使用 root 超级管理员账号生产环境和本地开发都要遵循最小权限原则需要单独创建一个专供 C/C 程序使用的数据库账号并且只给它分配指定数据库的操作权限。 如下 :首先登录 MySQL 的 root 管理员账号执行创建专用程序用户的语句 create user connectorlocalhost identified by 123456;。这条语句创建本地登录账号 connector密码 123456专门给本地 C/C 程序使用执行完成后查询 user 表就能在用户列表里看到这条全新记录。 刚创建完成的用户默认没有任何库表操作权限哪怕登录成功也无法读写数据。因此下一步执行授权语句grant all on conn.* to connectorlocalhost;。这条语句代表给 connector 账号开放 conn 这个数据库下所有数据表的全部操作权限测试环境可以直接用 all正式项目只分配 select、insert、update、delete 四类基础权限。 权限分配完成后执行 flush privileges; 刷新权限缓存让新建用户、分配的权限立刻在 MySQL 服务中生效。如果跳过这条刷新语句部分 MySQL 版本会出现权限延迟代码连接数据库时报访问拒绝。 到这里数据库侧的准备工作全部完成拥有了程序专用登录账号、对应数据库访问权限接下来才能着手本地开发环境配置。mysql connect引入MySQL第三方库首先我们明确核心知识点想要用 C 语言代码操作 MySQL不能凭空编写网络通信逻辑必须依赖 MySQL 官方提供的第三方开发库 Connector/C。这套库分为两大核心组件缺一不可分别是include 头文件目录、lib 库文件目录。头文件里存放所有 API 函数、结构体的声明告诉编译器有哪些可用函数lib 目录存放编译好的静态库、动态库里面是函数的底层实现代码编译程序时需要链接这部分库程序运行时也需要加载动态库。在正式写数据库连接逻辑之前我们必须先把这套开发库完整部署到本地环境这是所有编码工作的前置基础。使用 Connector/C 库有两个硬性前提第一是本地 MySQL 服务必须正常运行服务没有启动的话后续代码即便库引入成功也无法建立 TCP 连接访问数据库第二是获取对应系统平台的 Connector/C 开发包Linux、Windows、MacOS 的库文件互不兼容需要去 MySQL 官网下载匹配操作系统版本的压缩包。 在 Linux 系统中也可以直接使用 yum、apt 包管理器一键安装开发包。下面我们看一下拆解解压后的 Connector/C 包目录结构:tree 命令打印出的文件夹层级解压下载好的 Connector/C 压缩包后根目录下只会出现两个关键文件夹include 文件夹与 lib 文件夹。先说 include 目录里面包含大量.h 后缀头文件其中核心文件是 mysql.h我们写 C 代码时必须#include mysql.h这个文件定义了 MYSQL 连接句柄、MYSQL_RES 结果集、mysql_init、mysql_real_connect 等全部操作函数的声明没有这个头文件编译器会报找不到 MySQL 相关类型、函数的错误。其余配套头文件负责字符集、错误码、线程、预处理语句等辅助功能。再讲 lib 目录里面存放编译程序需要链接的库文件分为静态库.a和动态库.so两类。截图里能看到 libmysqlclient.a静态库以及带版本号的 libmysqlclient.so 系列动态链接库软链接指向对应真实库文件。静态库编译时会直接打包进可执行程序动态库运行时才会被程序加载是 Linux 环境下最常用的链接方式。mysql接口介绍我们引入好 Connector/C 库、环境校验完成之后操作 MySQL 数据库的所有代码都必须遵循一套固定的接口调用顺序顺序不能颠倒。完整流程为先调用 mysql_init() 初始化连接句柄 → 调用 mysql_real_connect() 建立 TCP 网络连接 → 调用 mysql_query() 发送 SQL 语句执行操作 → 处理查询结果集查询语句专属步骤 → 全部操作完成后调用 mysql_close() 关闭连接释放资源。 每一个接口各司其职前一个接口执行成功才能执行下一个接口少任何一步或者顺序错乱程序都会出现崩溃、连接失败、数据读取异常等问题。初始化 mysql_init() :只要我们想要使用 MySQL 客户端库第一步必须执行初始化没有初始化就无法调用任何数据库操作接口。这个接口的函数原型是MYSQL *mysql_init(MYSQL *mysql);日常标准写法是 MYSQL *mfp mysql_init(NULL);传入 NULL 代表让函数在堆内存中自动创建一块 MYSQL 结构体空间函数执行完成后返回该结构体的指针。MYSQL 结构体是整个数据库连接的核心载体里面会存储后续连接的 IP 地址、账号、密码、端口、字符集、套接字描述符、错误信息、底层操作函数指针等全部数据。所有后续连接、执行 SQL、获取错误的接口都需要传入这个 MYSQL 指针作为第一个参数。如果初始化失败函数会返回 NULL代表内存分配失败此时程序无法继续操作数据库需要直接终止并打印错误信息。连接数据库 mysql_real_connect() :完成 mysql_init 初始化得到合法 MYSQL 句柄之后下一步必须调用 mysql_real_connect() 接口和 MySQL 服务端建立基于 TCP/IP 的网络通信通道因为 MySQL 底层通信依赖 TCP 协议。函数一共七个参数每个参数作用清晰直观。第一个参数就是 mysql_init 返回的 MYSQL 句柄指针第二个 host 是数据库服务地址本地连接填写 localhost 或者 127.0.0.1第三个 user 是登录数据库的用户名也就是我们之前创建的 connector第四个 passwd 是账号对应的登录密码第五个 db 是程序要操作的目标数据库名这里填写 conn第六个 port 是 MySQL 服务端口默认固定 3306第七个 unix_socket 本地连接填 NULL 即可最后 clientflag 是连接标识默认传 0。函数执行逻辑内部自动发起 TCP 三次握手按照 MySQL 私有协议把账号、密码、库名发送给服务端服务端校验 user 表的账号权限校验通过就返回和入参一致的 MYSQL 指针代表连接成功校验失败返回 NULL我们可以通过配套 mysql_error 接口拿到拒绝原因比如账号不存在、密码错误、没有目标数据库权限。这里连接操作必须使用我们前置创建好的 connector 专用账号不能随意使用 root。下达mysql命令 mysql_query() :成功和数据库建立连接之后所有增删改查、建表、删库等 SQL 命令都依靠 mysql_query()这个接口下发给 MySQL 服务端。函数原型为 int mysql_query(MYSQL *mysql, const char *q);第一个参数是我们已经建立好连接的 MYSQL 句柄第二个参数是字符串形式的完整 SQL 语句比如 select * from user、insert into user values(4,张飞)语句末尾不需要额外加分号也可以正常执行。函数拥有明确的返回值规则返回 0 代表 SQL 语句执行成功返回非 0 数字代表执行失败比如表不存在、字段不匹配、权限不足、SQL 语法写错都可以结合 mysql_error 打印具体报错内容区分问题。这里区分两种 SQL 执行结果如果是增、删、改这类无返回数据的语句执行完 mysql_query 直接通过返回值判断成功与否如果是 select 查询类语句mysql_query 仅负责把查询请求发给服务端想要拿到表里的数据还需要配套结果集接口解析返回内容。关闭 mysql_close() :整套数据库操作全部结束、不再需要访问 MySQL 服务时必须调用 mysql_close() 接口用来关闭 TCP 网络连接同时释放 mysql_init 阶段在堆上分配的 MYSQL 结构体内存避免程序出现内存泄漏、数据库闲置长连接堆积。函数原型为 void mysql_close(MYSQL *sock);只需要传入之前初始化、建立连接的 MYSQL 句柄指针即可没有返回值。这个接口是收尾操作必须放在所有 SQL 执行、结果集读取完成之后调用如果中途程序异常退出没有执行 mysql_close会导致这条 TCP 连接长时间挂在 MySQL 服务端占用服务连接数资源高并发场景下会引发服务连接耗尽无法登录的故障。总结:整套接口的调用逻辑是闭环的mysql_init 开辟存储连接信息的结构体mysql_real_connect 搭建程序与数据库的网络通道mysql_query 实现业务层面的 SQL 交互mysql_close 销毁连接与内存回收。这四个接口是 C/C 操作 MySQL 最基础、必不可少的核心接口后续所有读取查询数据、事务、预处理语句的拓展接口全部都要依托 MYSQL 句柄在这套基础流程之上才能使用。示例:下面我们开始编写代码我们在底层调用 MySQL 官方 C 接口复现前面讲的四大核心接口调用顺序mysql_init 初始化句柄、mysql_real_connect 建立连接、mysql_close 释放连接中间做两层判空容错用来验证程序能不能正常和 MySQL 服务建立通信。代码最开头先引入三类头文件iostream用来做控制台打印输出string封装字符串存储连接参数mysql/mysql.h是 MySQL 客户端库的核心头文件所有数据库操作接口、MYSQL 结构体都定义在这个文件内。接着定义全局常量统一存放连接数据库的固定参数和我们之前创建的账号完全匹配host 本地地址 127.0.0.1登录用户名 connector密码 123456要操作的目标数据库 conn服务端口 8080。把参数单独抽成常量后续修改账号、IP、库名时只改一处即可代码维护更方便。进入 main 主函数后第一步执行初始化操作 MYSQL *my mysql_init(nullptr);给 mysql_init 传入空指针自动在堆分配 MYSQL 连接句柄。紧接着做容错判断如果返回值是 nullptr代表初始化内存分配失败打印错误信息后直接 return 1 终止程序错误码 1 用来标记初始化阶段故障。初始化成功后调用核心连接函数 mysql_real_connect把 string 类型的参数通过.c_str()转为 C 语言原生 char * 字符串依次传入地址、用户名、密码、数据库名、端口unix_socket 填 nullptr客户端标识传 0。函数返回值和 nullptr 对比若相等代表 TCP 握手、账号密码校验、权限校验任意一步失败打印连接错误return 2 标记连接阶段故障。两层校验全部通过就会打印 connect success代表程序和 MySQL 服务成功建立 TCP 连接。最后调用mysql_close(my)关闭连接、释放句柄内存return 0 正常退出程序。下面我们运行上述代码观察运行结果:执行编译好的./mytest终端输出 connect success证明整套库引入、账号权限、网络通信全部没有问题完成连接验证接下来就可以基于这个可用连接执行增删改查 SQL 语句操作数据表。下面我们需要建立一张表进行实验操作:程序连接成功后我们需要一张数据表用来测试插入、修改、删除、查询操作所以先在 MySQL 命令行执行建表语句创建 user 测试表。建表语句定义了四张字段id 是 bigint 类型主键设置自增每条数据自动生成唯一编号name 是 varchar (32) 字符串非空约束必须填写姓名age 是 int 整数非空约束年龄不能为空telphone 手机号字段 varchar (32)设置唯一约束不允许两条数据手机号重复。执行desc user;查看表结构表格清晰展示每一列的字段名、数据类型、是否允许为空、索引类型、默认值、额外属性能直观核对我们设置的主键、自增、唯一约束是否生效。这张 user 表就是我们接下来所有 C 代码操作的目标对象所有 insert、update、delete、select 语句都针对这张表编写。所以下面我们就进行下达 mysql 指令的代码编写:上面代码是在之前成功建立 MySQL 连接的基础上封装了一个简易交互式终端让程序能够手动输入任意 SQL 语句下发给数据库核心依靠 mysql_query 接口完成指令下发。我们在外层使用 while(true) 搭建无限循环只要不输入 quit程序就会持续接收用户输入的 SQL。循环内部先打印提示符 MySQL模拟 mysql 命令行客户端的交互界面方便区分输入区域。紧接着调用 std::getline(std::cin, sql) 读取用户整行输入同时做判断如果读取输入失败或者用户输入的内容等于字符串 quit程序就打印 bye bye 并跳出循环结束交互流程。拿到完整 SQL 字符串后调用核心执行函数int n mysql_query(my, sql.c_str());把 C 的 string 转为 C 语言需要的 char * 字符串传入接口。函数返回值用来区分执行结果约定规则是返回 0 代表 SQL 执行无错误非 0 数字代表执行失败。代码中通过 if 分支做结果打印n 等于 0 时输出当前 SQL 执行成功同时打印返回值 0失败则通过标准错误流打印失败提示与错误码。编译运行程序后终端会出现 MySQL 等待输入说明交互循环逻辑正常生效程序已经具备下发任意 SQL 指令的能力。下面我们接着插入数据:成功运行后我们接着在交互提示符后输入标准 insert 插入语句向之前创建的 user 测试表写入数据语句示例为 insert into user (name, age, telphone) values (tom, 12, 123456789);。执行完成后程序输出 success: 0对应 mysql_query 返回值为 0代表插入操作没有语法错误、权限校验通过数据库已经接收并执行这条新增语句。这里补充一个实操细节SQL 语句末尾的分号可加可不加mysql_query 接口内部会自动兼容两种写法不会影响执行结果。我们重复执行一条结构一致、手机号不同的 insert 语句同样打印 success:0代表第二条数据也写入成功两条插入操作都没有触发表的约束报错telphone 字段唯一约束因为手机号不一样没有产生冲突。怎么证明?程序仅返回执行成功码只能代表 SQL 无语法故障想要确认数据真正落盘入库需要登录原生 MySQL 客户端查询 user 表。所以我们在终端执行 select * from user; 全表查询语句结果表里能看到两条完整记录第一条 id 为 1、第二条 id 为 2name、age、telphone 和我们在程序中输入的内容完全匹配。id 自增主键自动生成也印证插入操作真实修改了数据库内的物理数据并非仅在程序内存中模拟执行。这一步验证也区分了两种场景mysql_query 返回 0 仅代表语句执行无报错不代表肉眼可见的数据变更必须通过原生客户端查询数据库底层表才能百分百确认增删改操作持久化完成。前面我们通过程序完成了 insert 插入数据现在用同样的mysql_query接口实现数据更新操作底层执行逻辑和插入完全一致。我们先定义存储更新语句的 string 对象 sql完整 SQL 语句为 update user set nameJimmy where id2;作用是把主键 id 等于 2 这条记录的 name 字段修改为 Jimmy。随后调用 mysql_query(my, sql.c_str()) 下发语句通过返回值 n 判断执行状态0 代表执行成功非 0 代表失败。下面我们运行:运行编译好的程序在交互终端输入这条 update 语句后程序直接输出 success说明 SQL 语句已经被 MySQL 服务正常接收执行。为了确认修改真正生效我们切换到原生 mysql 客户端执行 select * from user; 查询整张 user 表能清晰看到 id2 对应的 name 字段已经从 tom 变成 Jimmy其他字段数据保持不变证明更新操作持久化到数据库中。从这套逻辑可以总结insert 新增、update 修改、delete 删除这三类 DML 语句全部只依靠mysql_query这一个接口就能完成下发代码写法统一只需要更换字符串里的 SQL 文本即可底层没有额外复杂接口。这里要注意 : 无论插入、更新还是删除只要 mysql_query 返回非 0、程序提示 failed基本都是数据库约束或者语法层面出现冲突。最典型的场景就是主键唯一冲突比如重复插入相同 id 的记录、向 telphone 唯一字段写入重复手机号MySQL 会触发唯一约束拦截直接返回执行失败除此之外字段非空约束未传值、字段数据类型不匹配、表名字段名拼写错误、用户缺少对应操作权限都会导致语句执行失败程序都会捕获到失败返回值。这类报错仅靠返回值只能知道执行出错想要查看详细失败原因还可以搭配mysql_error()接口打印完整数据库错误信息方便定位问题。这里补充一个高频踩坑点程序读写英文数字完全正常但读写中文时出现问号、乱码根源是程序与 MySQL 服务的字符集不匹配:MySQL 客户端库默认字符集是 latin1而我们建表、存储数据大多使用 utf8 字符集两边编码不一致中文数据传输解析时就会乱码。官方提供专用接口 mysql_set_character_set() 统一设置连接字符集使用时机必须放在mysql_real_connect连接成功之后、执行任何 SQL 语句之前传入参数为我们已经建立好的 MYSQL 连接句柄第二个参数指定utf8。调用这条接口后当前这条数据库连接的收发数据都会统一采用 utf8 编码程序插入中文、查询中文都不会再出现乱码是开发中必须加上的标准化配置。C/C查询的处理细节前面我们执行 insert、update、delete 这类修改数据的 SQL 时只需要调用 mysql_query通过返回值 0 就能确认操作完成不需要额外读取数据。但 select 查询语句完全不一样如上图这段代码我们只执行 mysql_query(select * from user)程序只会打印 success但是控制台看不到表里的任何数据。为什么? 原因很简单mysql_query的作用仅仅是把 SQL 发送给 MySQL 服务、判断语句语法和权限是否合法它不会主动把查询到的多行数据拉取到程序并打印。增删改执行完没有返回数据集所以只看返回值就够用select 会带回完整表格数据必须额外调用一套专门读取结果集的接口才能拿到查询内容。mysql_store_result()当 mysql_query执 行 select 并且返回 0执行成功之后必须调用 mysql_store_result() 把服务端返回的所有查询数据全部拉取到本地程序内存:这个函数原型是MYSQL_RES *mysql_store_result(MYSQL *mysql);入参就是我们已经连接好的 MYSQL 句柄返回值是一个 MYSQL_RES 类型的指针这个结构体专门存放整张查询结果表包含行数、列数、每一行的字段数据。底层逻辑上这个函数会通过 MYSQL 句柄内部封装的函数指针读取 TCP 通道里传回的所有行数据并且在堆内存中分配空间存储这些数据。这里有一个非常关键的注意点这片内存是手动分配出来的程序不会自动回收如果读完数据不释放会持续占用内存引发内存泄漏。后续必须配套 mysql_free_result 释放 MYSQL_RES 占用的内存。MYSQLRES:首先在官方文档的定义中我们要分清 MYSQL 与 MYSQL_RES 两个核心结构体的区别文档里一共给出两个基础结构体二者完全不同是整套 C API 的两大载体。第一个是 MYSQL 结构体它是单条数据库连接的句柄我们从 mysql_init 创建、mysql_real_connect 建立 TCP 通道全程操作的都是这个结构体。几乎所有数据库接口第一个参数都要传 MYSQL*文档也明确提醒不能复制这个结构体复制后的对象无法保证正常使用。第二个就是红框标注的 MYSQL_RES专门用来承载带返回行的查询结果只有执行 SELECT、SHOW、DESCRIBE、EXPLAIN 这类会返回表格数据的 SQL 时才会生成 MYSQL_RES 对象。执行 insert、update、delete 没有返回表格不会产生这个结构体。服务端传回的整张查询表格数据在代码里统一叫做结果集 result set全部存放在 MYSQL_RES 内部。想要得到 MYSQL_RES 对象就必须在 mysql_query 执行查询且返回 0 成功后调用 MYSQL_RES *mysql_store_result(MYSQL *mysql);。函数接收已经连通的 MYSQL 连接句柄内部一次性把服务端所有查询行拉取到程序堆内存执行成功返回合法 MYSQL_RES * 指针失败返回 NULL。这里要记住关键细节这块数据是堆上动态分配的程序不会自动回收使用完毕必须调用 mysql_free_result 释放否则持续内存泄漏。怎么理解这个 MYSQLRES 结构体呢?如上图我们可以把 MYSQL_RES 内部存储逻辑类比成字符串二维数组 char** res []。我们 user 表里每一行数据会被封装成一个一维 char数组数组里每个元素代表一个字段不管数据库里是数字、字符串、自增 id客户端库统一全部转成 char字符串多张行数据组合在一起就形成二维数组全部托管在 MYSQL_RES 结构体内部。我们后续读取数据的接口本质就是遍历这个二维数组逐行、逐字段取出字符串打印。又因为是表结构表有行和列拿到 MYSQL_RES 指针后我们可以通过两个接口获取整张结果集的行和列方便循环遍历豆好所以再介绍两个接口:获取结果行数 mysql_num_rows() :第一个接口my_ulonglong mysql_num_rows(MYSQL_RES *res);传入结果集指针返回查询结果一共有多少行记录适合提前判断表里有没有数据、循环控制总次数。获取结果列数 mysql_num_fields() :第二个接口unsigned int mysql_num_fields(MYSQL_RES *res);获取一张结果集有多少列字段比如 user 表是 id、name、age、telphone 四列调用后返回数字 4遍历单行字段时用来做循环上限。这两个函数入参都只能是合法的 MYSQL_RES 指针不能传入 MYSQL 连接句柄否则程序会直接崩溃。下面我们验证一下行和列:首先定义查询 SQL 字符串 select * from user调用 mysql_query 向数据库下发查询语句通过返回值n判断语句是否执行成功。如果返回非 0代表 SQL 语法错误、权限不足等问题直接打印失败信息并退出程序只有n 0时才能继续处理查询返回的数据。确认查询语句执行无错误后调用 mysql_store_result(my)传入已建立好的数据库连接句柄 my把 MySQL 服务端传回的整张表格数据一次性拉取到程序内存返回MYSQL_RES类型的结果集指针 res。这里必须增加判空逻辑如果res等于空指针代表拉取结果集失败打印错误并终止程序避免后续操作空指针引发程序崩溃。拿到合法的res结果集指针后分别调用两个统计接口mysql_num_rows(res)用来获取查询结果一共有多少行记录返回值存入rows变量mysql_num_fields(res)用来获取单条数据包含多少个字段也就是多少列返回值存入fields变量。最后通过控制台打印出行、列的数值直观展示当前查询结果集的尺寸。程序运行后首先输出 select * from user success证明查询语句下发成功紧接着打印行3、列4和下方数据表内容完全匹配。数据表内一共有 3 条数据记录对应 3 行每条数据包含 id、name、age、telphone 四个字段对应 4 列接口统计出来的数值和真实表格结构完全一致验证了 mysql_num_rows、mysql_num_fields 两个接口统计数据准确可靠。同时查询返回的行数、列数会跟随 SQL 筛选条件动态变化。如果我们给 select 语句加上 where 条件过滤部分数据或者只查询指定几个字段两个接口返回的行列数值会同步改变接口会自动根据当前结果集实时统计不需要我们手动计算。现在我们已经通过 mysql_store_result 拿到了完整结果集 MYSQL_RES* res也通过 mysql_num_rows、mysql_num_fields 拿到总行、总列但只能看到数字拿不到表里每一条真实数据所以 MySQL C API 专门提供mysql_fetch_row用来逐行提取数据。获取结果内容 mysql_fetch_row() :函数原型MYSQL_ROW mysql_fetch_row(MYSQL_RES *result);唯一入参是我们之前拿到的结果集指针 res。它内部自带迭代器指针每调用一次自动向后移动一行如果读到最后一行无数据时返回 nullptr代表遍历结束。无论数据库字段是 int、bigint、varchar 类型客户端全部转成字符串给我们代码里统一按字符串打印、解析即可不用区分字段原始类型。我们现在已经有了行和列了下面就应该进行遍历操作MySQL为了支持遍历操作提供了一个结构体 MYSQL_ROW如下:先看底层定义typedef char** MYSQL_ROW;MYSQL_ROW 本质就是二级字符指针等效字符串一维数组。调用一次该函数就会从结果集里取出一行完整记录该行里每一列数据都被统一转为 char* 字符串存进这个一维数组通过下标 row[0]、row[1] 就能分别取第 1 列、第 2 列字段。所以最终的遍历就是:在进入循环读取数据之前我们已经完成了查询、拉取结果集、获取行列总数三步基础操作。第一步执行 mysql_query 下发 select 语句确认返回 0 代表查询无语法、权限错误第二步调用 mysql_store_result 将数据库返回的整张数据表加载到程序内存得到 MYSQL_RES 类型的结果集指针 res第三步通过 mysql_num_rows(res) 拿到结果总行数、mysql_num_fields(res)拿到单条记录总列数。 这两个行列数值是循环的边界条件外层循环控制一共读取多少行数据内层循环控制一行里要打印多少个字段没有这两个数值就无法确定循环终止条件。代码里先打印出行、列数字用来提前校验结果集尺寸是否和预期数据表匹配方便调试排查数据缺失问题。红框内第一层循环 for(int i 0; i rows; i)循环次数等于查询结果总行数每一次循环代表读取数据表里完整一行记录。循环内部核心语句 MYSQL_ROW row mysql_fetch_row(res);每调用一次这个函数就会从 MYSQL_RES 结果集中提取下一行数据。MYSQL_ROW 本质是 char**二级字符指针等效一维字符串数组当前行每一列的数据都会转为 char * 字符串存放在这个数组里不管数据库里字段是数字、字符串类型客户端统一转字符串处理。函数内部自带迭代器每执行一次游标自动向后偏移一行不需要我们手动维护行下标循环走完 rows 次就刚好把全部数据读取完毕。内层循环 for(int j 0; j fields; j)用来遍历当前这一行的全部字段。row[j]代表当前行的第 j 列数据下标从 0 开始依次对应 id、name、age、telphone用制表符\t分隔不同字段保证控制台输出排版整齐。注释里的 *(rowj) 和 row[j] 写法完全等价数组下标语法本质就是指针偏移取值两种写法都可以读取对应列数据。一行内所有字段循环打印完成后执行 std::cout \n; 换行切换到下一行记录还原数据库表格分行展示的效果。运行结果:程序运行输出分为三层信息第一层打印 select * from user success 代表查询语句下发成功第二层打印行 3、列 4和 user 表 3 条记录、4 个字段的结构完全对应第三层循环打印出 3 行完整业务数据每条记录的 id、姓名、年龄、手机号全部完整展示中文姓名、数字都没有丢失、乱码证明双层循环搭配 mysql_fetch_row 可以完整、正确读取整张查询结果集。但是上面我们用 mysql_fetch_row 只能拿到表里每一行存储的数据内容比如 2、Jimmy、12 这类业务值。但很多场景我们还需要拿到字段本身的描述信息列名、字段数据类型、所属表、字段长度、是否主键、是否允许为空等元数据。所以 MySQL C API 提供mysql_fetch_fields() 接口专门一次性读取结果集里所有字段的完整属性解决只查数据、看不到表头字段信息的问题。获取列名 mysql_fetch_fields() :参数是已经加载完数据的 MYSQL_RES* 结果集指针返回值为 MYSQL_FIELD*本质是一个结构体数组首地址数组长度等于 mysql_num_fields(res) 获取到的总列数。我们可以通过下标 fields_arr[0]、fields_arr[1] 依次访问第 0 列、第 1 列的字段属性结构体。MYSQL_FIELD 是存储单列全部属性的结构体如上图成员比较多我们只讲重要的char *name 当前查询显示的列名如果 SQL 里给字段起了别名这里存别名没有别名就是原字段名最常用用来打印表头。char *org_name 字段在数据库表中原始真实名称不受别名影响。char *table 当前字段所属数据表的别名org_table 是原始表名。char *db 该表所属的数据库名。unsigned long length 建表时定义的字段长度比如 varchar(32) 这里值就是 32。unsigned long max_length 本次查询结果里该列数据实际最长字符串长度。unsigned int flags 标记字段约束属性位标识主键、自增、非空、唯一、无符号数字等可以做位运算判断。enum enum_field_types type 字段原始数据类型区分是 INT、BIGINT、VARCHAR、DATE 等类型。其余带 xxx_length 的成员是对应字符串的字节长度日常打印表头基本不用。接着我们使用该接口进行遍历:我们先看红框里获取并打印列名的完整代码逻辑在已经拿到结果集 res、算出总列数 fields 之后调用 mysql_fetch_fields (res) 得到 MYSQL_FIELD 类型的结构体数组首地址存放到 fields_array 指针中这个数组里每一个元素都对应查询结果里一列的完整属性数组长度刚好等于总列数。紧接着开启单层循环循环次数等于字段总数在循环内部通过 fields_array [i].name 取出每一列的列名字符串搭配制表符分隔输出循环结束后换行这样就能在打印数据表内容之前先输出一行表头和原生 mysql 客户端查询时展示的字段标题完全对应。表头打印完成后再执行之前学习的双层循环逻辑读取每一行真实数据外层循环控制总行数每次调用 mysql_fetch_row 取出一行 MYSQL_ROW 数据内层循环遍历该行所有字段按列依次打印单元格内容每一行数据遍历完毕就换行输出表头和数据拼接在一起控制台输出的排版就和数据库命令行查询效果完全一致。运行程序后可以看到先打印 id、name、age、telphone 这四个字段名再逐行输出三条用户记录直观证明我们成功拿到了表的列属性信息不只是单纯读取存储的数据。整套查询、打印流程全部完成之后不能忽略资源释放的收尾操作我们调用mysql_free_result (res) 释放 MYSQL_RES 结果集占用的堆内存mysql_store_result 会在堆上分配空间存放整张查询数据如果不执行这条释放接口多次查询会持续堆积内存造成内存泄漏同时 fields_array、MYSQL_ROW 里所有指向数据的指针都会依附 res 存在释放之后这些指针就会全部失效所以必须在所有读取、打印逻辑结束之后再执行释放。释放完结果集最后调用 mysql_close (my) 关闭数据库 TCP 连接销毁最开始 mysql_init 创建的 MYSQL 连接句柄回收连接相关资源之后主函数正常 return 0 结束程序至此一条完整的 select 查询全流程闭环完成从初始化连接、下发 SQL、拉取结果集、读取列属性表头、遍历行数据、释放结果集、关闭连接整套规范操作全部覆盖区分开增删改无结果集无需释放 res、查询语句必须手动释放结果集的核心差异。二、总结本文介绍了如何在C/C程序中通过MySQL官方提供的Connector/C库实现数据库操作。首先讲解了创建专用数据库账号、配置权限等准备工作然后详细解析了四大核心接口mysql_init初始化连接、mysql_real_connect建立TCP连接、mysql_query执行SQL语句、mysql_close关闭连接。文章重点说明了查询操作的特殊处理流程包括使用mysql_store_result获取结果集、mysql_fetch_row遍历数据行、mysql_fetch_fields获取列名等元数据。最后强调了必须调用mysql_free_result释放结果集内存避免内存泄漏。通过完整示例演示了从连接到查询的完整流程为C/C开发者提供了操作MySQL数据库的实用指南。谢谢大家的观看!