C++ 11 的 codecvt 与编码转换

news2024/10/17 14:39:27

1 编码与乱码

乱码产生的主要原因是编码与字符集不匹配,这种不匹配时怎么造成的呢?首先要来了解一下编码和字符集的关系。

1.1 编码与字符集

由于标准的英文 ASCII 已经成了全球标准,每台电脑的 BIOS 里存着一份标准 ASCII 表(包括字模),所以,使用英文和数字基本上不会产生乱码。这里说基本上,是因为不是绝对,因为据说德国、法国和一些说拉丁语的国家搞过一次起义,试图将德语字母、法语字母或拉丁字母代替 ASCII 表中的英文字母,搞一个适用于自己的母语系统的字母表。不知道这场起义是如何被镇压的,反正现在通行的 ASCII 表还是英文字母。不过随着 UNICODE 字符集标准化的推进,重排 ASCII 表也没有什么实际的意义了。

中国人要显示汉字,日本人要平(片)假名,韩国人要思密达,总之,这些国家的文字不可能指望只留了 127 个可见字符位置的 ASCII 表。解决汉字问题最简单的方案就i是采用扩展 ASCII 的方案,具体实施方法就是对于 ASCII 表能表达的字符,仍然用一个字节的 ASCII 码表示,对于汉字这样的非英文字符则用两个字节表示。那么问题来了,对于一串既有中文字符又有英文字符的编码,如何判断某一个字节的编码到底是个英文字母呢,还是和后面一个字节一起组成一个双字节的汉字字符呢?重点来了,既然可见英文字母和数字的 ASCII 码都是小于 128 的,那么超过 128 的不就是汉字码?确实是这样的,汉字的双字节编码中,第一个字节的最高位总是 1,这也是汉字的区位码第一个字节都是从 0x80 区开始的原因,在这之前的区位是不使用的。

这种扩展 ASCII 方案,在操作系统中被称为 MBCS (Multi-Bytes Charactor Set,多字节字符集)编码。举个例子直观地理解一下这个字符编码方案吧,比如 “abc中国” 这一串文字,以 MBCS 编码后计算机内部存储的编码序列是:0x61,0x62,0x63,0xd6,0xd0,0xb9,0xfa。这 7 个字节中前 3 个分别是英文字母 “abc” 的 标准ASCII,后面 4 个字节是两个汉字的编码。汉字的编码是怎么确定的呢?这就要说说字符集和字符编码了。字符集(Charset)和编码(Encoding)是两个不同的概念,但是确实紧密相关的两个事情。以汉字为例,从最早的 GB2312,到 GBK,再到 GB18030,字符集中容纳的汉字的数量逐步增加,但是编码的规则没变,依然是用一个字节区号+一个字节区内编号组成。比如汉字 “中” ,在三个字符集中都是安排在 D6 区,区内编号是 D0(兼容性考虑)。对于 MBCS 这样的扩展 ASCII 编码方式来说,字符集就决定了编码,所以我们看到的编码就是汉字原本的区位码。

1.2 Unicode

同样的一串字符编码,在 GBK 字符集上就能显示汉字,在日文字符集上就可能显示对应的日文假名或乱码,这就是乱码产生的第一个原因:字符集与编码不匹配。Unicode 的出现就是为了解决字符集与编码不匹配的问题,Unicode的全称是:“Universal Multiple-Octet Coded Character Set”,简称为 UCS(Universal Character Set)。Unicode 的哲学非常简单,那就是全世界都用一个字符集就好了,全世界所有文字的字符,在 Unicode 中都有自己的位置和编码。Unicode 有两个分支,一个是 UCS-2,即用两个字节给每个字符编码,另一个分支是 UCS-4,即用四个字节给每个字符编码。目前主要使用的是 UCS-2,UCS-4 是为 UCS-2 不够用的时候做备份。

