C 编程:如何为 Unicode 编程?
Posted
技术标签:
【中文标题】C 编程:如何为 Unicode 编程?【英文标题】:C programming: How to program for Unicode? 【发布时间】:2010-10-06 07:13:21 【问题描述】:进行严格的 Unicode 编程需要哪些先决条件?
这是否意味着我的代码不应该在任何地方使用char
类型并且需要使用可以处理wint_t
和wchar_t
的函数?
那么多字节字符序列在这个场景中的作用是什么?
【问题讨论】:
【参考方案1】:C99 或更早版本
C 标准 (C99) 提供了宽字符和多字节字符,但由于无法保证这些宽字符可以容纳什么,因此它们的价值受到了一定的限制。对于给定的实现,它们提供了有用的支持,但如果您的代码必须能够在实现之间移动,则无法保证它们有用。
因此,Hans van Eck 建议的方法(即围绕 ICU - International Components for Unicode - library 编写包装器)是合理的,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 通用字符名称
语法通用字符名:
\u
hex-quad\U
hex-quad hex-quadhex-quad:十六进制数字 十六进制数字 十六进制数字 十六进制数字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>
定义 size_t
,
<wchar.h>
定义 mbstate_t
,
和<stdint.h>
定义uint_least16_t
和uint_least32_t
。)
<uchar.h>
标头还定义了一组最小的(可重新启动的)转换函数:
mbrtoc16()
c16rtomb()
mbrtoc32()
c32rtomb()
关于哪些 Unicode 字符可以使用\unnnn
或\U00nnnnnn
符号在标识符中使用有一些规则。您可能必须主动激活对标识符中此类字符的支持。例如,GCC 要求 -fextended-identifiers
允许在标识符中使用这些。
请注意,macOS Sierra (10.12.5) 仅举一个平台,不支持<uchar.h>
。
【讨论】:
我认为你在这里卖的wchar_t
和朋友有点短。这些类型对于允许 C 库以 any 编码(包括非 Unicode 编码)处理文本至关重要。如果没有广泛的字符类型和函数,C 库将需要一组文本处理函数来处理每个支持的编码:想象一下 koi8len、koi8tok、koi8printf 仅用于 KOI-8 编码文本和 utf8len, utf8tok, utf8printf 用于 UTF-8 文本。相反,我们很幸运只有 一个 组这些函数(不包括原始 ASCII 函数):wcslen
、wcstok
和 wprintf
。
程序员需要做的就是使用 C 库字符转换函数(mbstowcs
和朋友)将任何支持的编码转换为wchar_t
。一旦采用wchar_t
格式,程序员就可以使用C 库提供的单组宽文本处理函数。一个好的 C 库实现将支持大多数程序员需要的几乎任何编码(在我的一个系统上,我可以访问 221 种唯一编码)。
至于它们是否足够宽以至于有用:标准要求实现必须保证wchar_t
足够宽以包含实现支持的任何字符。这意味着(可能有一个明显的例外)大多数实现将确保它们足够宽,以使使用 wchar_t
的程序可以处理系统支持的任何编码(Microsoft 的 wchar_t
只有 16 位宽,这意味着它们的实现可以不完全支持所有编码,尤其是各种 UTF 编码,但它们是例外而不是规则)。【参考方案2】:
请注意,这不是关于“严格的 unicode 编程”本身,而是一些实践经验。
我们在公司所做的是围绕 IBM 的 ICU 库创建一个包装库。包装库有一个 UTF-8 接口,在需要调用 ICU 时转换为 UTF-16。在我们的例子中,我们并不太担心性能下降。当性能成为问题时,我们还提供了 UTF-16 接口(使用我们自己的数据类型)。
应用程序可以基本保持原样(使用 char),但在某些情况下,它们需要注意某些问题。例如,我们使用包装器代替 strncpy() 来避免切断 UTF-8 序列。在我们的例子中,这已经足够了,但也可以考虑检查组合字符。我们还有用于计算代码点数量、字素数量等的包装器。
在与其他系统交互时,我们有时需要进行自定义字符组合,因此您可能需要一些灵活性(取决于您的应用程序)。
我们不使用 wchar_t。使用 ICU 避免了可移植性方面的意外问题(但当然不会出现其他意外问题 :-)。
【讨论】:
一个有效的 UTF-8 字节序列永远不会被 strncpy 截断(截断)。有效的 UTF-8 序列可能不包含任何 0x00 字节(当然,终止的空字节除外)。 @Dan Moulding:如果你 strncpy(),比如说,一个包含单个汉字(可能是 3 个字节)的字符串到一个 2 字节的字符数组中,你会创建一个无效的 UTF-8 序列. @Hans van Eck:如果您的包装器将单个 3 字节汉字复制到 2 字节数组中,那么您要么截断它并创建一个无效序列,要么您会有未定义的行为。显然,如果你要复制数据,目标需要足够大;那不用说了。我的观点是,正确使用strncpy
与 UTF-8 一起使用是完全安全的。
@DanMoulding:如果你知道你的目标缓冲区足够大,你可以使用strcpy
(这对于UTF-8来说确实是安全的)。使用strncpy
的人可能这样做是因为他们不知道目标缓冲区是否足够大,所以他们希望传递最大数量的字节来复制——这确实可能会创建无效的 UTF-8序列。【参考方案3】:
这个FAQ 是一个丰富的信息。在该页面和this article by Joel Spolsky 之间,您将有一个良好的开端。
我在此过程中得出的一个结论:
wchar_t
在 Windows 上是 16 位,但在其他平台上不一定是 16 位。我认为这在 Windows 上是必要的邪恶,但可能可以在其他地方避免。它在 Windows 上很重要的原因是您需要它来使用名称中包含非 ASCII 字符的文件(以及 W 版本的函数)。
请注意,采用 wchar_t
字符串的 Windows API 需要 UTF-16 编码。另请注意,这与 UCS-2 不同。注意代理对。这个test page 有启发性的测试。
【讨论】:
请注意,stdiof*
和朋友在 每个 平台上都使用 char *
,因为标准是这样规定的——使用 wcs*
代替 wchar_t。【参考方案4】:
要做严格的 Unicode 编程:
仅使用支持 Unicode 的字符串 API(不使用strlen
、strcpy
、...但它们的宽字符串对应物 wstrlen
、wsstrcpy
、...)
处理文本块时,使用允许无损失地存储 Unicode 字符(utf-7、utf-8、utf-16、ucs-2、...)的编码。
检查您的操作系统默认字符集是否与 Unicode 兼容(例如:utf-8)
使用与 Unicode 兼容的字体(例如 arial_unicode)
多字节字符序列是一种早于 UTF-16 编码(通常与 wchar_t
一起使用的编码)的编码,在我看来,它仅适用于 Windows。
我从未听说过wint_t
。
【讨论】:
wint_t 是在最重要的是始终明确区分文本和二进制数据。尝试遵循Python 3.x str
vs. bytes
或SQL TEXT
与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
s。 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 or UTF-16 or UTF-32 or UCS-2 Is wchar_t needed for Unicode support?没有。 UTF-8 是使用char*
字符串的完全有效的Unicode 编码。它的优点是,如果您的程序对非 ASCII 字节是透明的(例如,作用于 \r
和 \n
的换行符,但通过其他字符不变),您根本不需要进行任何更改!
如果您使用 UTF-8,则需要更改 char
= 字符(例如,不要在循环中调用 toupper
)或 char
= 屏幕列(例如,用于文本换行)。
如果您使用 UTF-32,您将拥有固定宽度字符的简单性(但不是固定宽度 graphemes,但需要更改所有字符串的类型) .
如果您使用 UTF-16,您将不得不放弃固定宽度字符的假设和 8 位代码单元的假设,这使得这是最困难的升级路径来自单字节编码。
我会建议积极避免 wchar_t
,因为它不是跨平台的:有时是 UTF-32,有时是 UTF-16,有时是预 Unicode 东亚编码。我建议使用typedefs
更重要的是,avoid TCHAR
。
【讨论】:
我不认为这很不幸 - char 是一个 int。这是一个好处。使用文字字符常量是一种用途。如果最后我记得传递了const char *
,则采用char *
的函数可能会出现问题(但我对此以及哪些函数含糊不清,因此请稍加注意)。仅仅因为它与其他语言更复杂并不意味着它是一个糟糕的设计。
由于可以对普通的char
进行签名,因此对UTF8
使用普通字符会导致符号扩展出现问题。对 UTF8 也使用 unsigned char
— 或 uint8_t
。【参考方案6】:
据我所知,wchar_t 依赖于实现(从wiki article 可以看出)。而且它不是unicode。
【讨论】:
【参考方案7】:我不会相信任何标准库实现。只需滚动您自己的 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;
【讨论】:
【参考方案8】:您基本上希望将内存中的字符串作为wchar_t
数组而不是char 来处理。当您执行任何类型的 I/O(如读取/写入文件)时,您可以使用 UTF-8(这可能是最常见的编码)进行编码/解码,这很容易实现。只需谷歌 RFC。所以在内存中没有什么应该是多字节的。一个wchar_t
代表一个字符。然而,当你开始序列化时,你需要编码为 UTF-8 之类的东西,其中一些字符由多个字节表示。
您还必须为宽字符串编写新版本的strcmp
等,但这不是什么大问题。最大的问题是与只接受 char 数组的库/现有代码的互操作。
当涉及到sizeof(wchar_t)
(如果你想正确的话,你将需要 4 个字节),如果需要,你可以随时使用typedef
/macro
hacks 将其重新定义为更大的大小。
【讨论】:
以上是关于C 编程:如何为 Unicode 编程?的主要内容,如果未能解决你的问题,请参考以下文章
如何为我在 Swift 中以编程方式创建的 UIViewController 子类创建 init?