glibc 知:手册06:字符集处理

本文探讨了字符集处理技术,特别是在C库中支持多字符集的扩展功能,涉及宽字符处理、多字节与宽字符串转换函数、以及iconv通用字符集转换接口。介绍了mbrlen、mbrtowc、wcrtomb等函数的使用,并深入gconv模块的初始化、结束及转换过程,展示了如何编写高效且兼容的字符集转换模块。

1. 前言

The GNU C Library Reference Manual for version 2.35

2. 字符集处理

计算的早期使用的字符集每个字符只有 6、7 或 8 位:从来没有使用超过 8 位(一个字节)来表示单个字符的情况。随着越来越多的人处理非罗马字符集,这种方法的局限性变得更加明显,其中并非构成语言字符集的所有字符都可以用 2^8 个选项表示。本章展示了添加到 C 库以支持多个字符集的功能。

2.1. 扩展字符简介

有多种解决方案可用于克服字节和字符之间关系为 1:1 的字符集与比率为 2:1 或 4:1 的字符集之间的差异。本节的其余部分提供了一些示例,以帮助理解在开发 C 库的功能时所做的设计决策。

我们必须立即做出的区分是内部和外部表示之间的区别。内部表示是指程序使用的表示,同时将文本保存在内存中。当文本通过某种通信渠道存储或传输时,使用外部表示。外部表示的示例包括在目录中等待读取和解析的文件。

传统上,这两种表示方式之间没有区别。在内部和外部使用相同的单字节表示同样舒适和有用。这种舒适度会随着更多和更大的字符集而降低。

内部表示要克服的问题之一是处理使用不同字符集进行外部编码的文本。假设一个程序读取两个文本并使用一些度量比较它们。仅当文本在内部以通用格式保存时,比较才能有用地进行。

对于这样一种常见的格式(= 字符集),八位肯定已经不够用了。所以最小的实体必须增长:现在将使用宽字符。不是每个字符一个字节,而是使用两个或四个。(三个在内存中不好寻址,超过四个字节似乎没有必要)。

如本手册的其他部分所示,已经创建了一个全新的函数系列,可以处理内存中的宽字符文本。这种内部宽字符表示最常用的字符集是 Unicode 和 ISO 10646(也称为通用字符集的 UCS)。Unicode 最初计划为 16 位字符集;而 ISO 10646 被设计为 31 位大代码空间。这两个标准实际上是相同的。它们具有相同的字符库和代码表,但 Unicode 指定了附加语义。目前,只分配了前 0x10000 个代码位置(所谓的基本多语言平面,BMP)中的字符,但在这 16 位空间之外的更专业字符的分配已经在进行中。已为 Unicode 和 ISO 10646 字符定义了许多编码:UCS-2 是一个 16 位字,只能表示 BMP 中的字符,UCS-4 是一个 32 位字,不能表示任何 Unicode 和 ISO 10646 字符, UTF-8 是一种 ASCII 兼容编码,其中 ASCII 字符由 ASCII 字节表示,非 ASCII 字符由 2-6 个非 ASCII 字节的序列表示,最后 UTF-16 是 UCS-2 的扩展,其中某些对UCS-2 字可用于编码最大为 0x10ffff 的非 BMP 字符。

char 类型不适合表示宽字符。出于这个原因,ISO C 标准引入了一种新类型,旨在保留宽字符串中的一个字符。为了保持相似性,对于那些采用单个宽字符的函数,还有一个对应于 int 的类型。

数据类型:wchar_t

此数据类型用作宽字符串的基本类型。换句话说,这种类型的对象数组相当于多字节字符串的 char[]。该类型在 stddef.h 中定义。

引入 wchar_t 的 ISO C90 标准没有说明有关表示的任何具体内容。它只要求这种类型能够存储基本字符集的所有元素。因此,将 wchar_t 定义为 char 是合法的,这对嵌入式系统可能有意义。

