20、C语言操作MySQL数据库:MySQL Connector/C安装、mysql_init、mysql_real_connect、基本CRUD

说实话,很多C语言开发者一听到要操作数据库,第一反应就是「这玩意儿得用Java或者Python吧?」。其实不然。C语言通过MySQL官方提供的Connector/C库,完全可以直连MySQL,执行增删改查。我在早期做嵌入式数据采集系统时,就是靠这套接口把传感器数据直接写入MySQL的。

今天我们就来把这套流程走一遍。从安装库开始,到连接数据库,再到最基本的CRUD操作,一步不落。

20.1 环境准备:安装MySQL Connector/C

在写代码之前,得先把库装上。MySQL Connector/C 是官方提供的C语言客户端库,说白了就是一组头文件和动态/静态链接库。

20.1.1 Linux下的安装

如果你用的是Ubuntu或Debian系,一条命令搞定:

sudo apt-get install libmysqlclient-dev

CentOS或RHEL系的话:

sudo yum install mysql-devel

安装完成后,头文件在 /usr/include/mysql/,库文件在 /usr/lib/x86_64-linux-gnu/ 或类似路径。

20.1.2 Windows下的安装

Windows稍微麻烦一点。去MySQL官网下载Connector/C的ZIP包,解压后你会看到 includelib 两个目录。我个人习惯把这两个目录放到项目文件夹里,然后在IDE中配置附加包含目录和附加库目录。

小提示: 编译时记得链接 libmysql.lib(静态库)或 libmysql.dll(动态库)。我当年第一次编译时忘了链接,报了一堆未解析的外部符号,折腾了半小时才发现。

20.1.3 验证安装

写个简单的测试程序,看看能不能找到头文件:

#include <mysql/mysql.h>

int main() {
    printf("MySQL client version: %s\n", mysql_get_client_info());
    return 0;
}

编译命令:

gcc -o test test.c -lmysqlclient

如果能正常输出版本号,说明环境没问题了。

20.2 初始化连接:mysql_init 与 mysql_real_connect

连接MySQL数据库,核心就两步:初始化句柄,然后建立连接。

20.2.1 mysql_init

mysql_init() 用来分配或初始化一个 MYSQL 对象。这个对象后续所有操作都要用到。

MYSQL *conn;
conn = mysql_init(NULL);
if (conn == NULL) {
    fprintf(stderr, "mysql_init failed\n");
    return -1;
}

嗯,这里要注意:mysql_init 失败的情况其实很少见,通常是内存不足。但作为严谨的C程序员,检查返回值是基本素养。

20.2.2 mysql_real_connect

有了句柄,接下来就是连接数据库服务器了:

MYSQL *conn_ptr;
conn_ptr = mysql_real_connect(conn, 
                              "localhost",   // 主机名
                              "root",        // 用户名
                              "password",    // 密码
                              "test_db",     // 数据库名
                              3306,          // 端口号
                              NULL,          // Unix socket(通常传NULL)
                              0);            // 客户端标志

if (conn_ptr == NULL) {
    fprintf(stderr, "Connection failed: %s\n", mysql_error(conn));
    mysql_close(conn);
    return -1;
}

我曾经在生产环境中遇到过一个问题:连接超时。默认情况下,mysql_real_connect 的超时时间很短,如果网络不稳定,很容易失败。后来我加了 mysql_options 设置连接超时,才彻底解决。

避坑指南: 连接失败后一定要调用 mysql_close 释放资源。我曾经见过有人连接失败后直接退出,导致句柄泄漏,最终进程句柄数耗尽。

20.3 基本CRUD操作

连接建立好了,接下来就是干活了。CRUD——增(Create)、查(Retrieve)、改(Update)、删(Delete),一个都不能少。

20.3.1 执行SQL语句:mysql_query

所有SQL操作都通过 mysql_query 来执行:

if (mysql_query(conn, "SQL语句")) {
    fprintf(stderr, "Query failed: %s\n", mysql_error(conn));
}

返回0表示成功,非0表示失败。就这么简单。

20.3.2 创建表(CREATE)

const char *sql = "CREATE TABLE IF NOT EXISTS students ("
                  "id INT AUTO_INCREMENT PRIMARY KEY,"
                  "name VARCHAR(50) NOT NULL,"
                  "age INT,"
                  "score FLOAT"
                  ")";

if (mysql_query(conn, sql)) {
    fprintf(stderr, "Create table failed: %s\n", mysql_error(conn));
}

这里用了 IF NOT EXISTS,防止重复创建时报错。我在写自动化脚本时经常这么用,省心。

20.3.3 插入数据(INSERT)

char sql[256];
snprintf(sql, sizeof(sql), 
         "INSERT INTO students (name, age, score) VALUES ('%s', %d, %.1f)",
         "张三", 20, 88.5);

