演示数据库操作

复制代码
#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 的值不同(xyz123456)。因此,需要用 变量拼接 的方式,把用户输入的 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 会执行:

  1. UPDATE tb SET pswd = ''(清空所有密码);
  2. DROP TABLE tb(删除整个表);
  3. -- 后面的内容被注释,失效。

最终会导致表被删除,数据丢失。

五、正确的做法(参数化查询)

实际开发中,必须用 参数化查询(避免拼接字符串),示例如下:

复制代码
// 用 ? 作为占位符,代替直接拼接变量
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 的原因是:

  1. 需要将用户输入的动态密码(code_pswd)嵌入 SQL 语句;
  2. %s 占位并拼接,是简单直接的字符串格式化方式;
  3. 单引号是 SQL 语法要求(字符串值必须包裹)。

但这只是 演示级写法 ,实际开发必须用参数化查询(sqlite3_prepare_v2 + sqlite3_bind_*)避免 SQL 注入。

相关推荐
装不满的克莱因瓶8 分钟前
什么是脏读、幻读、不可重复读?Mysql的隔离级别是什么?
数据库·mysql·事务·隔离级别·不可重复读·幻读·脏读
百***920222 分钟前
java进阶1——JVM
java·开发语言·jvm
aramae34 分钟前
MySQL数据库入门指南
android·数据库·经验分享·笔记·mysql
Pluchon1 小时前
硅基计划6.0 柒 JavaEE 浅谈JVM&GC垃圾回收
java·jvm·数据结构·java-ee·gc
Apache IoTDB1 小时前
时序数据库 IoTDB 集成 MyBatisPlus,告别复杂编码,简化时序数据 ORM 开发
数据库·struts·servlet·时序数据库·iotdb
isNotNullX2 小时前
怎么用数据仓库来进行数据治理?
大数据·数据库·数据仓库·数据治理
小坏讲微服务2 小时前
Spring Cloud Alibaba Gateway 集成 Redis 限流的完整配置
数据库·redis·分布式·后端·spring cloud·架构·gateway
whb2341741242 小时前
测试linux删除Oracle文件,使用文件句柄恢复
linux·运维·oracle
HitpointNetSuite2 小时前
连锁餐饮行业ERP系统如何选择?
大数据·数据库·oracle·netsuite·erp
百***17072 小时前
MySQL 常用 SQL 语句大全
数据库·sql·mysql