数据库的创建与连接:SQLite 的打开、关闭与错误处理
说实话,做嵌入式数据库开发这么多年,我见过太多新手一上来就急着写 SQL 语句,结果连数据库都没打开成功。嗯,这就像你还没开门就想进屋——门都打不开,后面的事全是白搭。
今天咱们就踏踏实实把「开门关门」这件事聊透。SQLite 的数据库创建与连接,说白了就是三个核心动作:打开文件、操作数据、关闭文件。但这里面的坑,我当年可没少踩。
1. 核心 API:sqlite3_open 与 sqlite3_close
SQLite 的 C 接口非常简洁。你只需要记住两个函数:
sqlite3_open()— 打开或创建数据库文件sqlite3_close()— 关闭数据库连接
它们的函数签名长这样:
int sqlite3_open(
const char *filename, // 数据库文件路径(UTF-8)
sqlite3 **ppDb // 输出参数:数据库连接句柄
);
int sqlite3_close(sqlite3 *db);
这里有个细节我特别想强调:sqlite3_open 的第二个参数是 sqlite3 **,也就是二级指针。为什么?因为函数内部要分配内存,然后把地址写回到你的指针变量里。我第一次用的时候忘了取地址,传了个空指针进去,结果程序直接崩溃——嗯,那会儿我还以为是 SQLite 的 bug,后来才发现是自己菜。
关键行为:
- 如果文件不存在,
sqlite3_open会自动创建它 - 如果文件已存在,它会直接打开
- 打开成功后,必须用
sqlite3_close关闭,否则会内存泄漏
2. 第一个完整示例:创建并打开数据库
咱们直接上代码。我个人习惯把错误检查放在每一步之后,这样出问题能立刻定位。
#include <stdio.h>
#include <sqlite3.h>
int main() {
sqlite3 *db = NULL;
int rc;
// 打开(或创建)数据库文件
rc = sqlite3_open("test.db", &db);
if (rc != SQLITE_OK) {
fprintf(stderr, "打开数据库失败: %s\n", sqlite3_errmsg(db));
return 1;
}
printf("数据库打开成功!\n");
// ... 在这里执行你的 SQL 操作 ...
// 关闭数据库
sqlite3_close(db);
printf("数据库已关闭。\n");
return 0;
}
编译的时候记得链接 SQLite 库:
gcc -o demo demo.c -lsqlite3
运行后,你会看到当前目录下多了一个 test.db 文件。这就是你的数据库文件。
小技巧: 如果你不想让数据库文件散落在各处,可以指定一个固定路径,比如 /var/data/myapp.db。但要注意目录权限——我曾经在嵌入式设备上折腾了半天,最后发现是目录不可写。
3. 错误处理机制:别让程序静默死掉
很多初学者写 SQLite 代码时,只检查返回值是不是 SQLITE_OK,其他情况一概忽略。这其实很危险。你想想看,如果磁盘满了、文件权限不对、或者数据库损坏了,程序还在那假装一切正常,最后数据丢了都不知道。
SQLite 提供了两个非常有用的错误信息函数:
sqlite3_errmsg(db)— 返回最后一次错误的文本描述sqlite3_errcode(db)— 返回最后一次错误的数值代码
我建议你封装一个通用的错误处理宏,这样代码会干净很多:
#define CHECK_SQLITE(rc, db, msg) \
if (rc != SQLITE_OK) { \
fprintf(stderr, "%s: %s (错误码: %d)\n", msg, sqlite3_errmsg(db), rc); \
sqlite3_close(db); \
return rc; \
}
用了这个宏之后,你的代码会变成这样:
rc = sqlite3_open("test.db", &db);
CHECK_SQLITE(rc, db, "打开数据库失败");
// 后续操作同理
rc = sqlite3_exec(db, "CREATE TABLE IF NOT EXISTS users(id INT, name TEXT)", 0, 0, 0);
CHECK_SQLITE(rc, db, "创建表失败");
避坑指南: 我曾经在项目里犯过一个低级错误——在 sqlite3_open 失败后,直接调用 sqlite3_errmsg(db),但此时 db 还是 NULL。结果程序直接段错误。正确的做法是:如果 sqlite3_open 返回非 SQLITE_OK,不要用 sqlite3_errmsg(db),而是用 sqlite3_errstr(rc) 来获取错误描述。
4. 打开模式:不只是简单的读写
你以为 sqlite3_open 只有一种打开方式?其实 SQLite 提供了多种打开模式,通过 sqlite3_open_v2() 来控制:
| 标志 | 含义 | 适用场景 |
|---|---|---|
SQLITE_OPEN_READONLY |
只读模式 | 查询数据,不修改 |
SQLITE_OPEN_READWRITE |
读写模式,文件必须存在 | 修改已有数据库 |
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE |
读写模式,文件不存在则创建 | 默认行为,最常用 |
举个例子,如果你只想读取一个配置文件,不希望误操作修改它,可以用只读模式:
sqlite3 *db;
int rc = sqlite3_open_v2("config.db", &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "只读打开失败: %s\n", sqlite3_errstr(rc));
return 1;
}
这样即使后面代码里有写操作,SQLite 也会直接报错,不会偷偷修改你的数据。我个人在嵌入式设备上做固件升级时,就喜欢用只读模式打开旧数据库,防止升级过程中意外写坏。
5. 知识体系图:数据库生命周期
下面这张图总结了数据库从创建到关闭的完整流程,以及错误处理的关键节点:
6. 完整示例:带错误处理的数据库创建与关闭
最后,咱们把上面所有知识点揉在一起,写一个完整的示例。这个代码我在实际项目里用了很多年,基本结构没变过:
#include <stdio.h>
#include <sqlite3.h>
int main() {
sqlite3 *db = NULL;
int rc;
// 1. 打开数据库(如果不存在则创建)
rc = sqlite3_open("myapp.db", &db);
if (rc != SQLITE_OK) {
// 注意:此时 db 可能为 NULL,不能用 sqlite3_errmsg(db)
fprintf(stderr, "无法打开数据库: %s\n", sqlite3_errstr(rc));
return 1;
}
printf("数据库 myapp.db 打开/创建成功\n");
// 2. 创建一个简单的表
const char *sql = "CREATE TABLE IF NOT EXISTS sensor_data ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"temperature REAL,"
"humidity REAL,"
"timestamp TEXT DEFAULT (datetime('now'))"
");";
rc = sqlite3_exec(db, sql, 0, 0, 0);
if (rc != SQLITE_OK) {
fprintf(stderr, "创建表失败: %s\n", sqlite3_errmsg(db));
sqlite3_close(db);
return 1;
}
printf("表 sensor_data 创建成功(或已存在)\n");
// 3. 插入一条测试数据
sql = "INSERT INTO sensor_data (temperature, humidity) VALUES (25.3, 60.1);";
rc = sqlite3_exec(db, sql, 0, 0, 0);
if (rc != SQLITE_OK) {
fprintf(stderr, "插入数据失败: %s\n", sqlite3_errmsg(db));
sqlite3_close(db);
return 1;
}
printf("测试数据插入成功\n");
// 4. 关闭数据库
rc = sqlite3_close(db);
if (rc != SQLITE_OK) {
fprintf(stderr, "关闭数据库时出错: %s\n", sqlite3_errstr(rc));
return 1;
}
printf("数据库已安全关闭\n");
return 0;
}
个人经验: 我建议你在每个 sqlite3_close 之前都检查一下返回值。虽然大多数情况下关闭不会失败,但我在嵌入式设备上遇到过磁盘写保护导致关闭失败的情况——如果不检查,你根本不知道数据有没有完整写入。
好了,数据库的创建与连接就聊到这儿。记住三个核心点:打开要检查、操作要检查、关闭也要检查。把这三步做好,你的数据库程序就稳了一大半。