但在 GNU C 库中,wchar_t 始终为 32 位宽,因此能够表示所有 UCS-4 值,因此涵盖了所有 ISO 10646。一些 Unix 系统将 wchar_t 定义为 16 位类型,因此非常遵循 Unicode严格。这个定义完全符合标准,但这也意味着要表示来自 Unicode 和 ISO 10646 的所有字符,必须使用 UTF-16 代理字符,这实际上是一种多宽字符编码。但是采用多宽字符编码与 wchar_t 类型的目的相矛盾。

数据类型:wint_t

wint_t 是一种用于包含单个宽字符的参数和变量的数据类型。顾名思义,这种类型在使用普通 char 字符串时相当于 int。如果 wchar_t 和 wint_t 类型的大小为 32 位宽,则它们通常具有相同的表示形式,但如果 wchar_t 定义为 char,则由于参数提升,类型 wint_t 必须定义为 int。

此类型在 wchar.h 中定义,并在 ISO C90 修正案 1 中引入。

与 char 数据类型一样,宏可用于指定 wchar_t 类型对象中可表示的最小值和最大值。

宏:wint_t WCHAR_MIN

宏 WCHAR_MIN 求值为 wint_t 类型的对象可表示的最小值。

这个宏是在 ISO C90 的修正案 1 中引入的。

宏:wint_t WCHAR_MAX

宏 WCHAR_MAX 求值为 wint_t 类型的对象可表示的最大值。

这个宏是在 ISO C90 的修正案 1 中引入的。

另一个特殊的宽字符值相当于 EOF。

宏:wint_t WEOF

宏 WEOF 计算为 wint_t 类型的常量表达式,其值不同于扩展字符集的任何成员。

WEOF 不必与 EOF 具有相同的值,并且与 EOF 不同,它也不必为负值。换句话说,草率的代码就像

{
   
   
  int c;while ((c = getc (fp)) < 0)}

当使用宽字符时,必须重写以显式使用 WEOF:

{
   
   
  wint_t c;while ((c = getwc (fp)) != WEOF)}

这个宏是在 ISO C90 修正案 1 中引入的,并在 wchar.h 中定义。

这些内部表示在存储和传输方面存在问题。因为每个单独的宽字符都包含一个以上的字节,所以它们会受到字节顺序的影响。因此,具有不同字节顺序的机器在访问相同数据时会看到不同的值。这种字节顺序问题也适用于所有基于字节的通信协议,因此要求发送者必须决定将宽字符拆分为字节。最后一点(但并非最不重要)是宽字符通常比自定义的面向字节的字符集需要更多的存储空间。

