PAYATHON 2026

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 字节,十六进制
ΑC1CE 91
ΒC2CE 92
ΓC3CE 93
ΑΒΓC1 C2 C3CE 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 不会自动将视觉相同的预组合字符和组合序列统一为相同文本。

备注:内容仅供参考。