86

进行严格的 Unicode 编程需要哪些先决条件?

这是否意味着我的代码不应该char在任何地方使用类型,并且需要使用可以处理和的wint_t函数wchar_t

而多字节字符序列在这个场景中起到什么作用呢?

4

8 回答 8

43

C99 或更早版本

C 标准 (C99) 提供了宽字符和多字节字符,但由于无法保证这些宽字符可以容纳什么,因此它们的价值受到了一定的限制。对于给定的实现,它们提供了有用的支持,但是如果您的代码必须能够在实现之间移动,则不能充分保证它们是有用的。

因此,Hans van Eck 建议的方法(即围绕 ICU - Unicode 的国际组件 - 库编写一个包装器)是合理的,IMO。

UTF-8 编码有很多优点,其中之一是如果你不弄乱数据(例如截断它),那么它可以被不完全了解 UTF-8 复杂性的函数复制编码。绝对不是这种情况wchar_t

完整的 Unicode 是 21 位格式。也就是说,Unicode 保留了从 U+0000 到 U+10FFFF 的代码点。

关于 UTF-8、UTF-16 和 UTF-32 格式(其中 UTF 代表 Unicode 转换格式 - 请参阅Unicode)的有用之处之一是您可以在这三种表示之间进行转换而不会丢失信息。每个人都可以代表其他人可以代表的任何事物。UTF-8 和 UTF-16 都是多字节格式。

众所周知,UTF-8 是一种多字节格式,其结构严谨,可以可靠地找到字符串中字符的开头,从字符串中的任何点开始。单字节字符的高位设置为零。多字节字符的第一个字符以位模式 110、1110 或 11110(对于 2 字节、3 字节或 4 字节字符)之一开头,后续字节总是从 10 开始。连续字符总是在范围 0x80 .. 0xBF。有一些规则要求 UTF-8 字符必须以尽可能少的格式表示。这些规则的一个后果是字节 0xC0 和 0xC1(也是 0xF5..0xFF)不能出现在有效的 UTF-8 数据中。

 U+0000 ..   U+007F  1 byte   0xxx xxxx
 U+0080 ..   U+07FF  2 bytes  110x xxxx   10xx xxxx
 U+0800 ..   U+FFFF  3 bytes  1110 xxxx   10xx xxxx   10xx xxxx
U+10000 .. U+10FFFF  4 bytes  1111 0xxx   10xx xxxx   10xx xxxx   10xx xxxx

最初,人们希望 Unicode 是一个 16 位的代码集,并且一切都适合 16 位的代码空间。不幸的是,现实世界更加复杂,不得不将其扩展到当前的 21 位编码。

因此,UTF-16 是“基本多语言平面”的单个单元(16 位字)代码集,这意味着具有 Unicode 代码点 U+0000 .. U+FFFF 的字符,但使用两个单元(32 位)来表示此范围之外的字符。因此,使用 UTF-16 编码的代码必须能够处理可变宽度编码,就像 UTF-8 一样。双单元字符的代码称为代理。

代理是来自两个特殊范围的 Unicode 值的代码点,保留用作 UTF-16 中成对代码单元的前导和尾随值。前导(也称为高)代理从 U+D800 到 U+DBFF,尾随或低代理从 U+DC00 到 U+DFFF。它们被称为代理,因为它们不直接表示字符,而只是作为一对。

当然,UTF-32 可以在单个存储单元中编码任何 Unicode 代码点。它对计算有效,但对存储无效。

您可以在ICU和 Unicode 网站上找到更多信息。

C11 和<uchar.h>

C11 标准改变了规则,但即使是现在(2017 年年中),也不是所有的实现都赶上了这些变化。C11 标准将 Unicode 支持的更改总结为:

  • Unicode 字符和字符串 ( <uchar.h>)(最初在 ISO/IEC TR 19769:2004 中指定)

以下是功能的最小概述。该规范包括:

6.4.3 通用字符名称

语法
Universal-character-name:
    \u hex-quad
    \U hex-quad hex-quad