由于上述所有原因,如果后者是 UCS-2 或 UCS-4,则通常使用与内部编码不同的外部编码。外部编码是基于字节的,可以根据环境和要处理的文本适当地选择。这种外部编码可以使用多种不同的字符集(这里不会详尽地介绍信息——相反,主要组的描述就足够了)。所有基于 ASCII 的字符集都满足一个要求:它们是“文件系统安全的”。这意味着字符’/'在编码中仅用于表示它自己。对于像 EBCDIC(Extended Binary Coded Decimal Interchange Code,IBM 使用的字符集系列)这样的字符集,情况有些不同,但是如果操作系统不能直接理解 EBCDIC,那么无论如何都必须首先转换参数到系统的调用.

  • 最简单的字符集是单字节字符集。最多只能有 256 个字符(对于 8 位字符集),这不足以涵盖所有语言,但可能足以处理特定文本。处理 8 位字符集很简单。这不适用于稍后介绍的其他类型,因此,一个使用的应用程序可能需要使用 8 位字符集。

  • ISO 2022 标准定义了一种扩展字符集的机制,其中一个字符可以由多个字节表示。这是通过将状态与文本相关联来实现的。可用于更改状态的字符可以嵌入到文本中。文本中的每个字节在每种状态下都可能有不同的解释。状态甚至可能会影响给定字节是单独代表一个字符,还是必须与更多字节组合。

    在 ISO 2022 的大多数使用中,定义的字符集不允许状态更改超过下一个字符。这具有很大的优势,即只要可以识别字符的字节序列的开头,就可以正确解释文本。使用此策略的字符集示例包括各种 EUC 字符集(由 Sun 的操作系统、EUC-JP、EUC-KR、EUC-TW 和 EUC-CN 使用)或 Shift_JIS(SJIS,一种日语编码)。

    但 也有字符集使用对多个字符有效且必须由另一个字节序列更改的状态。例如 ISO-2022-JP、ISO-2022-KR 和 ISO-2022-CN。

  • 使用罗马字母为其他语言修复 8 位字符集的早期尝试导致了类似 ISO 6937 的字符集。这里表示像尖音符号这样的字符的字节本身不会产生输出:必须将它们与其他字符组合以获得所需的结果.例如,字节序列 0xc2 0x61(无间距的重音符号,后跟小写的‘a’)得到“小 a 带重音符号”。要单独获得尖音符字符,必须写入 0xc2 0x20(非间距尖音符后跟一个空格)。

    诸如 ISO 6937 之类的字符集用于一些嵌入式系统,例如电传。

  • 无需转换内部使用的 Unicode 或 ISO 10646 文本,通常只需使用不同于 UCS-2/UCS-4 的编码即可。Unicode 和 ISO 10646 标准甚至指定了这样的编码:UTF-8。这种编码能够表示长度为 1 到 6 的字节串中的所有 ISO 10646 31 位。

    还有一些其他尝试对 ISO 10646 进行编码,例如 UTF-7,但 UTF-8 是当今唯一应该使用的编码。事实上,幸运的是,UTF-8 很快就会成为唯一必须支持的外部编码。它被证明是普遍可用的,唯一的缺点是它有利于罗马语言,因为如果为这些脚本使用特定的字符集,它会使其他脚本(西里尔文、希腊语、亚洲脚本)的字节字符串表示比必要的更长。Unicode 压缩方案等方法可以缓解这些问题。

剩下的问题是:如何选择要使用的字符集或编码。答案:你不能自己决定,它是由系统的开发者或大多数用户决定的。由于目标是互操作性,因此必须使用其他人使用的任何东西。如果没有限制,则选择基于预期用户圈的要求。换句话说,如果一个项目预计只在俄罗斯使用,那么使用 KOI8-R 或类似的字符集就可以了。但是,如果同时来自希腊的人参与其中,则应该使用允许所有人协作的字符集。

最广泛有用的解决方案似乎是:使用最通用的字符集,即 ISO 10646。使用 UTF-8 作为外部编码,用户无法充分使用自己的语言的问题已成为过去。

在这一点上,关于宽字符表示的选择的最后一条评论是必要的。我们在上面说过,自然的选择是使用 Unicode 或 ISO 10646。这不是 ISO C 标准所要求的,但至少是鼓励的。该标准至少定义了一个宏 __STDC_ISO_10646__,该宏仅在 wchar_t 类型编码 ISO 10646 字符的系统上定义。如果未定义此符号,则应避免对宽字符表示进行假设。如果程序员只使用 C 库提供的函数来处理宽字符串,那么与其他系统的兼容性应该不会有问题。

2.2. 字符处理函数概述

Overview about Character Handling Functions

一个 Unix C 库包含两个系列中的三组不同的函数来处理字符集转换。ISO C90 标准中指定了函数系列之一(最常用的),因此即使在 Unix 世界之外也是可移植的。不幸的是,这个家庭是最没用的。应尽可能避免使用这些功能,尤其是在开发库(而不是应用程序)时。

第二个函数系列是在早期的 Unix 标准 (XPG2) 中引入的,并且仍然是最新和最伟大的 Unix 标准的一部分:Unix 98。它也是最强大和最有用的函数集。但我们将从 ISO C90 修正案 1 中定义的功能开始。