有了字符集,还需要与之对应的编码,目前基于 Unicode 字符集的编码格式主要有 UTF-8,UTF-16LE,UTF-16BE,UTF-32LE,UTF-32BE。后缀 “LE” 和 “BE” 是小端字节序和大端字节序的意思。在 Unicode 字符集中,汉字“中”的编码是 0x4e2d,“国”的编码是 0x56fd,如果采用 UTF-16LE 编码,“abc中国” 编码后在计算机中存储的编码序列是:0x61,0x00,0x62,0x00,0x63,0x00,0x2d,0x4e,0xfd,0x56。可能有人会有疑惑,UTF-16 或 UCS-2 的16 位编码只能表示 65536 个字符,表示全部的汉字可能都不够吧?其实 UTF-16 编码还留了一手,对于编码值小于 0x10000 的,直接用编码值,比如汉字“中”的编码。对于编码值大于 0x10000 的字符,先用字符编码减去 0x10000,剩下的数值(20位)分成两个 10 位编码,给前一个 10 位编码补上六位 110110 前缀,得到一个 16 位编码,后一个 10 位编码补上 110111 前缀,得到另一个 16 位编码,即用两个 16 位编码表示这个字符。目前 Unicode 已经安排的最大码位是 0x10FFFF,减去 0x10000 后,结果最大是 0xFFFFF,所以 20 位也就够用了。一旦 Unicode 的最大码位超过 0x10FFFF,估计 UCS-2 将会被弃用。

这里要说说第二种类型的乱码,那就是字节序问题导致的编码。在小端字节序的系统上使用大端字节序的 UCS 编码,即使都是 Unicode,也一样会出现乱码。因此,需要在不同的系统之间交换数据的文件、通过网络传出的数据,都需要注意字节序的问题,否则也会出现乱码。有没有不用考虑字节序问题的 UCS 编码格式呢?有,那就是 UTF-8。UTF-8 编码是变长码元,根据每个字符编码的数值确定这个字符的 UTF-8 编码长度。以“abc中国” 为例,采用 UTF-8 后在计算机中存储的编码序列是:0x61,0x62,0x63,0xe4,0xb8,0xad,0xe5,0x9b,0xbd。

假如一个文件用的是 UTF-8 编码,如果打开文件读取时使用 MBCS 解码读取,就会出现乱码,这是第三种类型的乱码,即编码与解码不匹配。解决的方法就是通过文件头的 BOM 探测一下文件的编码,或者用 “uchardet” 这样成熟的库来探测文件的编码

2 编码转换

有编码就有编码的转换,这是“江湖”的一体两面。C/C++ 提供了部分转码功能,比如wcstombs()mbstowcs() 函数,还有从 C++ 11 开始支持的 std::codecvt,但是总体来说,大部分 C++ 的开发者还是会求助于第三方的库,比如 libiconv 或 icu4c。

2.1 wcstombs() 和 mbstowcs()

这两个函数的作用是在 MBCS 字符编码和宽字符编码之间转换,其原型是:

size_t wcstombs (char* dest, const wchar_t* src, size_t max);
size_t mbstowcs (wchar_t* dest, const char* src, size_t max);

这两个函数的行为并不像其名字理解的那么简单,使用这两个函数的注意事项涉及字节序、本地的地域化设置以及 wchar_t 类型在不同系统上的差异。

首先说说地域化环境设置对这两个函数的影响。前面介绍过,MBCS 这样的扩展 ASCII 编码其实和字符集有紧密关系,同样的编码值在不同字符集上对应的是不同的文字,所以,不正确的本地化设置,可能会导致得不到正确的结果,举个例子:

const char* pstr = "abc中国";
wchar_t wcstr[16] = { 0 };
::mbstowcs(wcstr, pstr, 16);

如果采用默认的中性地域化设置,wcstr 中得到的编码序列(小端系统)是:0x61,0x00,0x62,0x00,0x63,0x00,0xd6,0x00,0xd0,0x00,0xb9,0x00,0xfa,0x00。可以看出来,这个函数不能正确理解汉字的编码,统统按照英文字符的处理方式改成宽字符串。并且汉字部分用的还是 GBK 字符集的区位码,不是 Unicode 字符集的编码。如果按照如下方式设置地域化环境:

std::locale::global(std::locale(""));

就能得到正确的结果:0x61,0x00,0x62,0x00,0x63,0x00,0x2d,0x4e,0xfd,0x56,这是在小端系统功能上的 UTF-16LE 编码。注意汉字的编码已经转成 Unicode 字符集编码了。反向转换时,地域化设置对 wcstombs() 函数的影响是相同的,这里不再赘述。接下来说说系统字节序对这两个函数的影响。上例中的结果是在小端字节序的系统上得到的编码序列,在大端字节序系统上,得到的将是UTF-16BE 编码,这一点应该比较容易理解。最后是 wchar_t 在 Windows 系统和 linux 系统上的差异。在 Windows 系统上,wchar_t 是 16 位宽度的 Unicode。以前都直接说是 UTF-16LE,因为 Windows 基本上都是在 Intel 的 CPU 上跑。现在不能这么肯定了,因为 Windows 也支持 ARM CPU了,未来可能还支持其他大端 CPU。在 linux 系统上,wchar_t 是 32 位宽度的 Unicode,至于是 LE 还是 BE,还要看系统是什么情况。

