前言
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或带版本后缀的库名),并把头文件目录加入包含路径。具体文件名随版本变化,构建脚本里不要写死猜测的名字,按安装目录实际内容配置。
// 适用于 C++17,需安装 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。这些接口类都有虚析构函数,通过基类指针删除是安全的。
// 适用于 C++17
#include <memory>
#include <string>
#include <vector>
class 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_ptr<sql::PreparedStatement> ps(
conn_->prepareStatement(
"INSERT INTO users(name, age) VALUES (?, ?)"));
ps->setString(1, name);
ps->setInt(2, age);
ps->executeUpdate();
}
std::vector<std::string> list_users() {
std::unique_ptr<sql::Statement> st(conn_->createStatement());
std::unique_ptr<sql::ResultSet> rs(
st->executeQuery("SELECT name FROM users ORDER BY id"));
std::vector<std::string> names;
while (rs->next())
names.push_back(rs->getString("name"));
return names;
}
private:
std::unique_ptr<sql::Connection> conn_;
};Db的析构会依次销毁unique_ptr成员,连接自动关闭。语句和结果集在函数返回时释放,即使executeQuery抛异常也不会泄漏。这就是 RAII 的价值:把「释放」绑定到作用域,而不是绑定到每一条正常路径。
注意getString返回的是连接器自己的字符串类型,可以隐式转换成std::string;如果字段可能为 NULL,要先判断rs->isNull("name"),否则取到的是空串还是异常取决于接口约定,不能想当然。
三、预处理语句:不只是防注入
预处理语句(prepared statement)把 SQL 模板和参数分开传输:服务器先解析模板,参数随后以二进制或文本形式绑定。它带来两个好处。第一,参数不会被当成 SQL 语法的一部分,用户输入里的引号、分号、注释符都只是数据,从根本上消除了拼接导致的注入。第二,同一条语句重复执行时可以复用执行计划,省去重复解析。
// 适用于 C++17
void raise_salary(sql::Connection& conn, int id, int delta) {
std::unique_ptr<sql::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本身不是线程安全的,同一条连接不能被多个线程同时使用;连接池的作用是「每个时刻一条连接只借给一个线程」。
// 适用于 C++17
#include <condition_variable>
#include <functional>
#include <memory>
#include <mutex>
#include <vector>
class Pool {
public:
using ConnPtr =
std::unique_ptr<sql::Connection,
std::function<void(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_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this] { return !idle_.empty(); });
std::unique_ptr<sql::Connection> conn = std::move(idle_.back());
idle_.pop_back();
return ConnPtr(conn.release(), [this](sql::Connection* p) {
{
std::lock_guard<std::mutex> guard(mutex_);
idle_.emplace_back(p);
}
cv_.notify_one();
});
}
~Pool() {
std::lock_guard<std::mutex> guard(mutex_);
idle_.clear();
}
private:
std::mutex mutex_;
std::condition_variable cv_;
std::vector<std::unique_ptr<sql::Connection>> idle_;
};这里有几点必须讲清楚。归还动作写在删除器里,所以借出的ConnPtr析构时自动归还,不需要手写release。删除器捕获了this,因此池对象必须比所有借出的连接活得久;如果池先销毁,归还时就会访问悬垂指针。wait用了谓词,避免虚假唤醒。析构函数里清空idle_之前要加锁,否则可能与另一个线程的归还竞争。
连接可能因为网络中断、服务器重启、超时而被单方面关闭。池里取出的连接在使用前最好做一次轻量探测(例如执行一条极简查询),失败就丢弃并重建;不要试图在一条已断开的连接上「重试」业务语句,因为事务状态已经不可知。所有连接器异常都派生自sql::SQLException,可以统一捕获:
// 适用于 C++17
#include <iostream>
try {
// 数据库操作
} 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/delete | unique_ptr+ RAII |
| SQL 构造 | 字符串拼接 | 预处理语句绑定参数 |
| 连接复用 | 全局单连接 | 连接池,按线程独占 |
| 错误处理 | 忽略返回值 | 捕获sql::SQLException,分类处理 |
| 配置 | 硬编码密码 | 外部配置 / 环境变量 |
连接 MySQL 本身不难,难的是把资源生命周期、线程安全和错误路径处理干净。把连接、语句、结果集都纳入 RAII,把 SQL 参数都走预处理,把连接做成有借有还的池,代码才能在异常和并发下保持稳定。具体接口细节以所装 Connector/C++ 版本的官方文档为准。