2.3. 可重启的多字节转换函数

Restartable Multibyte Conversion Functions

ISO C 标准定义了将字符串从多字节表示形式转换为宽字符串的函数。有几个特点:

  • 假定用于多字节编码的字符集未指定为函数的参数。而是使用当前语言环境的 LC_CTYPE 类别指定的字符集;请参阅区域设置类别
  • 一次处理多个字符的函数需要以 NUL 结尾的字符串作为参数(即,除非可以在适当的位置添加 NUL 字节,否则无法转换文本块)。GNU C 库包含一些允许指定大小的标准扩展,但基本上它们也期望终止字符串。

尽管有这些限制,但 ISO C 函数可以在许多情况下使用。例如,在图形用户界面中,如果文本不是简单的 ASCII,具有要求文本以宽字符串显示的功能并不少见。文本本身可能来自带有翻译的文件,用户应该决定当前的语言环境,这决定了翻译,因此也决定了使用的外部编码。在这种情况下(以及许多其他情况),这里描述的功能是完美的。如果在执行转换时需要更多自由,请查看 iconv 函数(请参阅通用字符集转换)。

2.3.1. 选择转换及其属性

Selecting the conversion and its properties

我们在上面已经说过,当前为 LC_CTYPE 类别选择的语言环境决定了我们将要描述的函数执行的转换。每个语言环境都使用自己的字符集(作为 localedef 的参数给出),这是假定为外部多字节编码的字符集。GNU C 库中的宽字符集始终是 UCS-4。

每个多字节字符集的一个特征是表示一个字符可能需要的最大字节数。在编写使用转换函数的代码时,此信息非常重要(如下面的示例所示)。ISO C 标准定义了两个提供此信息的宏。

宏:int MB_LEN_MAX

MB_LEN_MAX 为任何受支持的语言环境中的单个字符指定多字节序列中的最大字节数。它是一个编译时常量,在 limits.h 中定义。

宏:int MB_CUR_MAX

MB_CUR_MAX 扩展为一个正整数表达式,它是当前语言环境中多字节字符的最大字节数。该值永远不会大于 MB_LEN_MAX。与 MB_LEN_MAX 不同,此宏不必是编译时常量,而在 GNU C 库中则不是。

MB_CUR_MAX 在 stdlib.h 中定义。

两个不同的宏是必要的,因为严格的 ISO C90 编译器不允许可变长度数组定义,但仍然希望避免动态分配。这段不完整的代码显示了问题:

{
   
   
  char buf[MB_LEN_MAX];
  ssize_t len = 0;

  while (! feof (fp))
    {
   
   
      fread (&buf[len], 1, MB_CUR_MAX - len, fp);
      /* … process buf */
      len -= used;
    }
}

预计内部循环中的代码在数组 buf 中总是有足够的字节来转换一个多字节字符。数组 buf 必须静态调整大小,因为许多编译器不允许可变大小。fread 调用确保 MB_CUR_MAX 字节在 buf 中始终可用。请注意,如果 MB_CUR_MAX 不是编译时常量,这不是问题。

2.3.2. 表示转换的状态

在本章的介绍中,有人说某些字符集使用有状态编码。也就是说,编码值在某种程度上取决于文本中的先前字节。

由于转换函数允许在多个步骤中转换文本,因此我们必须有一种方法可以将此信息从函数的一次调用传递到另一个函数调用。

数据类型:mbstate_t

mbstate_t 类型的变量可以包含有关从一个调用转换函数到另一个调用所需的转换状态的所有信息。

mbstate_t 在 wchar.h 中定义。它是在 ISO C90 的修正案 1 中引入的。

要使用 mbstate_t 类型的对象,程序员必须定义此类对象(通常作为堆栈上的局部变量)并将指向该对象的指针传递给转换函数。这样,如果当前多字节字符集是有状态的,转换函数可以更新对象。