hex-quad:
    十六进制数字 hexadecimal-digit hexadecimal-digit hexadecimal-digit

7.28 Unicode 实用程序<uchar.h>

标头<uchar.h>声明了用于操作 Unicode 字符的类型和函数。

声明的类型是mbstate_t(在 7.29.1 中描述)和size_t(在 7.19 中描述);

char16_t

它是用于 16 位字符的无符号整数类型,与uint_least16_t(在 7.20.1.2 中描述)的类型相同;和

char32_t

它是用于 32 位字符的无符号整数类型,与uint_least32_t(也在 7.20.1.2 中描述)相同。

(翻译交叉引用:<stddef.h>defines size_t<wchar.h>defines mbstate_t、defines 和<stdint.h>definesuint_least16_tuint_least32_t。)<uchar.h>头文件还定义了一组最小的(可重新启动的)转换函数:

  • mbrtoc16()
  • c16rtomb()
  • mbrtoc32()
  • c32rtomb()

有关于哪些 Unicode 字符可以在标识符中使用的规则,使用\unnnnor\U00nnnnnn符号。您可能必须主动激活对标识符中此类字符的支持。例如,GCC 要求-fextended-identifiers在标识符中允许这些。

请注意,macOS Sierra (10.12.5),仅举一个平台,不支持<uchar.h>.

于 2009-02-09T07:00:49.923 回答
22

请注意,这不是关于“严格的 unicode 编程”本身,而是一些实践经验。

我们在我公司所做的是围绕 IBM 的 ICU 库创建一个包装库。包装库有一个 UTF-8 接口,在需要调用 ICU 时转换为 UTF-16。在我们的例子中,我们并不太担心性能下降。当性能成为问题时,我们还提供了 UTF-16 接口(使用我们自己的数据类型)。

应用程序可以基本保持原样(使用 char),尽管在某些情况下它们需要注意某些问题。例如,我们使用包装器代替 strncpy() 来避免切断 UTF-8 序列。在我们的例子中,这已经足够了,但也可以考虑检查组合字符。我们还有用于计算代码点数量、字素数量等的包装器。

在与其他系统交互时,我们有时需要进行自定义角色组合,因此您可能需要一些灵活性(取决于您的应用程序)。

我们不使用 wchar_t。使用 ICU 避免了可移植性方面的意外问题(但不是其他意外问题,当然 :-)。

于 2009-02-08T22:44:06.560 回答
11

常见问题解答包含大量信息。在该页面和Joel Spolsky 的这篇文章之间,您将有一个良好的开端。

我在此过程中得出的一个结论:

  • wchar_t在 Windows 上是 16 位,但在其他平台上不一定是 16 位。我认为这在 Windows 上是必要的邪恶,但可能可以在其他地方避免。它在 Windows 上很重要的原因是您需要它来使用名称中包含非 ASCII 字符的文件(以及 W 版本的函数)。

  • 请注意,采用wchar_t字符串的 Windows API 需要 UTF-16 编码。另请注意,这与 UCS-2 不同。注意代理对。这个测试页面有启发性的测试。

  • 如果您在 Windows 上编程,则不能使用fopen(), fread(),fwrite()等,因为它们只接受char *并且不理解 UTF-8 编码。使便携性变得痛苦。

于 2009-02-09T16:34:13.867 回答
7

要进行严格的 Unicode 编程:

  • 仅使用支持 Unicode 的字符串 API(NOT strlen , strcpy, ... 但它们的宽字符串对应物wstrlen, wsstrcpy, ...)
  • 处理文本块时,使用允许存储 Unicode 字符(utf-7、utf-8、utf-16、ucs-2、...)而不会丢失的编码。
  • 检查您的操作系统默认字符集是否与 Unicode 兼容(例如:utf-8)
  • 使用与 Unicode 兼容的字体(例如 arial_unicode)

多字节字符序列是一种早于 UTF-16 编码(通常与 UTF-16 一起使用的编码)的编码,wchar_t在我看来它仅适用于 Windows。

我从来没有听说过wint_t

于 2009-02-08T21:56:31.727 回答
3

