目录

一、为什么需要连接数据库

1. 命令行到程序驱动

2. MySQL Client 与 MySQL Server

二、 下载并配置 MySQL 开发库

1. Ubuntu 环境下的安装

2. Windows 环境下的配置

3. 开发库的目录结构解析

三、第一个 MySQL 程序

1. 源代码实现

2. 编译与运行

四、MySQL C API 工作流程与详解

1. 整体流程

2. 核心 API 详解与调用规范

2.1 初始化:mysql_init

2.2 建立连接:mysql_real_connect

2.3 执行 SQL 语句:mysql_query

2.4 接收结果集:mysql_store_result

2.5 获取结果集元数据:mysql_num_rows 与 mysql_num_fields

2.6 获取字段属性:mysql_fetch_fields

2.7 读取数据行:mysql_fetch_row

2.8 内存释放与连接销毁:mysql_free_result 与 mysql_close

五、查询结果解析

1. 数据载体类型

2. mysql_fetch_row 返回值问题

3. 结果集的遍历

六、事务操作

1. 事务控制

2. 事务代码案例

七、完整案例

1. 创建实验表

2. 初始化与链接

3. CUD 操作

4. 查询操作

5. 测试管理类

总结


一、为什么需要连接数据库

在面向企业级后端开发的场景中,C/C++ 常用于开发高性能、低延迟的底层服务(如高性能网关、游戏服务器、中间件等)。这些服务无一例外需要与关系型数据库进行高频的数据交互

本指南将深入探讨如何使用 MySQL 官方提供的原生 C API,在 C/C++ 程序中构建稳定、高效的数据库连接与数据操作链路


1. 命令行到程序驱动

在日常开发与调试中,我们通常使用 MySQL 命令行终端或图形化管理工具(如 Navicat、DataGrip)来操作数据库。这种方式属于交互式操作,其局限性显而易见:无法与复杂的业务逻辑深度绑定,也无法自动化处理海量、动态的数据流

程序访问数据库的核心流程

当我们在 C/C++ 程序中访问 MySQL 时,其本质是一个网络通信过程。典型的程序驱动访问流程如下:

  1. 驱动加载与初始化:程序在内存中初始化 MySQL 客户端库的环境变量与通信协议栈

  2. 建立物理连接:程序作为客户端,通过 TCP/IP 协议或本地连接向 MySQL Server 发起连接握手,并完成身份认证

  3. 发送指令与解析协议:程序将 SQL 语句按照 MySQL 通信协议标准进行封包,通过 Socket 发送至服务端;服务端执行完毕后,将结果集封包并返回

  4. 状态维护与连接销毁:程序对返回的数据包进行解包、反序列化,转化为程序内部的数据结构,并在使用完毕后释放网络连接与内存资源


2. MySQL Client 与 MySQL Server

MySQL 采用典型的 C/S(Client/Server)架构

  • MySQL Server(服务端):负责底层的并发控制、事务管理、索引维护以及数据持久化

  • MySQL Client Library(客户端开发库):官方称为 MySQL Connector/C。它是一套封装好的动态链接库或静态链接库,屏蔽了底层复杂的网络通信协议和字节序转换细节。C/C++ 程序正是通过调用该开发库暴露出的 C API 函数,来实现与 Server 的标准通信

二、 下载并配置 MySQL 开发库

要在 C/C++ 项目中使用 MySQL API,必须首先在宿主机上安装并配置好 MySQL 客户端开发库


1. Ubuntu 环境下的安装

在 Linux 环境下,现代包管理器已经将开发库高度集成。通过以下命令即可完成安装:

sudo apt-get update
sudo apt-get install libmysqlclient-dev

安装完成后,系统会自动将头文件放置在 /usr/include/mysql/ 目录下,将链接库文件放置在 /usr/lib/x86_64-linux-gnu/ 目录下


2. Windows 环境下的配置

在 Windows 环境下(以 Visual Studio 为例),配置流程通常如下:

  1. 前往 MySQL 官方网站下载 MySQL Connector/C 的 Windows Zip 包

  2. 解压归档包,将其中的 include 目录添加至 Visual Studio 项目属性的 "包含目录(Include Directories)" 中

  3. 将 lib 目录添加至项目属性的 "库目录(Library Directories)" 中

  4. 在 "链接器->输入->附加依赖项" 中追加 libmysql.lib

  5. 确保程序运行时,libmysql.dll 动态链接库文件存在于程序的可执行文件同级目录下