没有特定的函数或初始化程序可以将状态对象置于任何特定状态。规则是对象应始终表示第一次使用之前的初始状态,这是通过使用如下代码清除整个变量来实现的:

{
   
   
  mbstate_t state;
  memset (&state, '\0', sizeof (state));
  /* from now on state can be used.  */}

在使用转换函数生成输出时,通常需要测试当前状态是否对应于初始状态。这是必要的,例如,决定是否发出转义序列以在某些序列点将状态设置为初始状态。通信协议通常需要这样做。

函数:int mbsinit (const mbstate_t *ps)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

mbsinit函数判断ps指向的状态对象是否处于初始状态。如果 ps 是空指针或对象处于初始状态,则返回值非零。否则为零。

mbsinit 在 ISO C90 的修正案 1 中引入,并在 wchar.h 中声明。

使用 mbsinit 的代码通常如下所示:

{
   
   
  mbstate_t state;
  memset (&state, '\0', sizeof (state));
  /* Use state.  */if (! mbsinit (&state))
    {
   
   
      /* Emit code to return to initial state.  */
      const wchar_t empty[] = L"";
      const wchar_t *srcp = empty;
      wcsrtombs (outbuf, &srcp, outbuflen, &state);
    }}

发出转义序列以返回初始状态的代码很有趣。wcsrtombs 函数可用于确定必要的输出代码(请参阅转换多字节和宽字符串)。请注意,对于 GNU C 库,不需要执行从多字节文本到宽字符文本的转换的额外操作,因为宽字符编码不是有状态的。但是在任何标准中都没有提到禁止使 wchar_t 使用有状态编码。

2.3.3. 转换单个字符

Converting Single Characters

最基本的转换函数是那些处理单个字符的函数。请注意,这并不总是意味着单个字节。但是由于多字节字符集的子集通常由单字节序列组成,因此有一些函数可以帮助转换字节。通常,ASCII 是多字节字符集的子集。在这种情况下,每个 ASCII 字符代表自己,所有其他字符至少有一个超出 0 到 127 范围的第一个字节。

函数:wint_t btowc (int c)

Preliminary: | MT-Safe | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.

btowc 函数(“字节到宽字符”)使用当前选择的 LC_CTYPE 类别的语言环境中的转换规则,将处于初始移位状态的有效单字节字符 c 转换为等效的宽字符。

如果 (unsigned char) c 不是有效的单字节多字节字符或 c 是 EOF,则函数返回 WEOF。

请注意仅在初始班次状态下测试 c 有效性的限制。不使用从中获取状态信息的 mbstate_t 对象,并且该函数也不使用任何静态状态。

btowc 函数是在 ISO C90 修正案 1 中引入的,并在 wchar.h 中声明。

尽管单字节值总是在初始状态下被解释的限制,但这个函数在大多数情况下实际上是有用的。大多数字符要么完全是单字节字符集,要么是 ASCII 的扩展。但是这样写代码是可能的(并不是说这个具体的例子很有用):

wchar_t *
itow (unsigned long int val)
{
   
   
  static wchar_t buf[30];
  wchar_t *wcp = &buf[29];
  *wcp = L'\0';
  while (val != 0)
    {
   
   
      *--wcp = btowc ('0' + val % 10);
      val /= 10;
    }
  if (wcp == &buf[29])
    *--wcp = L'0';
  return wcp;
}

为什么需要使用如此复杂的实现而不是简单地将 ‘0’ + val % 10 转换为宽字符?答案是不能保证可以对用于 wchar_t 表示的字符集的字符执行这种算术运算。在其他情况下,字节在编译时不是恒定的,因此编译器无法完成工作。在这种情况下,需要使用 btowc。

还有一个用于在另一个方向进行转换的功能。

函数:int wctob (wint_t c)

Preliminary: | MT-Safe | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.

