C++ 中 UTF-8 字符格式不一致,如何统一控制台输入与 SQLite 查询的编码?
在程序内部统一使用 UTF-8,在输入时完成转换
通过代码页 1253 读入的是 Windows-1253 字节,u8"..." 字面量保存的是 UTF-8 字节。两者都能表示希腊字母,但对应的字节不同。把 Windows-1253 字节直接传给要求 UTF-8 的 SQLite 接口,查询就无法按预期匹配。
Windows 控制台可以采用以下流程:
ReadConsoleW 读取 UTF-16
→ WideCharToMultiByte 转为 UTF-8
→ sqlite3_bind_text 绑定查询参数
→ sqlite3_column_text 读取 UTF-8 结果
如果现有的 std::getline 配合 1253 代码页已经能正常工作,也可以保留,在读取后显式执行 Windows-1253 → UTF-16 → UTF-8 转换。
CP_UTF8 下输入变成 NUL 的问题需要单独排查。示例没有提供 Windows、控制台宿主和 C/C++ 运行库的版本,无法确定具体原因。不过,三个值为 0 的字节并不是 "ΑΒΓ" 的有效 UTF-8 表示。
为什么显示相同,字符串里的字节却不同?
std::string 保存一串 char,不会记录或自动判断编码。这里几个希腊大写字母对应的字节如下:
| 文本 | Windows-1253 字节,十六进制 | UTF-8 字节,十六进制 |
|---|---|---|
Α | C1 | CE 91 |
Β | C2 | CE 92 |
Γ | C3 | CE 93 |
ΑΒΓ | C1 C2 C3 | CE 91 CE 92 CE 93 |
"ΑΒΓ" 在 Windows-1253 下占 3 字节,在 UTF-8 下占 6 字节。std::string::size() 返回的是字节数,不能直接当作 Unicode 字符数。
输入和输出都使用 1253 时,程序读入 Windows-1253 字节,控制台也按 Windows-1253 解释,因此可以正确显示。把同样的字节传给 UTF-8 接口,接口就会按另一种编码解释它们。
下面两个设置分别指定控制台的输入代码页和输出代码页:
SetConsoleCP(CP_UTF8);
SetConsoleOutputCP(CP_UTF8);
它们不会转换已有的字符串,也不会设置编译器的字面量编码或调试器的显示编码。调试器显示乱码,可能是它使用的解码方式与实际字节不一致,仅凭这一点不能判断数据已经损坏。
SQLite 的 sqlite3_prepare_v2、sqlite3_bind_text 和 sqlite3_column_text 使用 UTF-8 文本,部分对应的 ...16 接口使用 UTF-16。SQLite 不会根据当前 Windows 控制台代码页,自动把传入的 1253 字节转换为 UTF-8。
用 ReadConsoleW 输入,再转成 UTF-8
ReadConsoleW 直接读取 UTF-16,可以避开窄字符控制台输入中的代码页转换。下面的示例使用默认的控制台行输入模式,按回车结束输入,并保留行内空格。
#include <windows.h>
#include <climits>
#include <iostream>
#include <stdexcept>
#include <string>
std::string to_utf8(const std::wstring& text)
{
if (text.empty()) {
return {};
}
if (text.size() > static_cast<std::size_t>(INT_MAX)) {
throw std::length_error("Input is too long");
}
const int length = static_cast<int>(text.size());
const int required = WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS,
text.data(), length,
nullptr, 0, nullptr, nullptr);
if (required == 0) {
throw std::runtime_error("UTF-16 to UTF-8 conversion failed");
}
std::string result(required, '\0');
const int written = WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS,
text.data(), length,
result.data(), required, nullptr, nullptr);
if (written != required) {
throw std::runtime_error("UTF-16 to UTF-8 conversion failed");
}
return result;
}
std::wstring read_console_line()
{
HANDLE input = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode = 0;
if (!GetConsoleMode(input, &mode)) {
throw std::runtime_error("Standard input is not a console");
}
std::wstring line;
for (;;) {
wchar_t buffer[256];
DWORD count = 0;
if (!ReadConsoleW(input, buffer, 256, &count, nullptr)) {
throw std::runtime_error("ReadConsoleW failed");
}
if (count == 0) {
throw std::runtime_error("Console input ended");
}
line.append(buffer, count);
if (line.back() == L'\n') {
line.pop_back();
if (!line.empty() && line.back() == L'\r') {
line.pop_back();
}
return line;
}
}
}
int main()
{
try {
if (!SetConsoleOutputCP(CP_UTF8)) {
throw std::runtime_error("SetConsoleOutputCP failed");
}
std::cout << "Enter Greek letters: " << std::flush;
const std::string input = to_utf8(read_console_line());
std::cout << "You entered: " << input << '\n';
std::cout << "UTF-8 bytes: " << input.size() << '\n';
}
catch (const std::exception& e) {
std::cerr << e.what() << '\n';
return 1;
}
}
这段代码需要可写的 std::string::data(),因此要求 C++17 或更新标准。准确输入 ΑΒΓ 后,转换结果应为 6 字节。
示例通过 std::cout 输出 UTF-8,所以设置了输出代码页。控制台输出也可以使用宽字符 API:先通过 MultiByteToWideChar(CP_UTF8, ...) 将 UTF-8 结果转成 UTF-16,再调用 WriteConsoleW。这种方式不依赖输出代码页。
ReadConsoleW 和 WriteConsoleW 用于控制台句柄。如果输入或输出被重定向到文件、管道,需要另行处理,并明确约定其中的字节编码。上面的示例会拒绝非控制台输入。
把 UTF-8 参数绑定到 SQLite 查询
获得 UTF-8 字符串后,通过参数绑定传给 SQLite。直接将用户输入拼接进 SQL,既可能因输入中的单引号破坏语法,也会带来 SQL 注入问题。
假设数据库连接 db 已打开,且存在 items(name) 表,可以这样查询:
#include <sqlite3.h>
#include <climits>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
void find_name(sqlite3* db, const std::string& input)
{
if (input.size() > static_cast<std::size_t>(INT_MAX)) {
throw std::length_error("Query parameter is too long");
}
sqlite3_stmt* raw = nullptr;
const int prepared = sqlite3_prepare_v2(
db,
"SELECT name FROM items WHERE name = ?1",
-1,
&raw,
nullptr);
std::unique_ptr<sqlite3_stmt, decltype(&sqlite3_finalize)>
stmt(raw, &sqlite3_finalize);
if (prepared != SQLITE_OK) {
throw std::runtime_error(sqlite3_errmsg(db));
}
const int bound = sqlite3_bind_text(
stmt.get(),
1,
input.data(),
static_cast<int>(input.size()),
SQLITE_TRANSIENT);
if (bound != SQLITE_OK) {
throw std::runtime_error(sqlite3_errmsg(db));
}
int rc;
while ((rc = sqlite3_step(stmt.get())) == SQLITE_ROW) {
const unsigned char* text =
sqlite3_column_text(stmt.get(), 0);
if (text == nullptr) {
throw std::runtime_error(
"Could not retrieve the matching name");
}
const int bytes = sqlite3_column_bytes(stmt.get(), 0);
std::cout.write(
reinterpret_cast<const char*>(text), bytes);
std::cout << '\n';
}
if (rc != SQLITE_DONE) {
throw std::runtime_error(sqlite3_errmsg(db));
}
}
将前一个示例中的 input 传给 find_name(db, input) 即可,表名和列名按实际数据库修改。
sqlite3_bind_text 的长度单位是字节。使用 SQLITE_TRANSIENT 时,SQLite 会在绑定调用返回前复制参数,因此不依赖原字符串后续的生命周期。sqlite3_column_text 返回的指针应在当前行有效期间使用,不能保留到下一次 sqlite3_step 或语句释放之后。
保留 getline 时,怎样转换 1253 输入?
如果现有环境中的 SetConsoleCP(1253) 配合 std::getline 已经能正确读取希腊字母,可以保留这部分代码,并在读取后显式转换:
std::string cp1253_to_utf8(const std::string& bytes)
{
if (bytes.empty()) {
return {};
}
if (bytes.size() > static_cast<std::size_t>(INT_MAX)) {
throw std::length_error("Input is too long");
}
const int length = static_cast<int>(bytes.size());
const int required = MultiByteToWideChar(
1253, MB_ERR_INVALID_CHARS,
bytes.data(), length, nullptr, 0);
if (required == 0) {
throw std::runtime_error("Invalid Windows-1253 input");
}
std::wstring wide(required, L'\0');
const int written = MultiByteToWideChar(
1253, MB_ERR_INVALID_CHARS,
bytes.data(), length, wide.data(), required);
if (written != required) {
throw std::runtime_error("Windows-1253 conversion failed");
}
return to_utf8(wide);
}
配合前面的 to_utf8 使用:
SetConsoleCP(1253);
SetConsoleOutputCP(CP_UTF8);
std::string raw;
if (!std::getline(std::cin, raw)) {
throw std::runtime_error("Input failed");
}
const std::string input = cp1253_to_utf8(raw);
std::cout << input << '\n';
输入和输出可以使用不同的代码页。这里读入的是 1253 字节,转换后输出的是 UTF-8 字节。
这种方式只能保留 Windows-1253 能表示的字符。读入时已经被替换或丢失的字符,后续转换无法恢复。如果需要支持更广泛的 Unicode 字符,应使用宽字符输入方案。
getch、setlocale 和字面量还要注意什么?
_getch 不适合直接读取 Unicode 文本
_getch 返回窄字符值,不能将每次返回值都当成一个完整的 Unicode 字符。需要逐键读取时,可以使用 _getwch,累积 UTF-16 数据后再转换。
_getwch 每次读取的也只是一个 UTF-16 代码单元,完整实现还需要处理代理项对、特殊键、退格和回显。普通的整行文本输入使用 ReadConsoleW,可以省去这些工作。
setlocale 不能统一所有编码设置
setlocale(LC_ALL, "") 根据运行环境选择 C 运行库的区域设置,可能影响字符分类和部分字符转换行为。它不会自动让控制台代码页、字符串字节和 SQLite 参数全部采用 UTF-8,因此不能作为通用的编码修复方法。
输入变成 ΑΑΑ 的具体原因,还需要结合运行库、流模式和实际字节排查,示例中的信息不足以确定原因。
u8"..." 在 C++20 中改变了类型
在 C++17 中,可以写:
std::string text = u8"Γειά σας";
从 C++20 开始,u8"..." 使用 char8_t 元素类型,应先保存为 std::u8string:
std::u8string text = u8"Γειά σας";
std::string bytes(
reinterpret_cast<const char*>(text.data()),
text.size());
这里复制的是已有的 UTF-8 字节,没有重新编码。使用 MSVC 时,可以通过 /utf-8 明确指定源文件和执行字符集,但该编译选项不会改变控制台输入的编码。
排查时应查看字符串的十六进制字节。如果已确认输入和数据库内容都采用 UTF-8,查询仍不匹配,再检查大小写、空格以及带重音字符的 Unicode 规范化形式。SQLite 不会自动将视觉相同的预组合字符和组合序列统一为相同文本。
备注:内容仅供参考。
