c++中对中文和多语言的支持。ASCII是一种创立于 1963 年的 7 位编码,用 0 到 127 之间的数值来代表最常用的字符,包含了控制字符(很多在今天已不再使用)、数字、大小写拉丁字母、空格和基本标点。
    ASCII 里只有基本的拉丁字母,它既没有带变音符的拉丁字母(如 é 和 ä ),也不支持像希腊字母(如 α、β、γ)、西里尔字母(如 Пушкин)这样的其他欧洲文字,包括肯定也不支持中文。所以很多其他编码方式纷纷应运而生,包括 ISO 646 系列、ISO/IEC 8859 系列等等;大部分编码方式都是头 128 个字符与 ASCII 兼容,后 128 个字符是自己的扩展,总共最多是 256 个字符。每次只有一套方式可以生效,称之为一个代码页(code page)。
    这种做法,只能适用于文字相近、且字符数不多的国家。

    最早的中文字符集标准是 1980 年的国标 GB2312,其中收录了 6763 个常用汉字和 682 个其他符号。我们平时会用到编码 GB2312,其实更正确的名字是 EUC-CN,它是一种与 ASCII 兼容的编码方式。GB2312它用单字节表示 ASCII 字符而用双字节表示 GB2312 中的字符由于 GB2312 中本身也含有 ASCII 中包含的字符,在使用中逐渐就形成了“半角”和“全角”的区别(半角ASCII 字符(ASCII标准中的ASCII) 全角ASCII 字符(GB2312标准中的ASCII) )

    微软中的unicode实际指的是utf-16编码方式,用16比特来表示一个字符(wchar_t — 16位)。
    微软外的世界unicode是一种标准,最主流的编码方式是与ASCII全兼容的utf-8。

    Unicode 字符是根据含义来区分的,而非根据字形。除了中日韩 中的汉字没有分开,像斜体(italics)、小大写字母(small caps)等排版效果在 Unicode 里也没有独立的对应。不过,因为 Unicode 里包含了很多数学、物理等自然科学中使用的特殊符号,并且还有一些表情符号。

    Unicode简介
    Unicode编码点是从0x0到0x10FFFF,一共1114112个位置,一般用U+(16进制字符) 来表示一个Unicode字符,如U+0020表示空格,U+6C49表示”汉”,U+1F600表示笑脸表情。
    Unicode字符常见的编码方式有:

    UTF-32:32位(0000 0000 - FFFF FFFF) 是编码点的直接映射 (并且还有冗余 因为Unicode最多6位 16进制数)
    https://en.wikipedia.org/wiki/UTF-32
    UTF-16: 16位,对于U+0000到U+FFFF的unicode字符,是直接映射。对于大于 U+FFFF 的字符,使用 32 比特的特殊映射关系——在 Unicode 的 16 比特编码点中 0xD800–0xDFFF 是一段空隙,使得这种变长编码成为可能。
    在一个 UTF-16 的序列中,如果看到内容是 0xD800–0xDBFF,那这就是 32 比特编码的前 16 比特;如果看到内容是 0xDC00–0xDFFF,那这是 32 比特编码的后 16 比特;如果内容在 0xD800–0xDFFF 之外,那就是一个 对纯16 比特的unicode的映射。
    https://en.wikipedia.org/wiki/UTF-16
    UTF-8 :1 到 4 字节的变长编码。在一个合法的 UTF-8 的序列中,如果看到一个字节的最高位是 0,那就是一个单字节的 Unicode 字符(3位的unicode码);
    如果一个字节的最高两比特是 10,那这是一个 Unicode 字符在编码后的后续字节;
    否则(除0,10的情况),这就是一个 Unicode 字符在编码后的首字节,
    将首字节 + 多个后续字节 拼在一起还原一个 Unicode 字符
    且最高位开始连续 1 的个数表示了这个字符按 UTF-8 的方式编码有几个字节。
    https://en.wikipedia.org/wiki/UTF-8
    只有UTF-8 完全保持了和 ASCII 的兼容性,目前得到了最广泛的使用
    从unicdoe编码映射到某种编码更好理解,举下面几个例子
    UTF-32:U+0020 映射为 0x00000020,U+6C49 映射为 0x00006C49,U+1F600 映射为 0x0001F600。—32位直接映射
    UTF-16:U+0020 映射为 0x0020,U+6C49 映射为 0x6C49 (16位的16进制转换成utf16就是一一对应直接转换),而 U+1F600 会映射为 0xD83D DE00。
    0xD83D 处于0xD800–0xDBFF之间 为 32 比特编码的前 16 比特 — 0001
    DE00 在0xDC00–0xDFFF之间为 32 比特编码的后 16 比特 —F600

    UTF-8:U+0020 映射为 0x20(0x20最高位为0 表示这是一个单字节的unicode编码对应的就是U+0020),U+6C49 映射为 0xE6 B1 89
    (0xE6 B1 89 最高位为E最高两比特不是10即它不是后续字节,则其为首字节,并且连续有3个1表示其后的E6 B1 89这3个字节共同还原一个unicode编码,B1和89都是最高两位都是10表示 这两个字节都是后续字节),
    而 U+1F600 会映射为 0xF0 9F 98 80。
    (0xF0 9F 98 80 最高位为E最高两比特不是10即它不是后续字节,则其为首字节,并且连续有4个1表示其后的F0 9F 98 80这4个字节(共32位)共同还原一个unicode编码,9F、98和80都是最高两位都是10表示 这三个字节都是后续字节),

    Unicode 有好几种(上面还不是全部)不同的编码方式,上面的 16 比特和 32 比特编码方式还有小头党和大头党之争(“汉”按字节读取时是 6C 49 呢,还是 49 6C?);同时,任何一种编码方式还需要跟传统的编码方式容易区分。
    因此,Unicode 文本文件通常有一个使用 BOM(byte order mark)字符的约定,即字符 U+FEFF。由于 Unicode 不使用 U+FFFE,在文件开头加一个 BOM 即可区分各种不同编码:

    如果文件开头是 0x00 00 FE FF,那这是大头在前的 UTF-32 编码;
    否则如果文件开头是 0xFF FE 00 00,那这是小头在前的 UTF-32 编码;
    否则如果文件开头是 0xFE FF,那这是大头在前的 UTF-16 编码;
    否则如果文件开头是 0xFF FE,那这是小头在前的 UTF-16 编码(注意,这条规则和第二条的顺序不能相反);
    否则如果文件开头是 0xEF BB BF,那这是 UTF-8 编码;
    否则,编码方式使用其他算法来确定。

    编辑器可以(有些在配置之后)根据 BOM 字符来自动决定文本文件的编码。比如,我一般在 Vim 中配置 set fileencodings=ucs-bom,utf-8,gbk,latin1。这样,Vim 在读入文件时,会首先检查 BOM 字符,有 BOM 字符按 BOM 字符决定文件编码;否则,试图将文件按 UTF-8 来解码(由于 UTF-8 有格式要求,非 UTF-8 编码的文件通常会导致失败);不行,则试图按 GBK 来解码(失败的概率就很低了);还不行,就把文件当作 Latin1 来处理(永远不会失败)。
    Windows 上编译 UTF-8 源码的文件,一定要有 BOM 字符告诉OS这是utf8。这也是很多错误的来源。
    在 UTF-8 编码下使用 BOM 字符并非必需,尤其在 Unix 上。但 Windows 上通常会使用 BOM 字符,以方便区分 UTF-8 和传统编码(因为windows上的传统编码(ANSI?)一般是当前locale的编码,例如简体中文就是gbk)。

    c++中的Unicode字符类型
    C++98 中有 char 和 wchar_t 两种不同的字符类型,其中 char 的长度是单字节,而 wchar_t 的长度不确定。在 Windows 上它是双字节,只能代表 UTF-16,而在 Unix 上一般是四字节,可以代表 UTF-32。为了解决这种混乱,在新的c++标准中有了下面的改进:

    C++11 引入了 char16_t 和 char32_t 两个独立的字符类型(不是typedef的类型别名 就是两个新的类型),分别代表 UTF-16 和 UTF-32。
    C++20 将引入 char8_t 类型,进一步区分了可能使用传统编码的窄字符char表示传统编码(win上 英文是windows-1252,简体中文是 GBK)和 UTF-8 字符类型。

    除了 string 和 wstring,我们也相应地有了 u16string、u32string(和将来的 u8string)。
    u8string u16string u32string 默认是一个字符所占的字节数分别是 1 2 4

    除了传统的窄字符 / 字符串字面量(如 “hi”)和宽字符 / 字符串字面量(如 L”hi”),引入了新的 UTF-8、UTF-16 和 UTF-32 字面量,分别形如 u8”hi”、u”hi” 和 U”hi”。—8,16,32

    为了确保非 ASCII 字符在源代码中可以简单地输入,引入了新的 Unicode 换码序列。比如,我们前面说到的三个字符可以这样表达成一个 UTF-32 字符串字面量:U” \u6C49\U0001F600”。要生成 UTF-16 或 UTF-8 字符串字面量只需要更改前缀即可。
    汉 笑脸

    下面的代码是UTF32和其他两种UTF编码的相互转换

    1. #include <iomanip>
    2. #include <iostream>
    3. #include <stdexcept>
    4. #include <string>
    5. using namespace std;
    6. const char32_t unicode_max = 0x10FFFF;
    7. void to_utf_16(char32_t ch,u16string& result)//将单个字符的UTF32的编码 转换成 UTF16的编码
    8. {
    9. if (ch > unicode_max) {//大于unicode编码的最大值 出错
    10. throw runtime_error(
    11. "invalid code point");
    12. }
    13. if (ch < 0x10000) {//0000 - ffff 这个ascii码范围是直接兼容的 使用 16 比特的直接映射
    14. result += char16_t(ch);
    15. } else {//大于U+ffff 使用32比特的特殊映射关系
    16. //内容是0xD800–0xDBFF,那这就是 32 比特编码的前 16 比特
    17. char16_t first =
    18. 0xD800 |
    19. ((ch - 0x10000) >> 10);//utf32的10000相当于UTF-16编码的D800 DC00
    20. //内容是 0xDC00–0xDFFF,那这是 32 比特编码的后 16 比特
    21. char16_t second =
    22. 0xDC00 | (ch & 0x3FF);
    23. //果内容在 0xD800–0xDFFF 之外,那就是一个 16 比特的直接映射。
    24. result += first;
    25. result += second;
    26. }
    27. }
    28. void to_utf_8(char32_t ch,
    29. string& result)
    30. {
    31. if (ch > unicode_max) {//大于unicode编码的最大值 出错
    32. throw runtime_error(
    33. "invalid code point");
    34. }
    35. //utf8是1-4字节的变长编码 utf8序列中 要看连续的1-4个字节才能还原1个UTF32
    36. //这里是utf32转为utf8--所以一个utf32可被转换为 1-4字节的utf8序列
    37. if (ch < 0x80) {//1字节
    38. result += ch;
    39. } else if (ch < 0x800) {//2字节
    40. result += 0xC0 | (ch >> 6);
    41. result += 0x80 | (ch & 0x3F);
    42. } else if (ch < 0x10000) {//3字节
    43. result += 0xE0 | (ch >> 12);
    44. result +=
    45. 0x80 | ((ch >> 6) & 0x3F);
    46. result += 0x80 | (ch & 0x3F);
    47. } else {//4字节
    48. result += 0xF0 | (ch >> 18);
    49. result +=
    50. 0x80 | ((ch >> 12) & 0x3F);
    51. result +=
    52. 0x80 | ((ch >> 6) & 0x3F);
    53. result += 0x80 | (ch & 0x3F);
    54. }
    55. }
    56. int main()
    57. {
    58. char32_t str[] =
    59. U" \u6C49\U0001F600";
    60. u16string u16str;
    61. string u8str;
    62. for (auto ch : str) {
    63. if (ch == 0) {
    64. break;
    65. }
    66. to_utf_16(ch, u16str);
    67. to_utf_8(ch, u8str);
    68. }
    69. cout << hex << setfill('0');
    70. for (char16_t ch : u16str) {
    71. cout << setw(4) << unsigned(ch)
    72. << ' ';
    73. }
    74. //0020 6c49 d83d de00
    75. cout << endl;
    76. for (unsigned char ch : u8str) {
    77. cout << setw(2) << unsigned(ch)
    78. << ' ';
    79. }
    80. //20 e6 b1 89 f0 9f 98 80
    81. cout << endl;
    82. }


    平台区别

    unix
    linx和macOS已经全面转向utf8,这样的系统中一般直接使用char[]和string来代表utf8字符串,包括输入 输出 和文件名,比较清晰。
    在真正需要文字处理(真正的文本基本都是用unicode编码 而非 单字节的ascii编码)的场合转换到UTF32(标准unicode编码 更清晰)往往会更简单,在以前及需要和 C 兼容的场合,会使用 wchar_t、uint32_t 或某个等价的类型别名;在新的纯 C++ 代码里,可使用 char32_t 和 u32string。

    unix下输出宽字符串(char32_t ,u32string) 需要使用wcout(这点和windows相同)。并且需要进行取余设置 如下

    1. std::locale::global(std::locale(""));
    2. std::wcout.imbue(std::locale());

    但是没有什么额外好处,反而可能在某些环境因为区域设置失败而引发问题,Unix 平台下一般只用 cout,不用 wcout。

    windows
    由于历史原因和保留向后兼容性的需要,一直使用char表示传统编码(win上 英文是windows-1252,简体中文是 GBK),用wchar_t表示UTF16。
    由于传统编码的设置需要重启才能生效,为了的得到多语言支持在和OS交互时必须使用UTF16。

    对于纯win编程,全面使用宽字符(wchar)是最简单的处理方式,但是文本很少用UTF16存储,大部分还是用的utf8存储(并加上BOM字符来和纯ascii码区别)。
    微软的编译器会把源码里窄字符字面量(解释可以看上面)中的非ASCII字符(比如中文的”你好” 没有加前缀的字面量默认是窄字符字面量 你好又是非ASCII字符)转换成char表示的传统编码(win上 英文是windows-1252,简体中文是 GBK),同样的源代码在不同编码的windows下可能产生不同的结果(比如当前win为英文系统则 传统编码为windows-1252 若当前windows为中文系统则传统编码为 GBK)。 所以如果你希望保留utf8序列在编码时尽量使用UTF8字面量

    1. #include <stdio.h>
    2. template <typename T>
    3. void dump(const T& str)
    4. {
    5. for (char ch : str) {
    6. printf(
    7. "%.2x ",
    8. static_cast<unsigned char>(ch));
    9. }
    10. putchar('\n');
    11. }
    12. int main()
    13. {
    14. // 传统编码为GBK时的结果是
    15. char str[] = "你好";
    16. char u8str[] = u8"你好";
    17. dump(str);//c4 e3 ba c3 00 在传统编码为windows-1252时 结果不一样
    18. dump(u8str);//e4 bd a0 e5 a5 bd 00 传统编码变了,依旧一样
    19. }


    Windows 下的 wcout 主要用在配合宽字符的输出,此外没什么大用处。原因一样,只有进行了正确的区域设置,才能输出跟该区域相匹配的宽字符串(不匹配的字符将导致后续输出全部消失!)。如果要输出中文,得写 setlocale(LC_ALL, “Chinese_China.936”);,这显然就让“统一码”unicode输出失去意义了(因为对于每个中文unicode都有 唯一编码 编译器应该能根据编码直接输出中文,而还需要手动设置有点多余)。


    但是(还是有个“但是”),如果你只用 wcout,不用cout或任何使用窄字符输出到 stdout 的函数(如 puts),这时倒有个还不错的解决方案,可以在终端输出多语言。我也是偶然才发现这一用法,并且没有在微软的网站上找到清晰的文档……代码如下所示:

    1. #include <fcntl.h>
    2. #include <io.h>
    3. #include <iostream>
    4. int main()
    5. {
    6. _setmode(_fileno(stdout),_O_WTEXT);
    7. std::wcout
    8. << L"中文 Español Français\n";
    9. std::wcout
    10. << "Narrow characters are "
    11. "also OK on wcout\n";
    12. // but not on cout...
    13. }


    由于窄字符在大部分Windows 系统上只支持传统编码,要打开一个当前编码不支持的文件名称,就必需使用宽字符的文件名。微软的fstream系列类及其open成员函数都支持 const wchar_t 类型的文件名,这是 C++ 标准里所没有的。

    *统一化处理


    想要写出跨平台的处理字符串的代码,考虑两种方式之一
    源代码兼容,但内码不同
    源代码和内码都完全兼容
    微软推荐的方式一般是前者。做 Windows 开发的人很多都知道 tchar.h 和 _T 宏,它们就起着类似的作用(虽然目的不同)。根据预定义宏的不同,系统会在同一套代码下选择不同的编码方式及对应的函数。

    1. #include <stdio.h>
    2. #include <tchar.h>
    3. int _tmain(int argc, TCHAR* argv[])
    4. {
    5. _putts(_T("Hello world!\n"));
    6. }
    7. //如果用缺省的命令行参数进行编译,上面的代码相当于
    8. #include <stdio.h>
    9. int main(int argc, char* argv[])
    10. {
    11. puts("Hello world!\n");
    12. }
    13. //而如果在命令行上加上了 /D_UNICODE,那代码则相当于
    14. #include <stdio.h>
    15. int wmain(int argc, wchar_t* argv[])
    16. {
    17. _putws(L"Hello world!\n");
    18. }

    当然,这个代码还是只能在 Windows 上用,并且仍然不漂亮(所有的字符和字符串字面量都得套上 _T)。后者无解,前者则可以找到替代方案(甚至自己写也不复杂)。C++ REST SDK 中就提供了类似的封装,可以跨平台地开发网络应用。但可以说,这种方式是一种主要照顾 Windows 的开发方式。


    相应的,对 Unix 开发者而言更自然的方式是全面使用 UTF-8,仅在跟操作系统、文件系统打交道时把字符串转换成需要的编码。利用临时对象的生命周期,我们可以像下面这样写帮助函数和宏。
    utf8_to_native.hpp:

    1. #ifndef UTF8_TO_NATIVE_HPP
    2. #define UTF8_TO_NATIVE_HPP
    3. #include <string>
    4. #if defined(_WIN32) || \
    5. defined(_UNICODE) //win系统下 定义两个API将utf8转成utf16(wstring)
    6. std::wstring utf8_to_wstring(
    7. const char* str);
    8. std::wstring utf8_to_wstring(
    9. const std::string& str);
    10. #define NATIVE_STR(s) \
    11. utf8_to_wstring(s).c_str() //win下生成一个临时对象,当前语句执行结束后这个临时对象会自动销毁。
    12. #else // #if defined //Unix系统下 不管提供的是字符指针还是string都会转换成字符指针
    13. inline const char*
    14. to_c_str(const char* str)
    15. {
    16. return str;
    17. }
    18. inline const char*
    19. to_c_str(const std::string& str)
    20. {
    21. return str.c_str();
    22. }
    23. #define NATIVE_STR(s) \
    24. to_c_str(s)
    25. #endif //#if defined
    26. #endif // UTF8_TO_NATIVE_HPP
    27. utf8_to_native.cpp
    28. #include "utf8_to_native.hpp"
    29. #if defined(_WIN32) || \
    30. defined(_UNICODE)
    31. #include <windows.h>
    32. #include <system_error>
    33. namespace {
    34. void throw_system_error(
    35. const char* reason)
    36. {
    37. std::string msg(reason);
    38. msg += " failed";
    39. std::error_code ec(
    40. GetLastError(),
    41. std::system_category());
    42. throw std::system_error(ec, msg);
    43. }
    44. } /* unnamed namespace */
    45. std::wstring utf8_to_wstring(
    46. const char* str)
    47. {
    48. int len = MultiByteToWideChar(
    49. CP_UTF8, 0, str, -1,
    50. nullptr, 0);
    51. if (len == 0) {
    52. throw_system_error(
    53. "utf8_to_wstring");
    54. }
    55. std::wstring result(len - 1,
    56. L'\0');
    57. if (MultiByteToWideChar(
    58. CP_UTF8, 0, str, -1,
    59. result.data(), len) == 0) {
    60. throw_system_error(
    61. "utf8_to_wstring");
    62. }
    63. return result;
    64. }
    65. std::wstring utf8_to_wstring(
    66. const std::string& str)
    67. {
    68. return utf8_to_wstring(
    69. str.c_str());
    70. }
    71. #endif
    72. #include <fstream>
    73. #include "utf8_to_native.hpp"
    74. int main()
    75. {
    76. using namespace std;
    77. const char filename[] =
    78. u8"测试.txt";//utf8
    79. ifstream ifs(
    80. NATIVE_STR(filename));
    81. // 对 ifs 进行操作
    82. }

    上面这样的代码可以同时适用于现代 Unix 和现代 Windows(任何语言的传统编码设置下),用来读取名为“测试.txt”的文件。


    API支持

    编程支持结束之前,我们快速介绍一下其他的一些支持 Unicode 及其转换的 API。
    Windows API
    上一节的代码在 Windows 下用到了 MultiByteToWideChar,从某个编码转到 UTF-16。Windows 也提供了反向的 WideCharToMultiByte,从 UTF-16 转到某个编码。从上面可以看到,C 接口用起来并不方便,可以考虑自己封装一下。
    iconv
    Unix 下最常用的底层编码转换接口是 iconv ,提供 iconv_open、iconv_close 和 iconv 三个函数。这同样是 C 接口,实践中应该封装一下。
    ICU4C
    ICU是一个完整的 Unicode 支持库,提供大量的方法,ICU4C 是其 C/C++ 的版本。ICU 有专门的字符串类型,内码是 UTF-16,但可以直接用于 IO streams 的输出。下面的程序应该在所有平台上都有同样的输出(但在 Windows 上要求当前系统传统编码能支持待输出的字符):

    1. #include <iostream>
    2. #include <string>
    3. #include <unicode/unistr.h>
    4. #include <unicode/ustream.h>
    5. using namespace std;
    6. using icu::UnicodeString;
    7. int main()
    8. {
    9. auto str = UnicodeString::fromUTF8(
    10. u8"你好");
    11. cout << str << endl;
    12. string u8str;
    13. str.toUTF8String(u8str);
    14. cout << "In UTF-8 it is "
    15. << u8str.size() << " bytes"
    16. << endl;
    17. }


    codecvt
    C++11 曾经引入了一个头文件 用作 UTF 编码间的转换,但很遗憾,那个头文件目前已因为存在安全性和易用性问题被宣告放弃(deprecated)。 中有另外一个 codecvt 模板 ,本身接口不那么好用,而且到 C++20 还会发生变化,这儿也不详细介绍了。有兴趣的话可以直接看参考资料。

    这几种编码之间相互有影响吗?到底之间关系是如何?平时我们开发过程中,应该怎么理解这几者之间的关系?
    互相基本没影响。但是,如果你要把程序里硬编码的字符串跟输入一起混合输出,那就必须保证编码相同了。如果输出到终端,那还要保证终端的设置与之相匹配——Linux上通常为UTF-8,(中文)Windows上通常为GBK。
    对unicode很详细的解释 如果以后真的要用,先看下 下面的这篇博客
    https://www.cnblogs.com/benbenalin/tag/Unicode/