wctob 函数(“宽字符到字节”)将有效的宽字符作为参数。如果这个字符在初始状态的多字节表示正好是一个字节长,那么这个函数的返回值就是这个字符。否则返回值为 EOF。

wctob 在 ISO C90 的修正案 1 中引入,并在 wchar.h 中声明。

有更通用的函数可以将单个字符从多字节表示转换为宽字符,反之亦然。这些函数对多字节表示的长度没有限制,也不需要它处于初始状态。

函数:size_t mbrtowc (wchar_t *restrict pwc, const char *restrict s, size_t n, mbstate_t *restrict ps)

Preliminary: | MT-Unsafe race:mbrtowc/!ps | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.

mbrtowc 函数(“多字节可重启为宽字符”)将 s 指向的字符串中的下一个多字节字符转换为宽字符,并将其存储在 pwc 指向的位置。根据当前为 LC_CTYPE 类别选择的语言环境执行转换。如果语言环境中使用的字符集的转换需要状态,则多字节字符串以 ps 指向的对象表示的状态进行解释。如果 ps 是空指针,则使用仅由 mbrtowc 函数使用的静态内部状态变量。

如果下一个多字节字符对应于空宽字符,则函数的返回值为 0,然后状态对象处于初始状态。如果接下来的 n 个或更少的字节构成正确的多字节字符,则返回值是从 s 开始的构成多字节字符的字节数。转换状态根据转换中消耗的字节进行更新。在这两种情况下,如果 pwc 不为空,宽字符(L’\0’ 或在转换中找到的字符)都存储在 pwc 指向的字符串中。

如果多字节字符串的前 n 个字节可能构成一个有效的多字节字符,但完成它所需的字节数超过 n 个,则函数的返回值为 (size_t) -2 并且没有值存储在 *pwc 中。转换状态已更新,所有 n 个输入字节都已消耗,不应再次提交。请注意,即使 n 的值大于或等于 MB_CUR_MAX 也会发生这种情况,因为输入可能包含冗余移位序列。

如果多字节字符串的前 n 个字节不可能形成有效的多字节字符,则不存储任何值,将全局变量 errno 设置为值 EILSEQ,并且函数返回 (size_t) -1。转换状态之后是未定义的。

如指定的那样,mbrtowc 函数可以处理包含嵌入空字节的多字节序列(这发生在 Unicode 编码中,例如 UTF-16),但 GNU C 库不支持这种多字节编码。当遇到空输入字节时,该函数将返回零,或返回 (size_t) -1) 并报告 EILSEQ 错误。iconv 函数可用于任意编码之间的转换。请参阅通用字符集转换接口

mbrtowc 在 ISO C90 的修正案 1 中引入,并在 wchar.h 中声明。

将多字节字符串复制为宽字符串同时将所有小写字符转换为大写的函数可能如下所示:

wchar_t *
mbstouwcs (const char *s)
{
   
   
  /* Include the null terminator in the conversion. */
  size_t len = strlen (s) + 1;
  wchar_t *result = reallocarray (NULL, len, sizeof (wchar_t));
  if (result == NULL)
    return NULL;

  wchar_t *wcp = result;
  mbstate_t state;
  memset (&state, '\0', sizeof (state));

  while (true)
    {
   
   
      wchar_t wc;
      size_t nbytes = mbrtowc (&wc, s, len, &state);
      if (nbytes == 0)
        {
   
   
          /* Terminate the result string. */
          *wcp = L'\0';
          break;
        }
      else if (nbytes == (size_t) -2)
        {
   
   
          /* Truncated input string. */
          errno = EILSEQ;
          free (result);
          return NULL;
        }
      else if (nbytes == (size_t) -1)
        {
   
   
          /* Some other error (including EILSEQ). */
          free (result);
          return NULL;
        }
      else
        {
   
   
          /* A character was converted. */
          *wcp++ = towupper (wc);
          len -= nbytes
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

canpool

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值