C编程:如何为Unicode编程?


82

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

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

在这种情况下,多字节字符序列起什么作用?

Answers:


21

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

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

应用程序在很大程度上可以保持原样(使用char),尽管在某些情况下,它们需要注意某些问题。例如,代替strncpy(),我们使用了一个包装器,该包装器避免了截断UTF-8序列。在我们的例子中,这已经足够了,但是人们也可以考虑对组合字符进行检查。我们还提供包装器,用于计算代码点数量,字素数量等。

与其他系统连接时,有时我们需要自定义字符组成,因此您可能需要一些灵活性(取决于您的应用程序)。

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


2
有效的UTF-8字节序列永远不会被strncpy截断(截断)。有效的UTF-8序列不得包含任何0x00字节(当然,终止空字节除外)。
Dan Moulding 2010年

8
@Dan Moulding:如果将strncpy()包含2个字节的char数组的字符串包含一个中文字符(可能为3个字节),则会创建无效的UTF-8序列。
汉斯·凡·埃克

@Hans van Eck:如果包装程序将单个3字节汉字复制到2字节数组中,那么您将截断它并创建一个无效序列,或者将出现未定义的行为。显然,如果要复制数据,则目标必须足够大。那不用说了。我的观点是,strncpy正确使用UTF-8是绝对安全的。
Dan Moulding

5
@DanMoulding:如果您知道目标缓冲区足够大,则可以使用strcpy(对于UTF-8,这确实是安全的)。使用这种方法的人strncpy可能是因为他们不知道目标缓冲区是否足够大,所以他们希望传递最大数量的字节以进行复制-这实际上可能会创建无效的UTF-8序列。
Frerich Raabe 2013年

41

C99或更早

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

因此,IMO汉斯·范·埃克(Hans van Eck)建议的方法是合理的(该方法是围绕ICU(Unicode的国际组件)库编写包装程序)。

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-8一样,使用UTF-16编码的代码必须能够处理可变宽度的编码。双单位字符的代码称为代理。

代理是来自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 十六进制四
    \U 进制十六进制四进制十六进制四
进制
    十六进制数字十六进制数字十六进制数字十六进制数字

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>definesmbstate_t<stdint.h>definesuint_least16_tuint_least32_t。)<uchar.h>标头还定义了最少的一组(可重新启动的)转换函数:

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

关于使用\unnnn\U00nnnnnn表示法可在标识符中使用哪些Unicode字符的规则。您可能必须积极激活对标识符中此类字符的支持。例如,GCC要求-fextended-identifiers允许使用这些标识符。

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


3
我想你在卖东西wchar_t,朋友在这里有点矮。这些类型对于使C库以任何编码(包括非Unicode编码)处理文本都是必不可少的。如果没有广泛的字符类型和函数,C库将为每种受支持的编码要求一组文本处理函数:假设仅对KOI-8编码的文本使用koi8len,koi8tok,koi8printf,而对UTF-8则使用utf8len,utf8tok,utf8printf文本。相反,我们很幸运有只是一个集这些功能(不包括原来的ASCII的): ,wcslenwcstokwprintf
Dan Moulding 2010年

1
程序员需要做的就是使用C库字符转换函数(mbstowcs和朋友)将任何受支持的编码转换为wchar_t。一旦wchar_t格式,程序员可以使用单组宽文本处理函数C库提供。良好的C库实现几乎可以支持大多数程序员所需的任何编码(在我的系统中,我可以访问221种独特的编码)。
丹·莫尔丁

至于它们是否足够宽以有用:该标准要求实现必须保证wchar_t足够宽以包含实现所支持的任何字符。这意味着(可能会有一个显着的例外)大多数实现将确保它们的宽度足够大,以使所使用的程序wchar_t能够处理系统支持的任何编码(Microsoftwchar_t只有16位宽,这意味着它们的实现不完全支持所有编码,最值得注意的是各种UTF编码,但是它们是例外,而不是规则)。
Dan Moulding 2010年

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编码。使便携性痛苦。


请注意,stdiof*和朋友可以char *每个平台上使用,因为该标准是这样说的-请wcs*改为将其用于wchar_t。

7

要执行严格的Unicode编程:

  • 仅使用字符串的API也支持Unicode( strlenstrcpy...但他们的同行WideString的wstrlenwsstrcpy...)
  • 处理文本块时,请使用允许存储Unicode字符(utf-7,utf-8,utf-16,ucs-2等)的编码,而不会丢失。
  • 检查您的操作系统默认字符集是否兼容Unicode(例如:utf-8)
  • 使用与Unicode兼容的字体(例如arial_unicode)

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

我从未听说过wint_t


就像wchar_t一样,wint_t是在<wchar.h>中定义的类型。它在宽字符方面的作用与int在“字符”方面的作用相同。它可以容纳任何宽字符值或WEOF。
乔纳森·莱夫勒

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,则将拥有固定宽度字符的简单性(但不能获得固定宽度的graphemes,但需要更改所有字符串的类型)。

如果使用UTF-16,则必须同时放弃固定宽度字符8位代码单元的假设,这使之成为单字节编码最困难的升级途径。

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

更重要的是,请避免TCHAR


我认为这根本不是不幸的-char是一个int。那是一个好处。想到使用文字字符常量是一种用途。我记得,采用a的函数char *可能会遇到问题const char *(但是我对此含糊不清,因此对于哪些函数,要加点盐处理)。仅仅因为它与其他语言更加复杂并不意味着它是一个糟糕的设计。
Pryftan

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;
}

2

您基本上想将内存中的字符串作为wchar_t数组而不是char处理。当您执行任何类型的I / O(如读取/写入文件)时,都可以使用易于实现的UTF-8(这可能是最常见的编码)进行编码/解码。只是谷歌的RFC。因此,内存中的任何内容都不应该是多字节的。一个wchar_t代表一个字符。但是,当您要进行序列化时,那就是需要编码为UTF-8之类的东西,其中某些字符由多个字节表示。

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

而涉及到sizeof(wchar_t)(如果需要正确处理,您将需要4个字节),如果需要,您可以随时使用typedef/ macrohacks将其重新定义为更大的大小。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.