综上所述,使用这两个转换函数不仅要保证地域化环境设置要正确,还要正确理解系统使用的字节序以及 wchar_t 类型在不同系统上的差异。GCC 的编译器有个选项,可以设置在 linux系统上将 wchar_t 类型强制设置为 16 位,但是请慎用。因为你的 wchar_t 长度与其他模块的 wchar_t 长度不一致,会导致很多潜在的错误。

2.2 std::codecvt

2.2.1 结合文件流实现编码转换

std::codecvt 是 C++ 11 引入的一组 Facet,配合 std::locale 使用,解决一些地域化环境设置时需要的编码转换问题。

std::codecvt<char, char, std::mbstate_t>identity conversion
std::codecvt<char16_t, char, std::mbstate_t>conversion between UTF-16 and UTF-8 (since C++11)(deprecated in C++20)
std::codecvt<char16_t, char8_t, std::mbstate_t>conversion between UTF-16 and UTF-8 (since C++20)
std::codecvt<char32_t, char, std::mbstate_t>conversion between UTF-32 and UTF-8 (since C++11)(deprecated in C++20)
std::codecvt<char32_t, char8_t, std::mbstate_t>conversion between UTF-32 and UTF-8 (since C++20)
std::codecvt<wchar_t, char, std::mbstate_t>conversion between the system’s native wide and the single-byte narrow character sets

除了上述 5 个基本的转换器,C++ 还定义了一些特殊的定制版本,比如 std::codecvt_utf8,std::codecvt_utf16,std::codecvt_utf8_utf16 等等。以 std::codecvt_utf8 为例:

template<
    class Elem,
    unsigned long Maxcode = 0x10ffff,
    std::codecvt_mode Mode = (std::codecvt_mode)0
> class codecvt_utf8 : public std::codecvt<Elem, char, std::mbstate_t>;

下面的例子代码演示了如何用 std::locale 结合 std::codecvt_utf8,将 utf16 编码的字符串以 utf-8 编码形式存入文件。

auto loc_with_utf8 = std::locale(std::locale(""), new std::codecvt_utf8<wchar_t>);
std::wofstream of(L"utf8.txt");
of.imbue(loc_with_utf8);
of << L"abc中国";
of.close();

打开 utf8.txt,可以看到文件编码是 UTF-8 编码。

在这里插入图片描述
假如希望存入文件时自动添加 BOM,可以这样构造 Facet:

auto loc_with_utf8 = std::locale(std::locale(""), new std::codecvt_utf8<wchar_t, 0x10FFFF, std::generate_header>);

这样写入文件时会自动添加 BOM,效果展示:
在这里插入图片描述
从 UTF-8 编码的文件中读取字符串的过程,是写文件的逆过程,此时如果希望读入字符串时跳过文件头的 BOM,可以使用 std::consume_header:

auto loc_with_utf8 = std::locale(std::locale(""), new std::codecvt_utf8<wchar_t, 0x10FFFF, std::consume_header>);
std::wifstream infile(L"utf8.txt");
std::wstring wstr;
infile.imbue(loc_with_utf8);
infile >> wstr;

codecvt_mode 还可以组合使用,比如构造一个小端字节序,并且写入文件头 BOM 的转换器,可以这样构造:

new std::codecvt_utf16<wchar_t, 0x10ffff, std::generate_header|std::little_endian>

2.2.2 使用 wstring_convert(C++ 11)

实现字节编码的字符串和宽字符串之间转换,还可以用 std::wstring_convert 和 std::wbuffer_convert。不过,这两个类在 C++ 17 被标记为 “deprecated”,前途未卜,所以,不建议使用,这里只是简单介绍一下。std::wstring_convert 的定义如下:

template<class Codecvt,
         class Elem = wchar_t,
         class Tr = std::char_traits<Elem> >
class wbuffer_convert : public std::basic_streambuf<Elem, Tr>

可以借助 std::wstring_convert 构造一个 UTF-8 与 UTF-18 的转换器:

std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;

将 UTF-16 编码的字符串转化成 UTF-8 编码的字符串,用 to_bytes() 函数:

std::wstring wstr = L"abc中国";
std::string utf8Str = converter.to_bytes(wstr);

将 UTF-8 编码的字符串转成 UTF-16 编码的字符串,用 from_bytes() 函数:

std::wstring wStr = conv.from_bytes(utf8Str);  

3 使用第三方编码库

3.1 icu4c

ICU 是个老牌的库,功能强大,编码转换只是它的一个子功能。如果只是简单想做个字符串的编码转换,不推荐用 ICU。不过,从 Windows 10 1703 开始,Windows 内部集成了 icu,相关的头文件和库已经包含在 Windows 10 SDK 1703 中。但是,微软的老毛病又犯了,从 1903 版本开始,Windows 10 SDK 将 icu 的两个库 icuuc 和 icuin 合并为一个,头文件也只用包含 icu.h。这使得 icu 库的老用户十分不爽,是忽略 Windows 10 的 ICU 库,继续使用独立的 ICU,还是跟着微软走,以后就用变形的 ICU 库?要好好想想。

3.2 libiconv

libiconv 也是个老牌的编码转换库,不干别的,专业转码。接口也很简单,就是 3 个函数,支持的编码格式和字符集非常全面,推荐使用。关于 libiconv 库的使用,可以参考《使用 libiconv 开源库做字符编码转换》这篇文章。

3.3 boost::codecvt

如果你对 C++ 的“半吊子且始乱终弃”的 codecvt 不感兴趣,可以考虑使用 boost 库的 codecvt Facet。

参考资料:

https://docs.microsoft.com/en-us/windows/win32/intl/international-components-for-unicode–icu-

https://www.boost.org/doc/libs/1_77_0/libs/serialization/doc/codecvt.html

https://en.cppreference.com/w/cpp/locale/locale

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2217119.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ES6扩展运算符

1.介绍&#xff1a; ... 扩展运算符能将数组转换为逗号分隔的参数序列&#xff1b; 扩展运算符&#xff08;spread&#xff09;也是三个点&#xff08;...&#xff09;。它好比 rest 参数的逆运算&#xff0c;将一个数组转为用逗号分隔的 参数序列&#xff0c;对数组进…

IP数据包格式、ICMP封装步骤

IP数据包格式 版本号&#xff1a;占4位&#xff0c;表示IP协议的版本&#xff0c;目前广泛使用的是IPv4&#xff0c;其版本号为4。 首部长度&#xff1a;占4位&#xff0c;表示IP首部的长度&#xff0c;单位为32位字节。首部长度最小为20字节&#xff0c;最大为60字节。 服务…

2.Java--入门程序

一、开发Java程序 步骤&#xff1a; 1.编写代码 其中第一行的HelloWorld叫类名&#xff0c;下面的框架叫main()方法&#xff0c; 类名要和文件名一致&#xff0c; 2.编译代码 用Javac进行编译&#xff0c;将编写的代码保存之后&#xff0c;打开WindowsR输入cmd 用cd文件夹…

40系显卡配置Apollo9.0

文章目录 一、下载安装 Apollo 源码1. 克隆 Apollo 源码 二、 启动Apollo环境容器三、 进入Apollo环境四、编译五、启动 Apollo 进行播包验证5.1 获取数据包5.2 启动 Dreamview5.3 在 Dreamview 中播放数据包5.4 通过命令行播放数据包 宿主机基础环境 系统&#xff1a;ubuntu22…

关键词提取技术:TextRank 详解

1. 什么是 TextRank&#xff1f; TextRank 是一种基于图的排序算法&#xff0c;用于从文本中提取关键词和进行自动摘要。它是受 Google 的 PageRank 算法启发而提出的。与 TF-IDF 不同&#xff0c;TextRank 不依赖外部语料库&#xff0c;也不需要训练数据。它通过构建词语之间的…

JavaWeb合集06-MySQL数据库

六、MySQL数据库 关系型数据库(RDBMS)&#xff1a;建立在关系模型基础上&#xff0c;由多张相互连接的二维表组成的数据库。 特点&#xff1a; 使用表存储数据&#xff0c;格式统一, 便于维护. 使用SQL语言操作&#xff0c;标准统一&#xff0c;使用方便&#xff0c;可用于复杂…

js中map,filter,find,foreach的用法介绍

