搜索

C++ 教程

C++基础

第1章 搭建开发环境与第一个程序 第2章 基本数据类型与变量 第3章 运算符与表达式 第4章 流程控制语句 第5章 数组与字符串 第6章 函数基础 第7章 指针基础 第8章 引用与const限定 第9章 动态内存管理 第10章 结构体与联合体 第11章 函数进阶

面向对象

第12章 面向对象编程入门 第13章 封装与访问控制 第14章 运算符重载 第15章 继承与派生 第16章 多态与虚函数

STL与进阶

第17章 模板编程基础 第18章 标准模板库(STL)容器 第19章 STL算法与迭代器 第20章 异常处理机制 第21章 文件输入输出 第22章 字符串类string 第23章 命名空间与作用域 第24章 类型转换操作符

现代C++

第25章 智能指针 第26章 移动语义与右值引用 第27章 Lambda表达式 第28章 多线程编程基础 第29章 模板元编程入门 第30章 设计模式基础 第31章 C++11/14新特性 第32章 C++17/20核心特性

实战进阶

第33章 性能优化技巧 第34章 调试与测试 第35章 项目结构与构建 第36章 网络编程入门 第37章 数据库连接操作 第38章 图形界面开发基础 第39章 综合项目实战 第40章 进阶学习路线

第37章 数据库连接操作

第37章 数据库连接操作

高级 👤 已掌握C++核心语法与网络基础,需要开发数据驱动的桌面或服务端应用 🔧 为C++程序接入MySQL、SQLite等数据库,实现数据的持久化存储与查询
本章你将学到
  • 使用SQLite3进行轻量级本地数据存储
  • 通过MySQL Connector/C++连接远程数据库服务器
  • 编写安全的SQL语句并处理查询结果集
核心概念ODBC预处理语句

你的C++程序需要记住用户信息、商品库存或者游戏存档,这些数据在程序关闭后不能消失。把数据存进文件是一种方式,但当数据关系复杂、查询频繁时,文件操作会变得笨重且低效。这时,你需要一个专业的“数据仓库管理员”——数据库。

数据库连接是什么?

生活类比: 就像你的手机通过Wi-Fi路由器连接互联网,C++程序需要通过一个“数据库驱动”连接到数据库服务器,建立一条传输SQL指令和接收数据的通道。技术定义: 数据库连接是应用程序与数据库管理系统(DBMS)之间建立的一个会话(Session),通过这个会话,程序可以执行SQL命令并获取结果。

在C++中,没有像Java的JDBC那样的标准数据库接口。我们主要通过与数据库厂商提供的原生C语言库(如SQLite)或通用接口封装库(如MySQL Connector/C++、ODBC)进行交互。本章将聚焦于两个最典型的场景:嵌入式数据库SQLite和客户端-服务器数据库MySQL。

37.1 连接方案选型:嵌入式 vs 客户端-服务器

选择哪种数据库,取决于你的应用场景。我在一个桌面工具项目中,因为用户环境无法保证安装MySQL服务,果断选择了SQLite,省去了大量的部署麻烦。

特性SQLite (嵌入式)MySQL (客户端-服务器)
架构库直接链接到程序中,数据库是单个文件(如 .db)程序通过网络协议(如TCP/IP)连接独立的数据库服务进程
部署复杂度零配置,无需安装独立服务,随程序分发需安装并配置数据库服务器,管理用户权限
典型应用桌面应用、移动应用、嵌入式设备、本地缓存Web后端、企业级应用、多用户并发访问的系统
性能特点本地文件IO,无网络延迟,轻量级事务支持高并发连接、复杂查询优化、主从复制
C++集成方式直接包含 `sqlite3.h`,链接 `sqlite3.lib` 或 `-lsqlite3`使用 `mysqlx` 或 `mysql.h` (C API),需链接 `mysqlcppconn` 等库

关键决策点

如果你的应用是单机版、用户数据独立存储(如每个用户一个配置文件),选SQLite。如果是中心化的服务端程序,需要多个客户端同时访问和修改同一份数据,必须选MySQL或同类数据库。实际开发中,很多团队在开发测试阶段用SQLite,上线部署再用MySQL,但要注意两者SQL方言的细微差别。

