本课文包含三个视频!
为什么中文版Windows是编程出现乱码的高发地带?怎么用 libiconv 把国标编码的汉字转换成宇宙统一码?怎么简化 libiconv 那些充满坑的 纯C 函数API?
1. 安装 libiconv
通常,你在 MSYS2 中安装过 GCC 编译套件,你的 msys2 系统中就会有 libiconv 开发库可用。可通过如下指令验证:
pacman -S libiconv
查看是否出现带有 “[已安装]” (或 installed)的 libiconv 库?
如果确实需要单独安装,指令为:
pacman -S mingw-w64-ucrt-x86_64-libiconv
再次提醒,如果你使用的 GCC 不是 UCRT64 版本,那么你可能需要的库名称应是
mingw-w64-x86_64-libiconv
(64位OS)或mingw-w64-i686-libiconv
(32位OS,不敢相信你在还在用)
2. 转码基础知识 🎥
Windows (中文版)是 编程出现乱码的高发地带之一。原因来自历史包袱:当年,各国政府(自然包括我国)都要求微软出的本地语言版本的操作系统,其字符编码,必须遵守当年各国国标。
004-libiconv编码转换-1基础知识-c++108杰
3. 函数使用包装
3.1 主要函数介绍
libiconv 主要函数有三个:
- iconv_open
原型:
iconv_t iconv_open(char const* toCode, char const * fromcode)
- 其中, iconv_t 为 “void *” 的别名
- 函数失败时,并不返回 nullptr,而是返回 (iconv_t)(-1)
- 入参依序为:目标编码名称,源编码名称
以下称该函数打开成功得到的结果为 “句柄”。
- iconv
原型:
size_t iconv( iconv_t cd,
char** in, size_t* inBytesLeft,
char** out, size_t* outBytesLeft);
第一个参数为句柄。其余四个参数都既为入参也为出参。
in:作入参,用于指示当前待转换的源字符串缓存区开始位置;作出参,用于告诉调用者,本次转换的结束位置(即下次转换的开始位置);
inByteLeft:作入参,指示当前待转换的源字符串缓存区有多大;作出参,告诉调用者,本次转换后还剩下多少字节未转换;
out: 作入参,用于指示可用于存储转换结果的缓存区开始位置;作出参,用于告诉调用者,本次转换后,结果存放的结束位置(可考虑作为下次用于存储结果的开始位置,本课堂出于简化,未采用此方法);
outBytesLeft: 作入参,用于指示本次转换可用来存储结果的缓存区大小(字节数);作出参,用于告诉调用者,本次转换后,用于存储结果的缓存区还剩余多少字节。
in 和 out 的内容都有可能造成本函数转换中途停下。最典型的如:out ,也就是输出缓存区大小不够了。比如说,源字符串有 61 字节,但目标输出缓存区(也就是 out)只有20个字节,就会造成 iconv() 转换若干字节后,就停下,并借助 errno (C库的一个宏,类似全局变量,但本质是对应到一个函数调用,且线程安全),告诉调用者:输出缓存区不足。
注意,编码转换并非 1:1 转换,由于源编码和目标编码用以表达单一个本地字符(比如一个汉字)的长度不一样,因此二者之间并不存在某个简单的比例关系。典型的如使用 UTF-8 编码表达一个汉字,可能是 2字节,也可能是 3字节、4字节。
为了避免 “输出缓存区不足” ,最粗暴的方法就是为 out 分配一个 “巨大” 的空间——比如,是源字符串长度的 4 倍、5 倍……这种方法既浪费内存,并且通常需要使用到 new 来动态分配内存,进一步拉低性能。
我们的解决方法相对复杂,但高效(或者说性能均衡):采用固定大小的临时连续内存来存储每次转换的结果,同时准备一个字符串流(std::stringstream)来连续存储每次转换的结果(新结果追加到旧结果之后)。
3.2 函数简化封装 🎥
005-libiconv编码转换-2简化函数-C++108杰
3.3 函数封装主要代码
namespace d2::myiconv
{
// 转换结构
struct IConvResult
{
std::string result; // 转换成功得到的,使用新编码的字符串
int errorNumber = 0; // 对应 errno, 出错号
std::string errorMessage; // 出错信息
};
// 转换函数
IConvResult Convert(std::string_view in, char const* fromCode, char const* toCode)
{
IConvResult Convert(std::string_view in, char const* fromCode, char const* toCode)
{
IConvResult ir;
// 调用 iconv_open
iconv_t cd = iconv_open(toCode, fromCode);
if (cd ==(iconv_t)(-1)) // 打开失败
{
ir.errorNumber = errno; // C -> C++
switch (ir.errorNumber)
{
case EINVAL:
ir.errorMessage = "不支持的编码";
break;
case ENOMEM:
ir.errorMessage = "内存不足";
break;
default:
ir.errorMessage = "未知错误";
break;
}
return ir;
}
char* inBufferPtr = const_cast<char *>(in.data()); // 指向输入缓存位置 (非常量)
size_t inBytesLeft = in.size(); // 输入缓存大小
std::stringstream ss;
std::size_t const sizeOfOutBuffer = 20; // 输出缓存区大小
char outBuffer[sizeOfOutBuffer]; // 输出缓存
while (inBytesLeft > 0) // 输入缓存中,还有剩余字符未被转换
{
char *outBufferPtr = outBuffer;
size_t outBufferLeft = sizeOfOutBuffer;
size_t result = iconv(cd, &inBufferPtr, &inBytesLeft, &outBufferPtr, &outBufferLeft);
if (result == (size_t)(-1)) // 转换停止了
{
auto n = errno;
switch (n)
{
case E2BIG: // 输出缓存区不够用了……
{
break;
}
case EILSEQ:
{
ir.errorNumber = n;
ir.errorMessage = "输入字符序列不符合指定编码规则";
break;
}
case EINVAL:
{
ir.errorNumber = n;
ir.errorMessage = "输入的字符序列不完整";
break;
}
default:
{
ir.errorNumber = n;
ir.errorMessage = "转换过程发生未知错误";
break;
}
}
}
if (ir.errorNumber != 0)
{
break; // while
}
ss.write(outBuffer, sizeOfOutBuffer - outBufferLeft); // 将本轮的输出结果,写入输出流
} // while
iconv_close(cd);
if (ir.errorNumber == 0)
{
ir.result = ss.str();
}
return ir;
}
} //namespace d2::myiconv
使用示例:
char const* gbk = "假设这是一个GBK编码的字符串";
auto ir = d2:myiconv::Convert(gbk, "GBK", "UTF-8");
if (ir.errorNumber != 0)
{
std::cout << ir.errorNumber << " : " << ir.errorMessage << "\n";
}
else
{
// 转换成功:
std::cout << ir.result << std::endl;
}
对应的 CMakeLists.txt 示例:
cmake_minimum_required(VERSION 3.10.0)
project(HelloLibIconv VERSION 0.1.0 LANGUAGES C CXX)
add_executable(HelloLibIconv main.cpp gbk_str.cpp)
target_link_libraries(${PROJECT_NAME} PRIVATE iconv)
target_link_directories(${PROJECT_NAME} PRIVATE "c:/msys64/ucrt64/lib")
其中的 gbk_str.cpp 在 VSCODE 中应明确使用 GBK 编码保存,其内容为:
char const* gbk_str = "我是一个GBK字符编码的字符串!请保障我所在的CPP文件编码为GBK!";
4. 项目应用 🎥
上一节中 libfswatch 在监控 Windows 下名字带汉字的文件对象时,文件名输出会出现乱码。其原因于:libfswatch 从 Windows 读文件对象信息时,未使用特定的 UNICODE 版本的 API,而是使用 Windows 本地语言版 API,因此读到的文件名中的中文字符是 GBxxxx 编码(既中国国标),但 libfswatch 将它视为 UTF-8 编码。
基于 libiconv,使用我们所包装的函数,解决乱码的核心代码是:
// 输出变动的文件路径:
auto utf8Path = d2::myiconv::Convert(event.get_path(), "GBK", "UTF-8"); // 编码转换
if (utf8Path.errorNumber != 0)
{
std::cout << utf8Path.errorMessage << std::endl;
break;
}
std::cout << utf8Path.result << "\n";
下面的视频给出了采用我们所写的 Convert() 的解决方案。
006-libiconv编码转换-3项目应用-C++108杰
5. C++ 封装
d2::myiconv::Convert()函数使用起来,比原来的纯C函数“三板斧”组合,要方便不少,但也有个比较明显的缺点:无法复用 iconv_open() 得到的句柄,每次转换都需要先打开最后再关闭。如果仅是一次性转换无所谓,但有时有多个字符串需要分开转换,句柄不能复用的弊端就比较明显了。
解决方法是使用 C++ 面向对象的思想进一步加以封装,我们给出一种思路的类设计(仅 class 设计):
namespace d2::myiconv
{
// 转换结果
struct IConvResult
{
std::string result; // 转换成功得到的,使用新编码的字符串
int errorNumber = 0; // 对应 errno, 出错号
std::string errorMessage; // 出错信息
};
class IconvHelper final // 注:实现为 final 类
{
public:
// 构造(失败时可抛出异常)
IconvHelper(char const* fromCode, char const* toCode) noexcept(false);
IconvHelper(IconvHelper const& ) = delete; // 不允许复制
IconvHelper& operator = (IconvHelper const&) = delete;
IconvHelper(IconvHelper&& ih) noexcept; // 支持转移
IconvHelper& operator = (IconvHelper&& ih) noexcept;
~IconvHelper() noexcept; // 析构
// 静态转换,方便一次性转换 (不允许抛出异常)
static IConvResult Convert(std::string_view in,
char const* fromCode, char const* toCode) noexcept;
// 非静态的转换方法,方便多次复用 (不允许抛出异常)
IConvResult Convert(std::string_view in) noexcept;
private:
iconv_t cd;
};
} // namespace d2::myiconv
请进入 d2school 网站 本课的作业区,完成符合上述类定义的 C++ 版本的 libiconv 封装,并及时交作业。