js中map&#xff0c;filter&#xff0c;find&#xff0c;foreach的用法介绍 在 JavaScript 中&#xff0c;数组提供了一些常用的迭代方法&#xff0c;如 map、filter、find 和 forEach&#xff0c;这些方法允许你对数组中的每个元素进行操作&#xff0c;下面是它们的用法和区别…

用示波器观测RC一阶电路零输入响应是否激励必须是方波信号

概述 RC一阶电路是一种简单但非常重要的电路&#xff0c;广泛应用于滤波、信号处理和时间常数分析等领域。在研究RC电路的动态特性时&#xff0c;零输入响应&#xff08;Natural Response&#xff09;是一项关键内容。本文将详细解析用示波器观测RC一阶电路零输入响应时&#…

基于 MyBatis Plus 分页封装分页方法

一、前言 作为一个 CRUD 工程师&#xff0c;查询必然少不了&#xff0c;分页查询更是常见&#xff0c;市面上也有很多成熟的分页插件&#xff0c;都各有优缺点&#xff0c;这里整理一下&#xff0c;基于 MybatisPlus 的分页插件进一步封装分页的公共方法。 二、对象封装 其实…

elementUI,设置日期,只能选择过去的和今天的日期

在 el-date-picker 组件中加&#xff1a;:picker-options"pickerOptions" <el-form-item label"票据生成日期&#xff1a;"> <el-date-picker v-model"date1" type"daterange" range-separator"至" value-format&…

Webpack 完整指南

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;Webpack篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来webpack篇专栏内容:webpack介绍 目录 介绍 一、webpack 1.1、webpack是什么 1.2 webpack五个核心配置 1.…

学习threejs,拉伸几何体THREE.TubeGeometry管道

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️拉伸几何体THREE.TubeGeome…

医护人员排班|基于springBoot的医护人员排班系统设计与实现(附项目源码+论文+数据库)

私信或留言即免费送开题报告和任务书&#xff08;可指定任意题目&#xff09; 目录 一、摘要 二、相关技术 三、系统设计 四、数据库设计 五、核心代码 六、论文参考 七、源码获取 一、摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息…

js 对网页表格内容进行全选

起因&#xff0c; 目的: 需要从一个数据库&#xff0c;手动选取数据&#xff0c;而且这个网页没有全选按钮&#xff0c;只能一个一个点&#xff0c;很烦。 所以写了一个简单的 js 脚本&#xff0c; 一键全选。 过程: 代码 1 function clickAllBoxes() {const checkboxes do…

HarmonyNext保存Base64文件到Download下

本文介绍如何保存Base64的文件到Download下 参考文档地址&#xff1a; 保存用户文件-Harmony Next 用到的是DOWNLOAD模式保存文件 用户在使用save接口时&#xff0c;可以将pickerMode配置为DOWNLOAD模式&#xff0c;该模式下会拉起授权接口&#xff0c;用户确认后会在公共路径…

ChatGPT丨R语言在生态环境数据统计分析、绘图、模型中的应用

第一单元&#xff1a;生态环境数据统计概述 1.1 生态环境数据特点及统计方法介绍 1&#xff0e;生态环境数据复杂性和多样性 2&#xff0e;生态环境数据类型及分布特点 3&#xff0e;生态环境数据主要统计分析方法及统计检验&#xff08;t-检验、F检验、卡方检验&#xff0…

PostgreSQL Windows系统初始化、登录、创建用户及数据库

文章目录 PostgreSQL初始化PostgreSQL登录 PostgreSQL初始化 initdb 到安装目录下&#xff0c;找到目录E:\postgresql\bin&#xff08;自己的安装目录&#xff09;&#xff0c;在该目录下使用管理员方式打开cmd窗口。 initdb.exe -D "E:\postgresql\bin" E:\postgre…

车载软件架构---软件定义汽车的复杂性

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明自己,无利益不试图说服别人,是精神上的节…

闹钟、绘制与TCP

闹钟 pro文件&#xff1a; 头文件&#xff1a; #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include<QTimer> #include<QTime> #include<QDebug> #include<QTimerEvent> #include<QDateTime> #include<QTextToSpeech> #i…

sql-labs靶场第十五关测试报告

目录 一、测试环境 1、系统环境 2、使用工具/软件 二、测试目的 三、操作过程 1、寻找注入点 2、注入数据库 ①寻找注入方法 ②爆库&#xff0c;查看数据库名称 ③爆表&#xff0c;查看security库的所有表 ④爆列&#xff0c;查看users表的所有列 ⑤成功获取用户名…