37.2 实战SQLite3:从连接到查询

让我们从一个具体任务开始:创建一个用户表,并插入一条数据。SQLite3是一个C语言库,在C++中可以直接使用。首先需要获取库文件,可以从官网下载 sqlite-amalgamation-3410200.zip(包含源码)和预编译的DLL或SO文件。

你的C++程序
→
sqlite3.lib/.so/.dylib
→
user_data.db 文件
↓
打开/关闭连接
↗
执行SQL
↘
处理结果集

连接SQLite数据库的本质是打开一个文件句柄。核心对象是 `sqlite3*`,它代表一个数据库连接。

// 示例:打开或创建一个数据库文件
#include <iostream>
#include "sqlite3.h" // 包含头文件

int main() {
    sqlite3 *db = nullptr;
    const char *db_file = "myapp.db";

    // 打开数据库连接。如果文件不存在,SQLite会创建它。
    int rc = sqlite3_open(db_file, &db);

    if(rc != SQLITE_OK) {
        std::cerr << "无法打开数据库: " << sqlite3_errmsg(db) << std::endl;
        return 1;
    }
    std::cout << "数据库连接成功!" << std::endl;

    // ... 执行SQL操作

    sqlite3_close(db); // 必须关闭连接,释放资源
    return 0;
}

编译这个程序需要链接SQLite3库。在Linux/macOS上通常是 `g++ -o app app.cpp -lsqlite3`,在Windows的Visual Studio中,需要在项目属性中添加 `sqlite3.lib` 的附加依赖项。

⚠️ 常见误区

  • 忘记检查返回值: `sqlite3_open` 返回 `SQLITE_OK` (值为0) 才表示成功。我踩过这个坑,没检查返回值,后续所有操作都失败,却花了半天查别的原因。
  • 内存泄漏: 每个 `sqlite3_open` 必须对应一个 `sqlite3_close`。在复杂函数中,如果提前返回,务必确保关闭了连接。
  • 路径问题: 传入的相对路径 `"./data.db"` 是相对于程序运行时的当前工作目录,不是源代码目录。最好使用绝对路径或明确管理工作目录。

37.3 执行SQL与处理结果

打开连接后,就可以发送SQL命令了。最简单的是执行不返回数据的DDL(创建表)或DML(插入、更新、删除)语句,使用 `sqlite3_exec` 函数。

// 创建用户表
const char *sql_create = "CREATE TABLE IF NOT EXISTS users ("
                          "id INTEGER PRIMARY KEY AUTOINCREMENT, "
                          "name TEXT NOT NULL, "
                          "age INTEGER);";

char *err_msg = nullptr;
rc = sqlite3_exec(db, sql_create, nullptr, nullptr, &err_msg);
if(rc != SQLITE_OK) {
    std::cerr << "SQL错误: " << err_msg << std::endl;
    sqlite3_free(err_msg); // 错误信息内存需要手动释放!
}

当需要执行查询(SELECT)并处理返回的多行数据时,`sqlite3_exec` 就不够灵活了。你需要使用 `sqlite3_prepare_v2`, `sqlite3_step`, `sqlite3_column_*` 这一套组合拳。

sqlite3_prepare_v2
→
循环 sqlite3_step
→
用 sqlite3_column_* 取数据
→
sqlite3_finalize
// 查询所有用户
const char *sql_select = "SELECT id, name, age FROM users;";
sqlite3_stmt *stmt = nullptr; // 预处理语句句柄

rc = sqlite3_prepare_v2(db, sql_select, -1, &stmt, nullptr);
if(rc != SQLITE_OK) { /* 处理错误 */ }