if (mysql_query(conn, sql)) {
    fprintf(stderr, "Insert failed: %s\n", mysql_error(conn));
} else {
    printf("Insert success, affected rows: %lu\n", 
           (unsigned long)mysql_affected_rows(conn));
}
重要提醒: 上面的 snprintf 拼接SQL的方式存在SQL注入风险!如果name来自用户输入,一定要用 mysql_real_escape_string 转义。我在早期项目中吃过这个亏,后来全部改用参数化查询了。

20.3.4 查询数据(SELECT)

查询稍微复杂一点,因为要处理结果集:

if (mysql_query(conn, "SELECT id, name, age, score FROM students")) {
    fprintf(stderr, "Query failed: %s\n", mysql_error(conn));
    return;
}

MYSQL_RES *result = mysql_store_result(conn);
if (result == NULL) {
    fprintf(stderr, "Store result failed: %s\n", mysql_error(conn));
    return;
}

int num_fields = mysql_num_fields(result);
MYSQL_ROW row;

while ((row = mysql_fetch_row(result))) {
    for (int i = 0; i < num_fields; i++) {
        printf("%s\t", row[i] ? row[i] : "NULL");
    }
    printf("\n");
}

mysql_free_result(result);

流程很清晰:mysql_store_result 把结果拉到客户端内存,然后 mysql_fetch_row 一行一行取。用完记得 mysql_free_result,不然内存泄漏。

你想想看,如果结果集特别大,比如几十万行,全部拉到内存里会不会爆?会的。这时候可以用 mysql_use_result 逐行读取,不缓存全部结果。但要注意,用 mysql_use_result 时,你不能在读取完所有行之前执行其他查询。

20.3.5 更新数据(UPDATE)

if (mysql_query(conn, "UPDATE students SET score = 95.0 WHERE name = '张三'")) {
    fprintf(stderr, "Update failed: %s\n", mysql_error(conn));
} else {
    printf("Update success, affected rows: %lu\n", 
           (unsigned long)mysql_affected_rows(conn));
}

mysql_affected_rows 返回受影响的行数。如果返回0,说明没有匹配的记录。这在调试时很有用。

20.3.6 删除数据(DELETE)

if (mysql_query(conn, "DELETE FROM students WHERE id = 1")) {
    fprintf(stderr, "Delete failed: %s\n", mysql_error(conn));
} else {
    printf("Delete success, affected rows: %lu\n", 
           (unsigned long)mysql_affected_rows(conn));
}

20.4 完整示例:一个简单的学生管理系统

把上面的代码串起来,就是一个完整的小程序了。下面是一个精简版:

#include <stdio.h>
#include <mysql/mysql.h>

int main() {
    MYSQL *conn = mysql_init(NULL);
    if (conn == NULL) {
        fprintf(stderr, "mysql_init failed\n");
        return 1;
    }

    if (mysql_real_connect(conn, "localhost", "root", "password", 
                           "test_db", 3306, NULL, 0) == NULL) {
        fprintf(stderr, "Connection failed: %s\n", mysql_error(conn));
        mysql_close(conn);
        return 1;
    }

    // 创建表
    mysql_query(conn, "CREATE TABLE IF NOT EXISTS students (...)"); 

    // 插入
    mysql_query(conn, "INSERT INTO students VALUES (...)");

    // 查询
    MYSQL_RES *res = mysql_store_result(conn);
    // ... 处理结果 ...

    mysql_free_result(res);
    mysql_close(conn);
    return 0;
}

20.5 知识体系总览

下面这张图把本章的核心流程串起来了,建议你保存下来对照着看:

MySQL Connector/C 操作流程 1. 安装库 libmysqlclient-dev 2. mysql_init 分配MYSQL句柄 3. mysql_real_connect 建立TCP连接 4. CRUD 操作 (mysql_query) CREATE INSERT RETRIEVE SELECT UPDATE UPDATE DELETE DELETE 5. 清理资源 mysql_free_result / mysql_close

20.6 常见错误与调试

写C语言操作数据库,最怕的就是出错了不知道原因。这里总结几个我踩过的坑:

错误现象 可能原因 解决方法
Can't connect to MySQL server MySQL服务未启动,或端口不对 检查 systemctl status mysql,确认端口
Access denied for user 用户名或密码错误 检查用户权限,试试命令行能否登录
Unknown database 数据库不存在 CREATE DATABASE 再连接
Segmentation fault 空指针或未初始化 检查 mysql_init 返回值,检查结果集是否为空
调试技巧: 每次调用 mysql_query 后,都检查返回值并打印 mysql_error(conn)。我在代码里会写一个宏,比如 CHECK_SQL(conn, sql),自动处理错误输出,省得每次都写重复代码。

好了,这一章的内容就到这里。MySQL Connector/C 的接口其实不多,但每个细节都值得认真对待。尤其是内存管理和错误处理,C语言没有自动垃圾回收,全靠自己小心。


公众号:蓝海资料掘金营,微信deep3321