3. 开发库的目录结构解析

标准的 MySQL Connector/C 开发库包含以下三个核心核心目录:

  • include/:存放所有的 C API 头文件。其中最核心的是 mysql.h,它包含了所有数据库操作所需的结构体定义以及 API 函数声明

  • lib/:存放链接库文件。包含在编译阶段使用的静态库(.a 或 .lib)以及在运行阶段使用的动态共享库(.so 或 .dll)

  • bin/:存放辅助工具。例如 Linux 下的 mysql_config 工具,可用于自动获取编译和链接时所需的参数

三、第一个 MySQL 程序

在正式编写复杂的连接与查询逻辑之前,我们需要通过一个程序来验证开发库是否配置成功。该程序不需要真实的数据库网络连接,它仅在本地内存中读取开发库的版本信息


1. 源代码实现

#include <iostream>
// Linux 环境下通常通过 <mysql/mysql.h> 引入
// Windows 环境下若已将 include 加入路径,则直接引入 <mysql.h>
#include <mysql/mysql.h>

int main() {
    // 获取当前调用的 MySQL 客户端共享库的版本信息
    const char* client_info = mysql_get_client_info();
    
    if (client_info != nullptr) {
        std::cout << "MySQL Client Library configured successfully!" << std::endl;
        std::cout << "Client Version: " << client_info << std::endl;
    } else {
        std::cerr << "Failed to retrieve MySQL client info." << std::endl;
        return 1;
    }
    
    return 0;
}

2. 编译与运行

在 Linux (Ubuntu) 下编译

使用 g++ 编译器时,必须显式通过 -l 参数指定链接 mysqlclient 动态库:

g++ main.cpp -o mysql_version_test -lmysqlclient
./mysql_version_test

输出结果:

通过这一步,证明编译器的链接器已经能够正确识别并调用 MySQL C API 的符号表,开发环境已正式就绪

四、MySQL C API 工作流程与详解

本节将梳理通信链路的生命周期,从基础交互到深度解析,逐步分解各环节实现机制,并对核心函数进行精准的定义


1. 整体流程

在 C/C++ 程序中,一次完整的数据库交互在底层遵循严格的线性顺序。任何一个步骤的缺失或调用顺序错误,都会直接导致内存泄漏或通信协议崩溃

典型的执行路径如下:

[分配句柄: mysql_init]
        │
        ▼
[建立连接: mysql_real_connect]
        │
        ▼
[发送指令: mysql_query]
        │
        ▼
[获取数据: mysql_store_result] ───► [读取元数据: mysql_num_fields / mysql_fetch_fields]
        │
        ▼
[行集迭代: mysql_fetch_row]
        │
        ▼
[释放数据: mysql_free_result]
        │
        ▼
[断开物理连接: mysql_close]

2. 核心 API 详解与调用规范

2.1 初始化:mysql_init

在与 MySQL 服务端建立任何网络连接之前,必须首先分配并初始化一个全局连接上下文控制块(即句柄结构体 MYSQL)

函数原型

​MYSQL *mysql_init(MYSQL *mysql);

参数解析:
mysql:若传入一个已有的结构体指针,函数将对其进行初始化;若传入 NULL(常用推荐做法)
       系统会自动动态分配一块 MYSQL 结构体的物理空间并返回其指针

返回值:成功时返回指向已初始化 MYSQL 句柄的指针;若失败则返回 NULL

2.2 建立连接:mysql_real_connect

该函数负责发起 TCP/IP 握手、进行身份认证,并在成功后激活底层的网络通信 Socket 套接字

函数原型

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:目标 MySQL 服务端的 IP 地址或主机名(若为本地连接可设为 "127.0.0.1")
user:数据库登录用户名
passwd:对应的明文密码
db:指定登录后默认切换的数据库名称,若不指定可传入 NULL
port:网络通信端口,默认通常为 3306
unix_socket:Unix 环境下的本地套接字路径,若通过网络连接则传入 NULL
clientflag:客户端控制位掩码,通常设为 0

返回值:成功时返回与入参一致的 MYSQL* 句柄指针;失败时返回 NULL

2.3 执行 SQL 语句:mysql_query

通过已建立的连接通道,将标准的 SQL 文本发送至服务端执行

函数原型

int mysql_query(MYSQL *mysql, const char *q);

