C++ 中使用 SQLite3 API 的经典流程
总结一下在 C++ 中使用 SQLite3 API 的经典流程
1. 核心概念#
在使用 C++ API 之前,需要理解 SQLite3 的几个核心对象和函数:
sqlite3
: 代表一个打开的数据库连接的句柄(handle)。所有针对该数据库的操作都需要这个对象。sqlite3_stmt
: 代表一个已经编译(prepared)的 SQL 语句。这是执行复杂查询和防止 SQL 注入的关键。- 返回码: SQLite3 的几乎所有函数都会返回一个整型状态码,例如
SQLITE_OK
表示成功,SQLITE_ERROR
表示通用错误,SQLITE_BUSY
表示数据库被锁定等。检查这些返回码是保证程序健壮性的关键。
2. 经典使用流程#
2.1 包含头文件和定义必要变量#
int main() {
sqlite3 *db; // 数据库句柄
char *errMsg = nullptr;
int rc; // 返回值
//...其他代码
}
cpp2.2 打开或创建数据库#
函数: int sqlite3_open(const char *filename, sqlite3 **ppDb)
作用: 打开一个数据库连接。
参数:
filename
: 数据库文件的路径。ppDb
: 一个指向sqlite3*
指针的指针,函数会把创建的数据库句柄写入这里。
// 创建或打开数据库
rc = sqlite3_open("test.db", &db);
if (rc) {
std::cerr << "Can't open database: " << sqlite3_errmsg(db) << std::endl;
return rc;
} else {
std::cout << "Opened database successfully" << std::endl;
}
cppsqlite3_errmsg(db)
是一个非常有用的函数,它可以获取最近一次操作失败的错误信息。
2.3 执行SQL语句(非查询类)#
对于不需要返回结果集的 SQL 语句,比如 CREATE TABLE
, INSERT
, UPDATE
, DELETE
,最简单的方式是使用 sqlite3_exec()
。
函数: int sqlite3_exec(sqlite3 *db, const char *sql, int (*callback)(void*,int,char**,char**), void *arg, char **errmsg)
作用: 一次性执行一个或多个 SQL 语句。
参数:
db
: 数据库句柄。sql
: 要执行的 SQL 字符串。callback
: 回调函数,每当查询产生一行结果时被调用(对于非查询语句,设为NULL
)。arg
: 传递给回调函数的第一个参数。errmsg
: 用于存放错误信息的指针。
// 创建一张表
const char *sql_create_table = "CREATE TABLE IF NOT EXISTS USERS("
"ID INT PRIMARY KEY NOT NULL,"
"NAME TEXT NOT NULL,"
"AGE INT NOT NULL);";
rc = sqlite3_exec(db, sql_create_table, nullptr, nullptr, &errMsg);
if (rc != SQLITE_OK) {
std::cerr << "SQL error: " << errMsg << std::endl;
sqlite3_free(errMsg); // 释放错误信息内存
} else {
std::cout << "Table created successfully" << std::endl;
}
// 插入数据
const char *sql_insert =
"INSERT INTO USERS (ID,NAME,AGE) VALUES (1, 'Alice', 25);"
"INSERT INTO USERS (ID,NAME,AGE) VALUES (2, 'Bob', 30);";
rc = sqlite3_exec(db, sql_insert, nullptr, nullptr, &errMsg);
if (rc != SQLITE_OK) {
std::cerr << "SQL error: " << errMsg << std::endl;
sqlite3_free(errMsg); // 释放错误信息内存
} else {
std::cout << "Records created successfully" << std::endl;
}
cpp2.4 执行查询并处理结果#
对于 SELECT
查询,虽然也可以使用 sqlite3_exec
配合回调函数,但更经典和推荐的做法是使用 prepare - step - finalize 流程。这种方式更灵活、更安全(可以防止 SQL 注入),并且效率更高。
2.4.1 准备SQL语句#
使用 sqlite3_prepare_v2()
将 SQL 语句编译成一个字节码对象 (sqlite3_stmt
)。
函数: int sqlite3_prepare_v2(sqlite3 *db, const char *zSql, int nByte, sqlite3_stmt **ppStmt, const char **pzTail)
作用: 编译 SQL 语句,为执行做准备。
参数:
db
: 数据库连接句柄zSql
: 要编译的SQL语句字符串bByte
:SQL字符串的字节长度,如果为 -1 SQLite 会自动计数ppStmt
: 输出参数,用于接收编译后的语句句柄pzTail
: 输出参数,指向SQL字符串中未被处理的部分,如果SQL字符串中包含多个语句,会返回第一个语句后剩余的部分,如果只有一个语句或者不关心剩余的部分,可以传入nullptr
sqlite3_stmt *stmt; // 语句句柄
const char *sql_select = "SELECT ID, NAME, AGE FROM USERS WHERE AGE > ?;";
// 准备SQL语句
rc = sqlite3_prepare_v2(db, sql_select, -1, &stmt, nullptr);
if (rc != SQLITE_OK) {
std::cerr << "Failed to prepare statement: " << sqlite3_errmsg(db) << std::endl;
sqlite3_close(db);
return rc;
}
cpp2.4.2 绑定参数#
如果 SQL 语句中使用了占位符 (?
),需要使用 sqlite3_bind_*()
系列函数来绑定具体的值。这可以有效防止 SQL 注入攻击。
// 绑定整数
int sqlite3_bind_int(sqlite3_stmt*, int, int);
// 绑定64位整数
int sqlite3_bind_int64(sqlite3_stmt*, int, sqlite3_int64);
// 绑定双精度浮点数
int sqlite3_bind_double(sqlite3_stmt*, int, double);
// 绑定空值
int sqlite3_bind_null(sqlite3_stmt*, int);
// 绑定文本字符串
int sqlite3_bind_text(sqlite3_stmt*, int, const char*, int, void(*)(void*));
// 绑定二进制数据
int sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int, void(*)(void*));
cpp这一系列的函数第一个参数为语句句柄,第二个参数为占位符索引(从1开始),第三个参数为值,第四个参数为一个析构函数指针,用于指定字符串的生命周期类型。这个参数可以为SQLITE_STATIC
和SQLITE_TRANSIENT
SQLITE_STATIC
:- 含义: 向 SQLite 保证,传入的这个字符串 (
zData
) 的内存是静态的、全局的、或者至少在sqlite3_stmt
整个生命周期内(直到sqlite3_finalize
被调用)都是有效且不会改变的。 - SQLite 的行为: 既然做了这个保证,SQLite 为了追求极致的性能,会直接使用传入的指针,而不会自己复制一份字符串数据。它只是持有了这个指针。
- 风险: 如果传入了一个局部变量(比如一个函数内的
std::string.c_str()
或者一个栈上的字符数组),当函数返回后,这个局部变量被销毁,内存被释放。但 SQLite 里的语句句柄stmt
还傻傻地存着那个已经无效的野指针。后面调用sqlite3_step()
去执行它时,它会尝试读取一个无效的内存地址,从而导致未定义行为,通常表现为程序崩溃、数据损坏或奇怪的错误。
- 含义: 向 SQLite 保证,传入的这个字符串 (
SQLITE_TRANSIENT
:- 含义: 这个宏告诉 SQLite,传入的这个字符串 (
zData
) 是临时的、短暂的 (transient),它的生命周期可能很短,在sqlite3_bind_text
函数调用返回后可能就失效了。 - SQLite 的行为: 收到这个信号后,SQLite 会变得很谨慎。它会在函数内部立即将字符串数据完整地复制一份,并由自己来管理这份拷贝的内存。这样,即使原始的字符串变量超出了作用域被销毁,也完全没关系,因为 SQLite 使用的是它自己的安全拷贝。
- 代价: 需要一次额外的内存分配和数据拷贝,相比
SQLITE_STATIC
会有微小的性能开销。但它换来的是安全和省心。
- 含义: 这个宏告诉 SQLite,传入的这个字符串 (
int age_limit = 20;
rc = sqlite3_bind_int(stmt, 1, age_limit);
if (rc != SQLITE_OK) {
std::cerr << "Failed to bind parameter: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
sqlite3_close(db);
return rc;
}
cpp2.4.3 执行语句并逐行获取结果#
使用 sqlite3_step()
来执行预处理语句。
- 函数:
int sqlite3_step(sqlite3_stmt *pStmt)
- 作用: 执行一次编译好的 SQL 语句。
- 返回值:
SQLITE_ROW
: 表示成功获取了一行数据,可以提取结果。SQLITE_DONE
: 表示所有行都已处理完毕。- 其他错误码。
当 sqlite3_step()
返回 SQLITE_ROW
时,使用 sqlite3_column_*()
系列函数来获取当前行各个字段的值。
- 函数:
sqlite3_column_int()
,sqlite3_column_text()
,sqlite3_column_double()
等。 - 作用: 获取查询结果中当前行的某一列数据。
// 循环执行step,逐行获取数据
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
int id = sqlite3_column_int(stmt, 0);
const unsigned char *name = sqlite3_column_text(stmt, 1);
int age = sqlite3_column_int(stmt, 2);
std::cout << "ID: " << id << ", Name: " << name << ", Age: " << age << std::endl;
}
if (rc != SQLITE_DONE) {
std::cerr << "Failed to execute statement: " << sqlite3_errmsg(db) << std::endl;
}
cpp2.5 销毁语句句柄#
查询结束后,必须使用 sqlite3_finalize()
来销毁 sqlite3_stmt
对象,释放资源。
- 函数:
int sqlite3_finalize(sqlite3_stmt *pStmt)
- 作用: 销毁预处理语句对象,回收内存。
// 释放语句句柄
sqlite3_finalize(stmt);
cpp2.6 关闭数据库#
所有操作完成后,使用 sqlite3_close()
关闭数据库连接,释放所有与该连接相关的资源。
- 函数:
int sqlite3_close(sqlite3 *db)
- 作用: 关闭数据库连接。
// 关闭数据库
sqlite3_close(db);
cpp3. 总结#
步骤 | 核心函数 | 主要作用 |
---|---|---|
1. 打开数据库 | sqlite3_open() | 获取数据库连接句柄 sqlite3* |
2. 执行非查询SQL | sqlite3_exec() | 简单快速地执行 CREATE , INSERT 等操作 |
3. 准备查询 | sqlite3_prepare_v2() | 编译 SELECT 语句,获取语句句柄 sqlite3_stmt* |
4. 绑定参数 | sqlite3_bind_*() | (可选) 为 ? 占位符安全地绑定值。 |
5. 逐行执行 | sqlite3_step() | 在循环中调用,每次获取一行结果 (SQLITE_ROW )。 |
6. 提取数据 | sqlite3_column_*() | 从当前行中按列索引提取数据。 |
7. 终结语句 | sqlite3_finalize() | 释放语句句柄 sqlite3_stmt* 的资源。 |
8. 关闭数据库 | sqlite3_close() | 释放数据库连接句柄 sqlite3* 的所有资源。 |