while((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
    // 按列索引(从0开始)或列名获取数据
    int id = sqlite3_column_int(stmt, 0);
    const unsigned char *name = sqlite3_column_text(stmt, 1); // TEXT类型
    int age = sqlite3_column_int(stmt, 2);
    std::cout << "ID: " << id << ", Name: " << name << ", Age: " << age << std::endl;
}
if(rc != SQLITE_DONE) { /* 处理非正常结束的错误 */ }

sqlite3_finalize(stmt); // 释放预处理语句资源

编者提示: 处理从数据库获取的文本数据(`sqlite3_column_text`)时,其返回的是指向SQLite内部缓冲区的指针,该数据仅在当前 `sqlite3_step` 调用期间有效。如果你需要持久化这个字符串,请立即复制它(例如使用 `std::string`)。这与你在第22章学习的 `std::string` 的内存管理逻辑不同,务必注意。

37.4 连接MySQL:使用Connector/C++

当你的数据需要被网络上的多个客户端共享时,就需要像MySQL这样的数据库服务器。Oracle官方提供了MySQL Connector/C++ 8.0,这是一个纯C++的驱动库,比老的C API更现代、更安全。

首先,你需要安装:1) MySQL服务器(如MySQL Community Server 8.0.36);2) Connector/C++库。在Ubuntu上可以 `apt install libmysqlcppconn8-2`,在Windows上需要从官网下载安装包并配置头文件和库路径。

预处理语句(Prepared Statement)

生活类比: 就像填空题模板,“查找名叫____的人”,你只需要填入不同的名字,模板结构不变。这比每次手写整个句子(SQL语句)更安全高效。技术定义: 预处理语句是预先编译好的SQL模板,其中参数用占位符表示。它可以防止SQL注入,并且对于重复执行(仅参数不同)的语句,性能更高。

使用Connector/C++连接MySQL的代码风格更面向对象。核心类是 `sql::mysql::MySQL_Driver`, `sql::Connection`, `sql::Statement`, `sql::PreparedStatement` 和 `sql::ResultSet`。

#include <iostream>
#include <mysqlx/xdevapi.h> // Connector/C++ 8.0 的主要头文件
// 注意:新版本推荐使用X DevAPI,老版本是 `#include <cppconn/driver.h>`

int main() {
    try {
        // 1. 创建会话(连接)
        mysqlx::Session sess("localhost", 3306, "your_username", "your_password", "your_database");
        std::cout << "连接到MySQL服务器成功!" << std::endl;

        // 2. 获取数据库模式(Schema)
        mysqlx::Schema db = sess.getSchema("your_database");

        // 3. 执行一个简单查询(不推荐,仅演示)
        mysqlx::SqlResult result = sess.sql("SHOW TABLES;").execute();
        auto rows = result.fetchAll();
        for(auto row : rows) {
            std::cout << row[0] << std::endl;
        }

        // 4. 使用预处理语句安全地插入数据(推荐)
        mysqlx::TableInsert insert_op = db.getTable("users").insert("name", "age");
        insert_op.values("Alice", 25).values("Bob", 30);
        insert_op.execute();
        std::cout << "数据插入成功。" << std::endl;

        sess.close();
    } catch (const mysqlx::Error &err) {
        std::cerr << "MySQL错误: " << err.what() << std::endl;
        return 1;
    } catch (const std::exception &ex) {
        std::cerr << "标准异常: " << ex.what() << std::endl;
        return 1;
    }
    return 0;
}

编译时需要链接 `mysqlcppconn8` 库。例如:`g++ -o mysql_app mysql_app.cpp -lmysqlcppconn8`。如果你看到链接错误“undefined reference to `mysqlx::Session::Session(...)`”,大概率是库路径没设置对或者库文件名不对。

简单脚本/工具

  • 选择: SQLite
  • 理由: 无需额外服务,单文件部署
  • 注意: 文件锁,不支持高并发写

桌面应用程序

  • 选择: SQLite
  • 理由: 用户数据本地存储,安装简单
  • 注意: 数据库文件可能被用户误删

游戏服务器后端

  • 选择: MySQL/PostgreSQL
  • 理由: 需要处理数百上千个并发连接,数据强一致
  • 注意: 需设计连接池,优化查询

数据采集与缓存

  • 选择: SQLite + 定期同步到MySQL
  • 理由: 本地快速写入,批量上传
  • 注意: 处理同步冲突和网络异常

37.5 错误处理与资源管理

数据库操作充满了不确定性:网络可能断开,磁盘可能写满,SQL可能有语法错误。健壮的程序必须处理这些异常。

对于SQLite,每个函数调用后检查返回值是必须的。对于MySQL Connector/C++,它使用C++异常机制,所以必须用 `try-catch` 块包裹可能出错的代码。一个重要的实践是使用RAII(Resource Acquisition Is Initialization)思想来管理数据库资源,确保在任何情况下(包括异常抛出)连接都能被正确关闭。