最重要的是始终明确区分文本和二进制数据。尝试遵循Python 3.x strvs.bytes或 SQL TEXTvs.的模型BLOB

不幸的是,C 将char“ASCII 字符”和int_least8_t. 您需要执行以下操作:

typedef char UTF8; // for code units of UTF-8 strings
typedef unsigned char BYTE; // for binary data

您可能还需要 UTF-16 和 UTF-32 代码单元的 typedef,但这更复杂,因为wchar_t未定义编码。你只需要一个预处理器#if。C 和 C++0x 中一些有用的宏是:

  • __STDC_UTF_16__— 如果已定义,则类型_Char16_t存在且为 UTF-16。
  • __STDC_UTF_32__— 如果已定义,则类型_Char32_t存在且为 UTF-32。
  • __STDC_ISO_10646__— 如果已定义,wchar_t则为 UTF-32。
  • _WIN32— 在 Windows 上,wchar_t是 UTF-16,尽管这违反了标准。
  • WCHAR_MAX— 可用于确定 的大小wchar_t,但不能确定操作系统是否使用它来表示 Unicode。

这是否意味着我的代码不应该在任何地方使用 char 类型并且需要使用可以处理 wint_t 和 wchar_t 的函数?

也可以看看:

不,UTF-8 是一种使用char*字符串的完全有效的 Unicode 编码。它的优点是,如果您的程序对非 ASCII 字节是透明的(例如,一个行尾转换器,它作用于\r\n通过其他字符不变),您根本不需要进行任何更改!

如果您使用 UTF-8,则需要更改char= 字符(例如,不要toupper在循环中调用)或char= 屏幕列(例如,用于文本换行)的所有假设。

如果您使用 UTF-32,您将拥有固定宽度字符的简单性(但不是固定宽度字形,但需要更改所有字符串的类型)。

如果您使用 UTF-16,您将不得不放弃固定宽度字符假设和 8 位代码单元的假设,这使得这是从单字节编码升级最困难的路径。

我建议积极避免 wchar_t,因为它不是跨平台的:有时是 UTF-32,有时是 UTF-16,有时是预 Unicode 东亚编码。我建议使用typedefs

更重要的是,避免TCHAR.

于 2010-08-18T13:45:18.050 回答
2

据我所知, wchar_t 是依赖于实现的(从这篇wiki 文章可以看出)。而且它不是unicode。

于 2009-02-09T06:03:11.167 回答
2

您基本上希望将内存中的字符串作为wchar_t数组而不是 char 来处理。当您执行任何类型的 I/O(如读取/写入文件)时,您可以使用 UTF-8(这可能是最常见的编码)进行编码/解码,这很容易实现。只需谷歌 RFC。所以在内存中没有什么应该是多字节的。一个wchar_t代表一个字符。然而,当你开始序列化时,你需要编码为 UTF-8 之类的东西,其中一些字符由多个字节表示。

您还必须strcmp为宽字符串编写 etc. 的新版本,但这不是一个大问题。最大的问题将是与只接受 char 数组的库/现有代码的互操作。

当涉及到sizeof(wchar_t)(如果你想正确地做,你将需要 4 个字节),你可以随时使用typedef/ macrohacks 将它重新定义为更大的大小,如果你需要的话。

于 2009-02-09T06:40:42.903 回答
2

我不会相信任何标准库的实现。只需滚动您自己的 unicode 类型。

#include <windows.h>

typedef unsigned char utf8_t;
typedef unsigned short utf16_t;
typedef unsigned long utf32_t;

int main ( int argc, char *argv[] )
{
  int msgBoxId;
  utf16_t lpText[] = { 0x03B1, 0x0009, 0x03B2, 0x0009, 0x03B3, 0x0009, 0x03B4, 0x0000 };
  utf16_t lpCaption[] = L"Greek Characters";
  unsigned int uType = MB_OK;
  msgBoxId = MessageBoxW( NULL, lpText, lpCaption, uType );
  return 0;
}
于 2017-03-29T18:45:44.073 回答