Для описания я привожу минимальное воспроизведение следующего кода:
#include <bits/stdc++.h>
#include <iostream>
#include <regex>
#include <string>
#include <string>
#include <Windows.h>
// GBK 转 UTF-8
std::string GBKToUTF8(const std::string& gbkStr) {
// 1. 先将 GBK 转换为宽字符(UTF-16)// Convert GBK to wide characters first (UTF-16)
int len = MultiByteToWideChar(CP_ACP, 0, gbkStr.c_str(), -1, nullptr, 0);
std::wstring wstr(len, 0);
MultiByteToWideChar(CP_ACP, 0, gbkStr.c_str(), -1, &wstr[0], len);
// 2. 将宽字符(UTF-16)转换为 UTF-8 // Convert wide characters (UTF-16) to UTF-8
len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, nullptr, 0, nullptr, nullptr);
std::string utf8Str(len, 0);
WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &utf8Str[0], len, nullptr, nullptr);
return utf8Str;
}
int main() {
// 示例身份证号,长度为18 // Example ID number, length 18
std::string id_number = GBKToUTF8("610702199404261983");
// 检查字符串长度 // Check string length
std::cout << "Length before: " << id_number.length() << "\n"
<< id_number << std::endl;
// 正则表达式 // Regular expression
const std::regex id_number_pattern18("^([1-6][1-9]|50)\\d{4}(18|19|20)\\d{2}((0[1-9])|10|11|12)(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$");
// 进行匹配 // Make a match
if (std::regex_match(id_number, id_number_pattern18)) {
std::cout << "Match successful!" << std::endl;
} else {
std::cout << "Match failed!" << std::endl;
}
return 0;
}
Проблема теперь в том, что когда строка id_number
перекодируется в UTF-8, длина меняется с 18 на 19. Кроме того, регулярное выражение больше не соответствует строке правильно (ее можно правильно сопоставить, если она не перекодирована).
Я подозреваю, что строка была перекодирована и добавлены какие-то невидимые символы, но я не знаю, как это исправить.
Вот несколько скриншотов отладки VS2022 (ISO C++17) для справки (конечно, скриншоты не из минимального кода воспроизведения, но их следует хорошо понимать):
Перед перекодированием
После перекодирования
Я не знаю, как это сделать на данный момент, или я хотел бы предоставить решение и описание того, как возникает проблема.
Вы запрашиваете API для преобразования последнего NUL-символа в ваших строках, поэтому ваши выходные строки теперь содержат два, а не один NUL-терминатор: один, предоставляемый std::[w]string
, и один в контролируемой последовательности. Решение простое: передайте size()
вместо -1
в качестве длины. Это также менее затратно.
Из документации: «Если этот параметр равен -1, функция обрабатывает всю входную строку, включая завершающий нулевой символ. Следовательно, результирующая строка символов имеет завершающий нулевой символ, а длина, возвращаемая функцией, включает этот символ».
Я не уверен в логике здесь. Почему вы используете кодовую страницу ANSI?
@Dúthomhas Исходная строка находится в формате CP 936, который для этого приложения является 8-битной кодовой страницей по умолчанию.
@RaymondChen Да, но в названии функции указано, что она принимает строку упрощенного китайского GBK.
@Dúthomhas Думаю, мы согласны. Упрощенный китайский GBK — это 8-битная кодировка, которую Windows называет CP 936.
Проблема в том, что вы просите MultiByteToWideChar()
и WideCharToMultiByte()
включить место для явного NUL-терминатора в длину, которую они возвращают:
[в] cbMultiByte
Размер строки в байтах, указанной параметром lpMultiByteStr. В качестве альтернативы этому параметру можно установить значение -1, если строка завершается нулем. Обратите внимание: если cbMultiByte равен 0, функция завершится неудачно.
Если этот параметр равен -1, функция обрабатывает всю входную строку, включая завершающий нулевой символ. Таким образом, результирующая строка Юникода имеет завершающий нулевой символ, а длина, возвращаемая функцией, включает этот символ.
Вы включаете это дополнительное пространство при выделении памяти для std::wstring
и std::string
. Но, в отличие от строк C, строки C++ не завершаются нулем. Они могут содержать встроенные NUL-символы, которые включены в их size
, и иметь неявный NUL-терминатор, который НЕ включен в их size
.
Таким образом, вы не должны рассматривать строки C++ как завершающиеся нулем. Не запрашивайте у API место для терминатора NUL. Вместо этого используйте фактические размеры строк, например:
std::string GBKToUTF8(const std::string& gbkStr) {
// 1. 先将 GBK 转换为宽字符(UTF-16)
int len = MultiByteToWideChar(CP_ACP, 0, gbkStr.c_str(), gbkStr.size(), nullptr, 0);
// ^^^^^^^^^^^^^
std::wstring wstr(len, 0);
MultiByteToWideChar(CP_ACP, 0, gbkStr.c_str(), gbkStr.size(), &wstr[0], len);
// ^^^^^^^^^^^^^
// 2. 将宽字符(UTF-16)转换为 UTF-8
len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), wstr.size(), nullptr, 0, nullptr, nullptr);
// ^^^^^^^^^^^
std::string utf8Str(len, 0);
WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), wstr.size(), &utf8Str[0], len, nullptr, nullptr);
// ^^^^^^^^^^^
return utf8Str;
}
«Альтернатива» — единственное правильное решение.
@IInspectable согласился. Я удалил другой пример
Так лучше, спасибо. Поскольку строки C++ могут содержать встроенные символы NUL, нет причин избегать поддержки этого. Хотя это, как правило, не очень хорошая идея, символы NUL могут случайно оказаться в контролируемой последовательности (как показано в вопросе). Благодаря поддержке встроенных символов NUL реализация больше не маскирует ошибки в других частях кода.
Shold, вероятно, будет
std::string utf8Str(len - 1, 0);
, так какlen
, кажется, считается финальным'\0'
для C-строки.