)
前言C 标准库没有数据库接口连 MySQL 必须依赖官方或第三方的连接器。官方方案是 MySQL Connector/C它提供两套风格不同的 API一套是仿 JDBC 的sql::Connection/sql::Statement/sql::ResultSet另一套是较新的 X DevAPI。网上大量老代码用的是 C 语言版的mysql_real_connect、mysql_query那属于 Connector/C不是 C 连接器两者头文件、链接库、错误处理都不一样。所谓「改进版」通常指相对「一个全局连接 字符串拼接 SQL 手动 delete」的老写法改为 RAII 管理资源、预处理语句绑定参数、连接池复用连接、异常统一处理。本文就按这四点讲代码以 JDBC 风格的 API 为例。需要先说明Connector/C 不同大版本的头文件路径、库名、驱动入口函数可能不同下面的类名与调用以 JDBC 风格接口的通用形态为准实际项目请以所装版本的官方文档为准。一、准备头文件、库与驱动入口典型项目需要包含驱动头、连接头以及 statement / prepared_statement / resultset / exception 这几组接口头。链接时通常要链接连接器提供的动态库Windows 下是mysqlcppconn系列Linux 下是mysqlcppconn或带版本后缀的库名并把头文件目录加入包含路径。具体文件名随版本变化构建脚本里不要写死猜测的名字按安装目录实际内容配置。// 适用于 C17需安装 MySQL Connector/C#include cppconn/connection.h#include cppconn/exception.h#include cppconn/prepared_statement.h#include cppconn/resultset.h#include cppconn/statement.h#include mysql_connection.h#include mysql_driver.h驱动入口通常是sql::mysql::get_mysql_driver_instance()它返回一个由库管理的驱动对象调用方不要delete。拿到驱动后调用connect(url, user, password)建立连接。URL 的常见形式是tcp://主机:端口数据库可以在连接后用setSchema选择也可以写进 URL。字符集建议在连接参数或连接后立即设置例如utf8mb4否则中文可能按服务器默认字符集解释。二、RAII让连接、语句、结果集自动释放连接器返回的都是裸指针需要调用方delete。手写delete在异常路径上极易漏掉正确做法是立刻交给std::unique_ptr。这些接口类都有虚析构函数通过基类指针删除是安全的。// 适用于 C17#include memory#include string#include vectorclass Db {public:Db(const std::string url, const std::string user,const std::string password) {sql::mysql::MySQL_Driver* driver sql::mysql::get_mysql_driver_instance();conn_.reset(driver-connect(url, user, password));conn_-setSchema(demo);}void add_user(const std::string name, int age) {std::unique_ptrsql::PreparedStatement ps(conn_-prepareStatement(INSERT INTO users(name, age) VALUES (?, ?)));ps-setString(1, name);ps-setInt(2, age);ps-executeUpdate();}std::vectorstd::string list_users() {std::unique_ptrsql::Statement st(conn_-createStatement());std::unique_ptrsql::ResultSet rs(st-executeQuery(SELECT name FROM users ORDER BY id));std::vectorstd::string names;while (rs-next())names.push_back(rs-getString(name));return names;}private:std::unique_ptrsql::Connection conn_;};Db的析构会依次销毁unique_ptr成员连接自动关闭。语句和结果集在函数返回时释放即使executeQuery抛异常也不会泄漏。这就是 RAII 的价值把「释放」绑定到作用域而不是绑定到每一条正常路径。注意getString返回的是连接器自己的字符串类型可以隐式转换成std::string如果字段可能为 NULL要先判断rs-isNull(name)否则取到的是空串还是异常取决于接口约定不能想当然。三、预处理语句不只是防注入预处理语句prepared statement把 SQL 模板和参数分开传输服务器先解析模板参数随后以二进制或文本形式绑定。它带来两个好处。第一参数不会被当成 SQL 语法的一部分用户输入里的引号、分号、注释符都只是数据从根本上消除了拼接导致的注入。第二同一条语句重复执行时可以复用执行计划省去重复解析。// 适用于 C17void raise_salary(sql::Connection conn, int id, int delta) {std::unique_ptrsql::PreparedStatement ps(conn.prepareStatement(UPDATE staff SET salary salary ? WHERE id ?));ps-setInt(1, delta);ps-setInt(2, id);ps-executeUpdate();}参数下标从 1 开始不是 0。setString、setInt、setDouble、setNull等按类型绑定绑定顺序必须和占位符一一对应。表名、列名、ORDER BY的方向不能用占位符替代——占位符只代表值要动态拼这些标识符时必须在白名单里查表不能直接接受用户输入。executeUpdate用于 INSERT / UPDATE / DELETE返回受影响行数executeQuery用于 SELECT返回结果集。用错方法通常不会静默成功而是抛异常但错误信息未必直观写的时候按语义选对。四、连接池与错误处理连接对象的创建成本不低TCP 握手、认证、会话初始化都要时间。高并发服务不应该每次请求都新建连接而应维护一个连接池。sql::Connection本身不是线程安全的同一条连接不能被多个线程同时使用连接池的作用是「每个时刻一条连接只借给一个线程」。// 适用于 C17#include condition_variable#include functional#include memory#include mutex#include vectorclass Pool {public:using ConnPtr std::unique_ptrsql::Connection,std::functionvoid(sql::Connection*);Pool(std::size_t size, const std::string url,const std::string user, const std::string password) {sql::mysql::MySQL_Driver* driver sql::mysql::get_mysql_driver_instance();for (std::size_t i 0; i size; i)idle_.emplace_back(driver-connect(url, user, password));}ConnPtr acquire() {std::unique_lockstd::mutex lock(mutex_);cv_.wait(lock, [this] { return !idle_.empty(); });std::unique_ptrsql::Connection conn std::move(idle_.back());idle_.pop_back();return ConnPtr(conn.release(), [this](sql::Connection* p) {{std::lock_guardstd::mutex guard(mutex_);idle_.emplace_back(p);}cv_.notify_one();});}~Pool() {std::lock_guardstd::mutex guard(mutex_);idle_.clear();}private:std::mutex mutex_;std::condition_variable cv_;std::vectorstd::unique_ptrsql::Connection idle_;};这里有几点必须讲清楚。归还动作写在删除器里所以借出的ConnPtr析构时自动归还不需要手写release。删除器捕获了this因此池对象必须比所有借出的连接活得久如果池先销毁归还时就会访问悬垂指针。wait用了谓词避免虚假唤醒。析构函数里清空idle_之前要加锁否则可能与另一个线程的归还竞争。连接可能因为网络中断、服务器重启、超时而被单方面关闭。池里取出的连接在使用前最好做一次轻量探测例如执行一条极简查询失败就丢弃并重建不要试图在一条已断开的连接上「重试」业务语句因为事务状态已经不可知。所有连接器异常都派生自sql::SQLException可以统一捕获// 适用于 C17#include iostreamtry {// 数据库操作} catch (const sql::SQLException e) {std::cerr mysql error: e.what() code e.getErrorCode() state e.getSQLState() \n;}getErrorCode是服务器或客户端错误码getSQLState是五字符状态码。业务层不应把原始错误信息直接回显给终端用户那里可能包含表名、SQL 片段等内部信息。常见坑点❌SELECT * FROM users WHERE name name 拼接用户输入。✅ 用prepareStatement加setString绑定参数。❌ 全局共享一个sql::Connection多个线程同时查询。✅ 每线程一条连接或使用连接池借出期间独占。❌ 用executeQuery执行 INSERT然后困惑为什么没有结果集。✅ 增删改掉用executeUpdate。❌ 裸指针new出语句和结果集异常路径忘记delete。✅ 立即交给unique_ptr或使用 RAII 包装类。❌ 把数据库密码硬编码在源码里提交到仓库。✅ 从环境变量或配置文件读取配置文件不入库。❌ 连接断开后直接在原连接上重试业务语句。✅ 丢弃旧连接、重建再重试整个事务。❌ 动态拼接表名或排序方向以为占位符能覆盖。✅ 标识符用白名单校验占位符只绑定值。❌ 取出连接后不归还池逐渐耗尽acquire永久阻塞。✅ 用 RAII 句柄析构自动归还设置获取超时并记录告警。总结主题老写法改进写法资源管理手动new/deleteunique_ptr RAIISQL 构造字符串拼接预处理语句绑定参数连接复用全局单连接连接池按线程独占错误处理忽略返回值捕获sql::SQLException分类处理配置硬编码密码外部配置 / 环境变量连接 MySQL 本身不难难的是把资源生命周期、线程安全和错误路径处理干净。把连接、语句、结果集都纳入 RAII把 SQL 参数都走预处理把连接做成有借有还的池代码才能在异常和并发下保持稳定。具体接口细节以所装 Connector/C 版本的官方文档为准。