参数解析:
q:以 \0 结尾的 SQL 语句字符串。传入的 SQL 语句末尾不需要追加分号

返回值:执行成功返回 0;发生语法错误或网络通信异常则返回非 0 值

2.4 接收结果集:mysql_store_result

对于 SELECT 等产生结果集的查询语句,在 mysql_query 执行成功后,必须调用此函数将服务端的物理数据包一次性缓存到客户端本地内存中

函数原型

MYSQL_RES *mysql_store_result(MYSQL *mysql);

返回值:成功时返回指向结果集的 MYSQL_RES 结构体指针;
       如果执行的是 INSERT 或 UPDATE 等非查询语句,或者读取数据失败,则返回 NULL

2.5 获取结果集元数据:mysql_num_rows 与 mysql_num_fields

在解析具体的数据行之前,通常需要获取结果集的维度信息(行数与列数),以便分配程序内部的迭代

函数原型

uint64_t mysql_num_rows(MYSQL_RES *result);      // 获取总行数
unsigned int mysql_num_fields(MYSQL_RES *result); // 获取总列数

2.6 获取字段属性:mysql_fetch_fields

该函数用于获取结果集中各个列的定义信息(如列名、数据类型等)

函数原型

MYSQL_FIELD *mysql_fetch_fields(MYSQL_RES *result);

返回值:返回一个由 MYSQL_FIELD 结构体组成的数组指针

2.7 读取数据行:mysql_fetch_row

这是解析二维结果集的核心迭代器。每调用一次,它就会从本地缓存中顺序取出下一行数据,并自动将内部的行指针向下平移

函数原型

MYSQL_ROW mysql_fetch_row(MYSQL_RES *result);

返回值:返回当前行的 C 风格字符串指针数组。如果已经迭代到结果集的末尾,则返回 NULL

2.8 内存释放与连接销毁:mysql_free_result 与 mysql_close

高并发服务中,必须严格执行资源的逆序清理,否则会引发严重的内存泄漏与句柄死锁。

函数原型

void mysql_free_result(MYSQL_RES *result); // 释放 MYSQL_RES
void mysql_close(MYSQL *mysql);            // 关闭网络 Socket 并回收 MYSQL 

五、查询结果解析

在使用 mysql_store_result 将服务端数据同步到客户端本地内存后,程序面临的核心问题是如何在内存中解构。MySQL C API 采用了指针来表达结果集


1. 数据载体类型

在 mysql.h 中,查询结果主要由两个核心类型进行抽象:

MYSQL_RES

MYSQL_RES 是一个结构体,它完整保存了当前查询返回的所有物理行数据、列的定义信息(元数据),以及用于遍历的指针。在程序层面,它的内部成员对开发者是不透明的,必须通过相关 API(如 mysql_num_rows)进行访问

MYSQL_ROW

通过查看源码定义,可以发现 MYSQL_ROW 的本质是:

typedef char **MYSQL_ROW;

它并不是一个结构体,而是一个二级字符指针。具体而言,它是一个指向字符指针数组的指针。数组中的每一个元素(char*)对应当前行中某一个字段的文本


2. mysql_fetch_row 返回值问题

在通用的 MySQL C API 文本协议中,无论底层的物理列在数据库里被定义为 INT、FLOAT、VARCHAR 还是 DATETIME,Server 返回给客户端的底层数据包在默认情况下全部会被序列化为 "C 风格字符串"

其底层逻辑如下:

  • 一张表由多列组成,每一列的值在内存中都表现为一个 char* 字符串

  • 一行数据就是这些 char* 指针的集合,即一个指针数组(char *row[])

  • mysql_fetch_row 返回的就是这个指针数组的首地址,因此其类型必然是 char**

如果数据库中的某个字段存在 NULL 值,该字段对应的 char* 指针将直接指向 nullptr。因此,在对 MYSQL_ROW 的各列元素进行字符串操作或利用 atoi()、atof() 进行类型转换前,必须首先进行 if (row[i] == nullptr) 的非空校验,否则会导致程序发生空指针访问崩溃


3. 结果集的遍历

解析一个完整的二维数据矩阵,标准流程是先通过 mysql_fetch_fields 提取表头(列名),随后通过双重循环(外层循环控制行迭代,内层循环控制列展开)将数据完全剥离出来

