#include <stdio.h>
#include <stdlib.h>
#include <sqlite3.h>
#include <string.h>
// 定义结构体存储用户数据(用于查询结果)
typedef struct {
char name[20];
char pswd[20];
} User;
// 回调函数:将查询结果写入主程序传递的数组
static int collect_data_callback(void *arg, int argc, char **argv, char **azColName) {
User *users = (User*)arg; // 转换为主程序传递的数组地址
int i = 0;
// 找到数组中第一个空位置(name为空表示未使用)
while (users[i].name[0] != '\0') {
i++;
}
// 存储当前行数据(处理NULL值)
if (argv[0] != NULL) strcpy(users[i].name, argv[0]);
if (argv[1] != NULL) strcpy(users[i].pswd, argv[1]);
return 0;
}
int main() {
sqlite3 *db; // 数据库句柄
char *zErrMsg = 0; // 错误信息缓冲区
int rc; // SQLite返回码
const char *sql; // SQL语句
char code_pswd[20]; // 用户输入的新密码
// 1. 打开数据库
rc = sqlite3_open("test.db", &db);
if (rc != SQLITE_OK) {
fprintf(stderr, "无法打开数据库: %s\n", sqlite3_errmsg(db));
return 1;
}
printf("数据库打开成功\n");
// 2. 创建表(若不存在)
sql = "CREATE TABLE IF NOT EXISTS tb("
"name TEXT PRIMARY KEY," // 主键:确保name唯一
"pswd TEXT NOT NULL" // 密码不能为空
");";
rc = sqlite3_exec(db, sql, NULL, NULL, &zErrMsg); // 创建表无结果,回调传NULL
if (rc != SQLITE_OK) {
fprintf(stderr, "创建表失败: %s\n", zErrMsg);
sqlite3_free(zErrMsg); // 释放错误信息内存
} else {
printf("表创建成功(若不存在)\n");
}
// 3. 插入初始数据(name='123', pswd='abcdefg')
sql = "INSERT INTO tb(name, pswd) VALUES('123', 'abcdefg');";
rc = sqlite3_exec(db, sql, NULL, NULL, &zErrMsg);
if (rc != SQLITE_OK) {
// 处理主键冲突(若记录已存在)
if (strstr(zErrMsg, "UNIQUE constraint failed") != NULL) {
printf("记录已存在,跳过插入操作\n");
} else {
fprintf(stderr, "插入数据失败: %s\n", zErrMsg);
}
sqlite3_free(zErrMsg);
} else {
printf("初始数据插入成功\n");
}
// 4. 用户输入新密码
printf("请输入新的密码: ");
scanf("%s", code_pswd); // 注意:实际应用需限制输入长度,避免缓冲区溢出
// 5. 构造并执行UPDATE语句
char update_sql[100]; // 足够存储SQL语句
sprintf(update_sql, "UPDATE tb SET pswd = '%s' WHERE name = '123';", code_pswd);
rc = sqlite3_exec(db, update_sql, NULL, NULL, &zErrMsg);
if (rc != SQLITE_OK) {
fprintf(stderr, "密码更新失败: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
} else {
printf("密码更新成功!\n");
// 【可选】查询数据,验证更新结果
User user_list[10] = {0}; // 存储查询结果(最多10条)
sql = "SELECT name, pswd FROM tb WHERE name = '123';";
rc = sqlite3_exec(db, sql, collect_data_callback, user_list, &zErrMsg);
if (rc == SQLITE_OK) {
printf("更新后的数据验证:\n");
for (int i = 0; i < 10; i++) {
if (user_list[i].name[0] == '\0') break; // 空数据则停止
printf("name: %s, 新密码: %s\n", user_list[i].name, user_list[i].pswd);
}
} else {
fprintf(stderr, "查询失败: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}
}
// 关闭数据库
sqlite3_close(db);
return 0;
}
这种 SQL 语句的构造方式(用 sprintf
将变量 code_pswd
拼接到 SQL 字符串中),核心目的是 把用户输入的 "动态密码" 嵌入到固定格式的 SQL 语句中,让 SQL 语句能根据用户输入的不同而变化。
一、为什么需要这样构造?
因为 code_pswd
是 用户输入的动态值(每次运行程序,用户输入的密码可能不同),而 SQL 语句本身需要明确的 "密码值" 才能执行更新。
- 如果用户输入
xyz
,需要执行的 SQL 是:UPDATE tb SET pswd = 'xyz' WHERE name = '123';
- 如果用户输入
123456
,需要执行的 SQL 是:UPDATE tb SET pswd = '123456' WHERE name = '123';
这两个 SQL 语句的结构完全相同,只有 pswd
的值不同(xyz
和 123456
)。因此,需要用 变量拼接 的方式,把用户输入的 code_pswd
动态填入 SQL 语句中。
二、sprintf
构造 SQL 的原理
sprintf
是 C 语言中用于 格式化字符串 的函数,作用是:按指定格式把数据 "拼" 到一个字符串里。
sprintf(update_sql, "UPDATE tb SET pswd = '%s' WHERE name = '123';", code_pswd);
- 第一个参数
update_sql
:存储拼接结果的字符串(最终的 SQL 语句)。 - 第二个参数:格式字符串,其中
%s
是 "字符串占位符",会被后续参数的值替换。 - 第三个参数
code_pswd
:用户输入的密码(比如xyz
),会替换%s
。
拼接后,update_sql
的内容就变成了:
UPDATE tb SET pswd = 'xyz' WHERE name = '123'; -- 假设用户输入 xyz
三、为什么要加单引号 '
?
SQL 语法规定:字符串类型的值必须用单引号包裹。
在表 tb
中,pswd
的类型是 TEXT
(字符串),因此赋值时必须写成 pswd = '新密码'
(单引号不可少)。
如果去掉单引号,SQL 会变成 pswd = xyz
,此时 SQLite 会把 xyz
当成 "列名" 而非 "字符串值",导致执行错误(提示 "无此列")。
四、这种方式的问题(重要!)
虽然这种拼接方式能实现功能,但存在 严重的 SQL 注入风险。
例如:如果用户输入的密码是 '; DROP TABLE tb; --
,拼接后的 SQL 会变成:
UPDATE tb SET pswd = ''; DROP TABLE tb; --' WHERE name = '123';
SQLite 会执行:
UPDATE tb SET pswd = ''
(清空所有密码);DROP TABLE tb
(删除整个表);--
后面的内容被注释,失效。
最终会导致表被删除,数据丢失。
五、正确的做法(参数化查询)
实际开发中,必须用 参数化查询(避免拼接字符串),示例如下:
// 用 ? 作为占位符,代替直接拼接变量
const char *update_sql = "UPDATE tb SET pswd = ? WHERE name = '123';";
sqlite3_stmt *stmt;
// 准备语句(编译 SQL)
rc = sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
// 绑定参数:把 code_pswd 填入第一个占位符(?)
sqlite3_bind_text(stmt, 1, code_pswd, -1, SQLITE_TRANSIENT);
// 执行语句
rc = sqlite3_step(stmt);
}
// 释放资源
sqlite3_finalize(stmt);
参数化查询中,?
是安全的占位符,code_pswd
会被当作 "纯值" 处理,不会被解析为 SQL 指令,彻底避免注入风险。
总结
代码中用 sprintf
构造 SQL 的原因是:
- 需要将用户输入的动态密码(
code_pswd
)嵌入 SQL 语句; - 用
%s
占位并拼接,是简单直接的字符串格式化方式; - 单引号是 SQL 语法要求(字符串值必须包裹)。
但这只是 演示级写法 ,实际开发必须用参数化查询(sqlite3_prepare_v2
+ sqlite3_bind_*
)避免 SQL 注入。