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包,解压后你会看到 include 和 lib 两个目录。我个人习惯把这两个目录放到项目文件夹里,然后在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 知识体系总览
下面这张图把本章的核心流程串起来了,建议你保存下来对照着看:
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语言没有自动垃圾回收,全靠自己小心。