void parse_result_set(MYSQL_RES *result) {
    if (result == nullptr) return;

    // 1. 获取结果集的维度
    unsigned int num_fields = mysql_num_fields(result);
    uint64_t num_rows = mysql_num_rows(result);
    

    // 2. 提取表头元数据并打印
    MYSQL_FIELD *fields = mysql_fetch_fields(result);
    for (unsigned int i = 0; i < num_fields; ++i) {
        std::cout << fields[i].name << "\t";
    }
    std::cout << "\n------------------------------------------------\n";

    // 3. 循环解包
    MYSQL_ROW row;
    while ((row = mysql_fetch_row(result)) != nullptr) {
        for (unsigned int i = 0; i < num_fields; ++i) {
            // 处理数据库中的 NULL 占位符
            if (row[i] != nullptr) {
                std::cout << row[i] << "\t";
            } else {
                std::cout << "[NULL]" << "\t";
            }
        }
        std::cout << "\n";
    }
}

六、事务操作

在执行高频写入、扣款或多表联动修改时,必须保证数据操作的原子性。MySQL 默认处于自动提交模式,即每执行一条 mysql_query,Server 就会立即将其物理落盘。为了引入事务,需要显式控制其提交时机


1. 事务控制

在 C/C++ 程序中控制事务,有以下两种操作路径:

纯 SQL 指令驱动(通过 mysql_query)

通过向客户端发送事务控制的标准文本来接管控制权:

  • 开启事务:mysql_query(mysql, "START TRANSACTION")

  • 提交事务:mysql_query(mysql, "COMMIT")

  • 回滚事务:mysql_query(mysql, "ROLLBACK")

API 函数驱动(推荐)

MySQL C API 提供了专门的接口函数,在性能和代码语义上更优:

  • 关闭自动提交(开启事务环境):mysql_autocommit(mysql, 0)(传入 0 表示禁用自动提交,即后续所有的 DML 语句都将自动自动纳入同一个事务块中)

  • 提交/回滚:直接调用 mysql_commit(mysql) 或 mysql_rollback(mysql)


2. 事务代码案例

以下是常见处理事务的典型错误捕获与异常回滚机制:

bool execute_financial_transaction(MYSQL *mysql, const std::string& sql1, 
    const std::string& sql2) 
{
    // 1. 显式关闭自动提交
    if (mysql_autocommit(mysql, 0) != 0) {
        std::cerr << mysql_error(mysql) << std::endl;
        return false;
    }

    // 2. 执行第一条业务 SQL 语句
    if (mysql_query(mysql, sql1.c_str()) != 0) {
        std::cerr << mysql_error(mysql) << std::endl;
        mysql_rollback(mysql); // 物理回滚
        mysql_autocommit(mysql, 1); // 恢复环境状态
        return false;
    }

    // 3. 执行第二条业务 SQL 语句
    if (mysql_query(mysql, sql2.c_str()) != 0) {
        std::cerr << mysql_error(mysql) << std::endl;
        mysql_rollback(mysql); // 物理回滚
        mysql_autocommit(mysql, 1); // 恢复环境状态
        return false;
    }

    // 4. 两步均执行成功,向 Server 发起持久化
    if (mysql_commit(mysql) != 0) {
        std::cerr << mysql_error(mysql) << std::endl;
        mysql_rollback(mysql);
        mysql_autocommit(mysql, 1);
        return false;
    }

    // 5. 还原初始的自动提交环境
    mysql_autocommit(mysql, 1);
    return true;
}

七、完整案例

为了将上述所有 API、事务控制逻辑融合进实战,下面给出一个结构完整的 C++ 类实现,演示如何对 student 表进行全生命周期的增删改查


1. 创建实验表

CREATE DATABASE IF NOT EXISTS school_db;
USE school_db;

