第33章:C语言与PostgreSQL基础:libpq库
说实话,很多C程序员一提到数据库就发怵。总觉得那是Java、Python的天下。但我在嵌入式领域摸爬滚打这么多年,可以负责任地告诉你:C语言操作PostgreSQL,不仅可行,而且非常高效。libpq就是PostgreSQL官方提供的C语言接口,轻量、稳定、功能完整。今天我就带你把它吃透。
1. libpq 是什么?为什么选它?
libpq 是 PostgreSQL 自带的 C 语言客户端库。说白了,它就是一座桥,让你的 C 程序能和 PostgreSQL 服务器对话。我最早接触它是在一个工业数据采集项目里,设备端只有 C 环境,还要实时写入传感器数据到数据库。当时试过 ODBC,太重了。换成 libpq,代码量直接砍半。
它的优点很突出:
- 原生支持:PostgreSQL 官方维护,版本同步更新。
- 轻量级:没有多余依赖,编译出来就几百 KB。
- 功能完整:连接管理、SQL 执行、事务、异步查询,全都有。
- 线程安全:每个连接对象独立,多线程场景下很友好。
-lpq,头文件是 #include <libpq-fe.h>。如果你用 CMake,可以加 find_package(PostgreSQL),省心很多。
2. PQconnectdb — 建立连接
连接数据库是第一步。函数原型很简单:
PGconn *PQconnectdb(const char *conninfo);
conninfo 是一个字符串,里面用 key=value 的形式传参数。我最常用的写法是:
PGconn *conn = PQconnectdb("host=127.0.0.1 port=5432 dbname=testdb user=myuser password=mypass");
注意,这个函数不会阻塞太久。它内部会尝试连接,但超时机制由 PostgreSQL 服务端控制。我个人习惯在连接后立刻检查状态:
if (PQstatus(conn) != CONNECTION_OK) {
fprintf(stderr, "连接失败: %s", PQerrorMessage(conn));
PQfinish(conn);
return -1;
}
我曾经在一个项目中忘了检查连接状态,结果后续的 PQexec 全部返回空指针,排查了半天才发现是连接串里密码写错了。嗯,从那以后我每次连接完都会加一句检查,成了肌肉记忆。
conninfo 中不要硬编码敏感信息。生产环境建议从配置文件或环境变量读取。另外,如果连接失败,PQerrorMessage 会给出详细原因,一定要打出来看看。
3. PQexec — 执行SQL
连接建立后,就可以用 PQexec 执行 SQL 了。它的签名是:
PGresult *PQexec(PGconn *conn, const char *command);
command 可以是任何 SQL 语句:SELECT、INSERT、UPDATE、DELETE,甚至 CREATE TABLE。返回的 PGresult 指针包含了执行结果的所有信息。
举个例子,插入一条数据:
PGresult *res = PQexec(conn, "INSERT INTO sensors (id, value) VALUES (1, 23.5)");
if (PQresultStatus(res) != PGRES_COMMAND_OK) {
fprintf(stderr, "INSERT 失败: %s", PQerrorMessage(conn));
PQclear(res);
// 处理错误
} else {
printf("插入成功,影响行数: %s", PQcmdTuples(res));
}
PQclear(res);
你想想看,这里有个很容易踩的坑:PQexec 执行后,必须调用 PQclear 释放结果对象。否则内存会一直涨。我在做长连接服务时遇到过,跑了一周后内存占用飙升到几个 GB,最后定位到就是忘了 PQclear。血的教训。
PQresultStatus 返回的是 PGRES_TUPLES_OK,而不是 PGRES_COMMAND_OK。这两个要区分开。
4. PQgetvalue — 获取查询结果
执行 SELECT 之后,怎么把数据取出来?PQgetvalue 就是干这个的。
char *PQgetvalue(const PGresult *res, int row, int col);
它返回指定行、指定列的文本值。注意,返回的是 char*,所有数据都以字符串形式给出。你需要自己转换成 int、float 等类型。
一个完整的查询示例:
PGresult *res = PQexec(conn, "SELECT id, name, temperature FROM sensors ORDER BY id");
if (PQresultStatus(res) != PGRES_TUPLES_OK) {
fprintf(stderr, "查询失败: %s", PQerrorMessage(conn));
PQclear(res);
return;
}
int rows = PQntuples(res);
int cols = PQnfields(res);
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%s\t", PQgetvalue(res, i, j));
}
printf("\n");
}
PQclear(res);
这里有几个辅助函数很实用:
| 函数 | 作用 |
|---|---|
PQntuples(res) | 返回结果集的行数 |
PQnfields(res) | 返回结果集的列数 |
PQfname(res, col) | 返回指定列的字段名 |
PQgetisnull(res, row, col) | 判断某个字段是否为 NULL |
我个人习惯在遍历结果前,先用 PQnfields 和 PQfname 打印一个表头,这样输出更清晰。另外,如果某个字段可能为 NULL,一定要用 PQgetisnull 判断,否则 PQgetvalue 会返回空字符串,容易混淆。
PQgetvalue 返回的字符串是内部缓冲区的指针,不要手动 free。它的生命周期到 PQclear 为止。如果你需要长期保存,记得用 strdup 复制一份。
5. 完整示例:从连接到查询
下面我把上面几个步骤串起来,写一个完整的 demo。这个程序连接本地数据库,查询 sensors 表并打印结果。
#include <stdio.h>
#include <libpq-fe.h>
int main() {
// 1. 连接数据库
PGconn *conn = PQconnectdb("host=127.0.0.1 port=5432 dbname=testdb user=myuser password=mypass");
if (PQstatus(conn) != CONNECTION_OK) {
fprintf(stderr, "连接失败: %s", PQerrorMessage(conn));
PQfinish(conn);
return 1;
}
printf("连接成功!\n");
// 2. 执行查询
PGresult *res = PQexec(conn, "SELECT id, name, value FROM sensors ORDER BY id");
if (PQresultStatus(res) != PGRES_TUPLES_OK) {
fprintf(stderr, "查询失败: %s", PQerrorMessage(conn));
PQclear(res);
PQfinish(conn);
return 1;
}
// 3. 获取并打印结果
int rows = PQntuples(res);
int cols = PQnfields(res);
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%s", PQgetvalue(res, i, j));
if (j < cols - 1) printf(", ");
}
printf("\n");
}
// 4. 清理
PQclear(res);
PQfinish(conn);
return 0;
}
编译命令:
gcc -o pg_demo pg_demo.c -lpq
运行前确保 PostgreSQL 服务已启动,并且 testdb 数据库和 sensors 表已存在。如果一切顺利,你会看到类似这样的输出:
连接成功!
1, 温度传感器, 23.5
2, 湿度传感器, 68.2
6. 避坑指南与个人经验
最后,我把自己这些年用 libpq 踩过的坑总结一下,希望能帮你少走弯路。
- 连接字符串格式:key=value 之间用空格分隔,不要加多余引号。我曾经因为写了
password="mypass"导致连接失败,去掉引号就好了。 - 错误处理不能偷懒:每个
PQexec之后都要检查PQresultStatus。不要假设 SQL 一定会成功。 - 内存泄漏:
PGresult用完后必须PQclear。PGconn用完后必须PQfinish。这是铁律。 - NULL 处理:查询结果中如果某个字段是 NULL,
PQgetvalue返回空字符串"",但PQgetisnull会返回 1。一定要用后者判断。 - 多线程注意:每个线程使用独立的
PGconn对象,不要共享连接。libpq 的库函数本身是线程安全的,但连接对象不是。
PQexec 拼接用户输入的 SQL 字符串。这会导致 SQL 注入攻击。请使用 PQexecParams 配合参数化查询,或者用 PQescapeStringConn 转义。安全无小事。
好了,关于 libpq 的基础用法就讲到这里。这三个函数——PQconnectdb、PQexec、PQgetvalue——是你和 PostgreSQL 打交道的三板斧。把它们练熟,大部分日常需求都能搞定。下次遇到更复杂的场景,比如异步查询、批量插入、二进制传输,我们再慢慢聊。