// 一个简单的RAII包装器示例,用于SQLite连接
class SQLiteDB {
public:
    SQLiteDB(const std::string &filename) {
        if(sqlite3_open(filename.c_str(), &db_) != SQLITE_OK) {
            throw std::runtime_error(sqlite3_errmsg(db_));
        }
    }
    ~SQLiteDB() {
        if(db_) sqlite3_close(db_);
    }
    // 删除拷贝构造和赋值,防止重复关闭
    SQLiteDB(const SQLiteDB&) = delete;
    SQLiteDB& operator=(const SQLiteDB&) = delete;

    operator sqlite3*() { return db_; } // 提供获取原始指针的方法
private:
    sqlite3 *db_ = nullptr;
};

// 使用:当sqlite_db对象离开作用域时,析构函数会自动关闭连接
try {
    SQLiteDB sqlite_db("data.db");
    // 使用 sqlite_db 进行操作...
} catch(const std::exception &e) {
    std::cerr << e.what() << std::endl;
}

对于MySQL,Connector/C++的 `Session` 对象本身已经实现了RAII,在其析构时会自动关闭连接。但你还是应该主动在 `try-catch` 块中调用 `close()`,以便更早地释放服务器资源并捕获可能的关闭错误。

动手试一试

1. 搭建SQLite环境: 从 sqlite.org 下载“Precompiled Binaries for Windows”的 `sqlite-tools-win32-*.zip` 和“Source Code”的 `sqlite-amalgamation-*.zip`。解压后者得到 `sqlite3.h` 和 `sqlite3.c`。创建一个控制台项目,将 `sqlite3.c` 加入源文件,包含 `sqlite3.h`,编写代码创建一个 `students.db` 数据库,并包含 `id`(主键)、`name`、`score` 三个字段的表格。
2. 实现CRUD: 在上面项目的基础上,编写函数实现插入3个学生记录、根据ID查询一个学生、更新某个学生的分数、删除某个学生的功能。将所有数据库操作封装在一个类中。
3. (选做)连接MySQL: 如果你有可用的MySQL服务器(可以安装XAMPP或WAMP套件快速获得),尝试使用MySQL Connector/C++ 8.0连接它,并执行一个简单的 `SELECT NOW();` 查询,将服务器时间打印到控制台。

检验你的理解

  1. SQLite的数据库文件后缀通常是什么?一个SQLite连接(`sqlite3*`)对应一个文件还是多个文件?
  2. 为什么使用预处理语句(如 `sqlite3_prepare_v2`)比直接拼接SQL字符串(如 `sqlite3_exec`)更安全?请从“SQL注入攻击”的角度解释。
  3. 在MySQL Connector/C++的代码中,如果连接字符串中的密码错误,程序会在哪一行抛出异常?是 `Session` 构造时,还是执行 `sql()` 时?
  4. 以下代码片段有什么问题?
    sqlite3 *db;
    sqlite3_open("test.db", &db);
    sqlite3_exec(db, "INSERT INTO logs(msg) VALUES('hello');", nullptr, nullptr, nullptr);
    // 程序在此处崩溃或异常退出

本章小结

  • 连接方案二选一: 单机、嵌入式场景用SQLite(文件数据库);多用户、网络化服务端用MySQL(客户端-服务器数据库)。
  • SQLite核心流程: `open` -> `prepare`/`exec` -> `step` (循环) -> `column` (取数据) -> `finalize`/`close`,每一步都必须检查返回值。
  • MySQL现代用法: 使用Connector/C++ 8.0的X DevAPI,通过 `Session` 对象连接,优先使用预处理语句或Table API进行安全的数据操作。
  • 安全与健壮性铁律: 永远使用预处理语句来防止SQL注入;使用RAII或严格的 `try-catch` 确保数据库连接等资源被正确释放。
  • 排错线索: 连接失败首先检查网络、地址、端口、用户名密码;SQL错误仔细阅读驱动返回的错误信息;链接错误检查库文件和编译命令。

意见反馈

微信二维码 微信扫码关注 抖音二维码 抖音扫码关注