CREATE TABLE IF NOT EXISTS student (
    id INT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    age INT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2. 初始化与链接

首先,我们先把连接句柄的动态初始化、网络物理连接、以及析构时的自动资源释放搭起来:

#include <iostream>
#include <string>
#include <mysql/mysql.h>

class StudentManager {
private:
    MYSQL *mysql; 

public:
    // 在对象创建时,自动分配内存句柄
    StudentManager() {
        mysql = mysql_init(nullptr);
    }
    
    // 利用 C++ 对象的生命周期,确保类对象销毁时,自动关闭网络连接
    ~StudentManager() {
        if (mysql != nullptr) {
            mysql_close(mysql);
            std::cout << "MySQL Connection closed cleanly.\n";
        }
    }

    // 初始化物理网络连接
    bool connect(const std::string& host, 
                const std::string& user, 
                const std::string& pass, 
                const std::string& db) 
    {
        if (mysql_real_connect(mysql, host.c_str(), user.c_str(), pass.c_str(), 
                db.c_str(), 3306, nullptr, 0) == nullptr) 
        {
            std::cerr << "Connection Error: " << mysql_error(mysql) << std::endl;
            return false;
        }
        // 设置通信字符集,防止中文乱码
        mysql_set_character_set(mysql, "utf8mb4");
        return true;
    }
};

3. CUD 操作

对于 INSERT、UPDATE、DELETE 这三种写入行为,它们的逻辑高度一致:拼接出合法的 SQL 文本,然后丢给 mysql_query 执行,只要返回 0 就算是成功

我们将这三个方法追加到 StudentManager 类中:

// C: 插入学生记录
bool insert_student(int id, const std::string& name, int age) {
    std::string sql = "INSERT INTO student (id, name, age) VALUES (" + 
                       std::to_string(id) + ", '" + name + "', " + 
                       std::to_string(age) + ")";
    return (mysql_query(mysql, sql.c_str()) == 0);
}

// U: 修改学生年龄
bool update_student_age(int id, int new_age) {
    std::string sql = "UPDATE student SET age = " + 
                       std::to_string(new_age) + " WHERE id = " + 
                       std::to_string(id) + ";";
    return (mysql_query(mysql, sql.c_str()) == 0);
}

// D: 删除学生记录
bool delete_student(int id) {
    std::string sql = "DELETE FROM student WHERE id = " + std::to_string(id) + ";";
    return (mysql_query(mysql, sql.c_str()) == 0);
}

4. 查询操作

这里要特别注意对 mysql_free_result 的释放

将该方法追加至类中:

// R: 查询解析
void select_all_students()
{
    std::string sql = "SELECT id, name, age FROM student;";
    if (mysql_query(mysql, sql.c_str()) != 0)
    {
        std::cerr << mysql_error(mysql) << std::endl;
        return;
    }

    // 1. 将数据转储到内存
    MYSQL_RES *result = mysql_store_result(mysql);
    if (result == nullptr) return;

    // 2. 读取总列数
    unsigned int fields = mysql_num_fields(result);
    MYSQL_ROW row;

    // 3. 逐行向下读取
    while ((row = mysql_fetch_row(result)) != nullptr)
    {
        for (unsigned int i = 0; i < fields; ++i)
            std::cout << (row[i] ? row[i] : "[NULL]") << "\t";
        std::cout << "\n";
    }
    std::cout << "=========================\n";

    // 4. 释放结果集
    mysql_free_result(result);
}

5. 测试管理类

最后,我们在 main 函数中对功能进行测试

int main() {
    StudentManager manager;
    
    if (!manager.connect("127.0.0.1", "root", "Your_Pass", "school_db"))
        return -1;

    // 1. 测试增
    manager.insert_student(101, "王美丽", 20);
    manager.insert_student(102, "赵铁柱", 22);
    manager.select_all_students();

    // 2. 测试改
    manager.update_student_age(101, 21);
    manager.select_all_students();

    // 3. 测试删
    manager.delete_student(102);
    manager.select_all_students();

    return 0;
}

总结

综上所述,我们完成了 MySQL C API 的学习,掌握了如何在 C/C++ 程序中连接 MySQL 数据库,并熟悉了从初始化、建立连接、执行 SQL、获取结果集、遍历数据到释放资源的完整开发流程。同时,我们也通过实际案例,进一步理解了如何将数据库操作融入到应用程序中,实现真正意义上的数据持久化

至此,本系列从数据库基础概念出发,依次学习了 MySQL 的安装与架构、库表管理、数据类型、约束、CRUD 操作、内置函数、复合查询、连接查询、索引、事务、视图、用户管理以及 C/C++ 数据库编程等内容,完成了从数据库基础使用底层原理工程实践的一整套知识体系

希望通过这一系列文章,不仅能够帮助读者学会 MySQL 的常见使用方法,更能够理解数据库背后的设计思想与实现原理,在实际开发中真正做到知其然,更知其所以然。至此,MySQL 系列圆满结束,感谢大家一路以来的阅读与支持

更多推荐