你如何应对签名的char - > int标准库的问题?

时间:2011-11-10 16:42:50

标签: c++ c character-encoding special-characters

这是我工作中一个非常长期存在的问题,我发现我没有一个好的解决方案......

天真地定义了int的所有字符测试函数:

int isspace(int ch);

但是char经常被签名,并且一个完整的角色通常不适合int,或任何用于字符串******的单个存储单元。

这些函数已成为当前C ++函数和方法的逻辑模板,并为当前的标准库奠定了基础。事实上,他们仍然得到了支持。

因此,如果您使用isspace(* pchar),最终可能会出现符号扩展问题。他们很难看到,因此根据我的经验他们很难防范。

同样地,因为isspace()和它的所有类型都是内容,并且因为字符串的实际宽度通常是未知的,而不是字符串分析 - 这意味着任何现代字符库本质上都不应该使用char或wchar_t,但是只有指针/迭代器,因为只有通过分析字符流才能知道它有多少组成一个逻辑字符,我对如何最好地解决这些问题感到有些不知所措?

我一直期待一个真正强大的库,它基于抽象出任何字符的大小因素,并且只使用字符串(提供诸如isspace之类的东西等),但要么我错过了,要么就是另一个更简单的解决方案盯着我,所有人(谁知道你在做什么)都使用......


**这些问题不适用于可以完全包含完整字符的固定大小的字符编码 - UTF-32显然是唯一具有这些特征的选项(或将自己局限于ASCII或一些这样的)。


所以,我的问题是:

“你如何以不受两个问题影响的方式测试空白,可打印等等:

1)签名扩展,和 2)可变宽度字符问题

毕竟,大多数字符编码是可变宽度:UTF-7,UTF-8,UTF-16,以及Shift-JIS等旧标准。如果编译器将char视为带符号的8位单元,即使扩展ASCII也会出现简单的符号扩展问题。

请注意:

无论char_type的大小是多少,大多数字符编码方案都是错误的。

此问题出现在标准C库以及C ++标准库中;仍尝试传递char和wchar_t,而不是各种isspace,isprint等实现中的字符串迭代器。

实际上,正是这些类型的函数破坏了std :: string的通用性。如果它只在存储单元中工作,并且没有试图假装将存储单元的含义理解为逻辑字符(例如isspace),那么抽象将更加诚实,并且会迫使程序员看起来其他地方有效的解决方案...

谢谢

参与的每个人。在这次讨论和WChars, Encodings, Standards and Portability之间,我对这些问题有了更好的处理。虽然没有简单的答案,但每一点理解都有帮助。

8 个答案:

答案 0 :(得分:10)

  

如何以不受两个问题影响的方式测试空白,可打印等等:
  1)标志扩展
  2)可变宽度字符问题
  毕竟,所有常用的Unicode编码都是可变宽度的,程序员是否意识到:UTF-7,UTF-8,UTF-16,以及Shift-JIS等旧标准......

显然,您必须使用支持Unicode的库,因为您已经(正确地)证明了C ++ 03标准库不是。 C ++ 11库已得到改进,但对于大多数用法来说仍然不够好。是的,有些OS'有一个32位的wchar_t,这使得它们能够正确处理UTF32,但这是一个实现,并不是C ++所保证的,并且对于许多unicode任务来说远远不够,例如迭代Graphemes(字母) 。

IBMICU
Libiconv
microUTF-8
UTF-8 CPP, version 1.0
utfproc
还有更多http://unicode.org/resources/libraries.html

如果问题不是关于特定的字符测试,而是更多关于代码实践的问题:做你的框架做的任何事情。如果您正在编写linux / QT /网络编码,请将所有内容保存在UTF-8中。如果您使用Windows进行编码,请将所有内容保存在UTF-16中。如果您需要弄乱代码点,请将所有内容保存在UTF-32中。否则(对于便携式通用代码),做任何你想做的事情,因为无论如何,你必须翻译一些操作系统或其他。

答案 1 :(得分:7)

我认为你混淆了许多不相关的概念。

首先,char只是一种数据类型。它的第一个也是最重要的含义是“系统的基本存储单元”,即“一个字节”。其签名有意留给实现,以便每个实现可以选择最合适的(即硬件支持的)版本。它的名字,暗示“字符”,很可能是C编程语言设计中最糟糕的决定。

下一个概念是文本字符串。在基础上,文本是一系列单元,通常称为“字符”,但它可能比这更复杂。为此,Unicode标准将术语“代码点”硬币化以指定最基本的文本单元。就目前而言,对于我们程序员来说,“text”是一系列代码点。

问题是代码点多于可能的字节值。可以用两种不同的方式克服这个问题:1)使用多字节编码将代码点序列表示为字节序列;或2)使用不同的基本数据类型。 C和C ++实际上提供了两个解决方案:本机主机接口(命令行参数,文件内容,环境变量)作为 byte 序列提供;但该语言还为“系统的字符集”提供了不透明的类型wchar_t,以及它们之间的翻译功能(mbstowcs / wcstombs)。

不幸的是,没有任何关于“系统的字符集”和“系统多字节编码”的具体内容,所以你和你之前的许多SO用户一样,对于如何处理那些神秘的宽字符感到困惑。现在人们想要的是一种明确的编码,它们可以跨平台共享。我们为此目的唯一有用的编码是 Unicode ,它为大量代码点赋予文本含义(目前最多2 21 ) 。除了文本编码外,还有一系列字节串编码,UTF-8,UTF-16和UTF-32。

检查给定文本字符串的内容的第一步是将它从您拥有的任何输入转换为一个明确的(Unicode)编码字符串。这个Unicode字符串本身可以用任何转换格式编码,但最简单的就是一系列原始代码点(通常是UTF-32,因为我们没有有用的21位数据类型)。

执行此转换已经超出了C ++标准(甚至是新标准)的范围,因此我们需要一个库来执行此操作。由于我们对“系统的字符集”一无所知,我们还需要库来处理它。

一个受欢迎的热门图书馆是iconv();典型的序列从输入多字节char*mbstowcs()std::wstringwchar_t*宽字符串,然后通过iconv()的WCHAR_T到UTF32转换到std::u32stringuint32_t*原始Unicode代码点序列。

此时我们的旅程结束了。我们现在可以通过代码点检查文本代码点(这可能足以判断某些东西是否是空格);或者我们可以调用较重的文本处理库来对我们的Unicode码点流执行复杂的文本操作(例如规范化,规范化,表示转换等)。这远远超出了通用程序员和文本处理专家的范围。

答案 2 :(得分:5)

将EOF以外的负值传递给isspace和其他字符宏无论如何都是无效的。如果您有char c,并且想要测试它是否为空格,请执行isspace((unsigned char)c)。这涉及扩展(通过零扩展)。 isspace(*pchar)是错误的 - 不要写它,当你看到它时不要让它站起来。如果你在看到它时训练自己恐慌,那就不那么难看了。

fgetc(例如)已经返回EOF或读取为unsigned char的字符,然后转换为int,因此对于此值没有符号扩展问题。< / p>

但实际上这是琐事,因为标准字符宏不包括Unicode或多字节编码。如果要正确处理Unicode,则需要Unicode库。我没有看过C ++ 11或C1X在这方面提供的内容,除了C ++ 11的std::u32string听起来很有希望。在此之前,答案是使用特定于实现或第三方的东西。 (联合国)幸运的是有很多库可供选择。

可能(我推测)“完整”的Unicode分类数据库是如此之大,因此可能会发生变化,以至于C ++标准无论如何都要求“完全”支持是不切实际的。它在某种程度上取决于应该支持哪些操作,但是你无法摆脱Unicode在20年内(自第一个标准版本以来)已经通过6个主要版本的问题,而C ++在13年中有2个主要版本。就C ++而言,Unicode字符集是一个快速移动的目标,因此它始终是实现定义的系统知道的代码点。

通常,有三种正确的方法可以处理Unicode文本:

  1. 在所有I / O(包括返回或接受字符串的系统调用)中,在外部使用的字符编码和内部固定宽度编码之间转换所有内容。您可以将此视为输入上的“反序列化”和输出上的“序列化”。如果您有一些具有将其转换为字节流或从字节流转换的函数的对象类型,那么您不会将字节流与对象混淆,或者检查字节流的各个部分以查找您认为可识别的序列化数据的片段。对于这个内部unicode字符串类,它不需要有任何不同。请注意,类不能std::string,也可能不是std::wstring,具体取决于实现。只是假装标准库不提供字符串,如果它有帮助,或者使用std::basic_string大的东西作为容器,但使用Unicode感知库来做任何复杂的事情。您可能还需要了解Unicode规范化,处理组合标记等,因为即使在固定宽度的Unicode编码中,每个字形可能有多个代码点。

  2. 混淆了一些字节序列和Unicode序列的混合,仔细跟踪哪个是哪个。它就像(1),但通常更难,因此虽然它可能是正确的,但在实践中它可能很容易出错。

  3. (仅限特殊用途):将UTF-8用于所有内容。有时这很好,例如,如果你所做的只是基于ASCII标点符号解析输入,并连接输出的字符串。基本上它适用于那些你不需要用顶部位设置理解任何东西的程序,只是不加改变地传递它。如果您需要实际渲染文本,或者以其他方式执行人类认为“明显”但实际上很复杂的事情,它就不能很好地工作。像整理一样。

答案 3 :(得分:3)

预先发表评论:像isspace这样的旧C函数为int 原因是:他们也支持EOF作为输入,因此他们需要能够 支持比char更多的值。该 “天真”的决定允许char签署 - 但是 使它未签名会对a产生严重的性能影响 PDP-11。

现在回答你的问题:

1)签署扩展

C ++函数没有这个问题。在C ++中, “正确”的测试方式,如角色是否是 一个空间是从你想要的任何地方抓取std::ctype方面, 并使用它。当然,<locale>中的C ++本地化具有 经过精心设计,使其尽可能使用,但如果 你正在进行任何重要的文本处理,你很快就会想到 您自己的便利包装器:一个采用区域设置的功能对象 和掩码指定要测试的特性并不难。 使其成为掩码上的模板,并赋予其locale参数a 默认为全局语言环境也不是火箭科学。投入一个 很少有typedef,你可以将IsSpace()之类的内容传递给std::find。 唯一的缺点是管理std::ctype对象的生命周期 你正在处理。但是,以下内容应该有效:

template<std::ctype_base::mask mask>
class Is  //  Must find a better name.
{
    std::locale myLocale;
            //< Needed to ensure no premature destruction of facet
    std::ctype<char> const* myCType;
public:
    Is( std::locale const& l = std::locale() )
        : myLocale( l )
        , myCType( std::use_facet<std::ctype<char> >( l ) )
    {
    }
    bool operator()( char ch ) const
    {
        return myCType->is( mask, ch );
    }
};

typedef Is<std::ctype_base::space> IsSpace;
//  ...

(考虑到STL的影响,有点令人惊讶的是 标准没有将上述内容定义为标准。)

2)可变宽度字符问题。

没有真正的答案。这一切都取决于你需要什么。对于一些 应用程序,只需查找几个特定的​​单字节字符即可 足够,并保持所有UTF-8,并忽略多字节 问题,是一个可行(和简单)的解决方案。除此之外,它经常发生 有用的转换为UTF-32(或取决于您的文本类型 处理,UTF-16),并将每个元素用作单个代码点。对于 全文处理,另一方面,你必须处理 即使您使用的是UTF-32,也会出现多码点字符:序列 \u006D\u0302是一个单个字符(一个小的m,有一个旋律过度 它)。

答案 4 :(得分:0)

我没有如此多地测试Qt库的国际化功能,但据我所知,QString完全支持unicode,并使用的是unicode-chars的QChar。我不知道那些内部实现,但我希望这意味着QChar是可变大小的字符。

将自己绑定到像Qt这样的大框架只是为了使用字符串会很奇怪。

答案 5 :(得分:0)

您似乎将7位ascii上定义的函数与通用空间识别功能混淆。标准C中的字符函数使用int来处理不同的编码,但允许EOF成为带外指示符。符号扩展没有问题,因为定义这些函数的数字没有第8位。提供这种可能性的字节是你的错误。

Plan 9尝试使用UTF库解决此问题,并假设所有输入数据都是UTF-8。这允许一些与ASCII的向后兼容性的度量,因此不兼容的程序不会全部死亡,但允许正确编写新程序。

C中的常见概念,即使仍然是char*代表一个字母数组。它应该被视为输入数据块。要获取此流中的字母,请使用chartorune()。每个Rune都是一个字母(/符号/代码点)的表示,因此最终可以定义一个函数isspacerune(),它最终会告诉你哪些字母是空格。

使用Rune数组,就像使用char数组一样,进行字符串操作,然后在写出之前调用runetochar()将字母重新编码为UTF-8

答案 6 :(得分:0)

你的序言论点有些不妥,可以说是不公平的,它只是在图书馆设计中不支持Unicode编码 - 当然不是多种Unicode编码。

开发C和C ++语言以及大多数库都是在Unicode开发之前。同样,作为系统级语言,它们需要与执行环境的最小可寻址字大小相对应的数据类型。不幸的是,char类型可能已经过载以表示执行环境的字符集和最小可寻址字。历史已经证明这可能是有缺陷的,但改变语言定义,实际上库会破坏大量的遗留代码,所以这些东西留给了新的语言,比如C#,它有8位{{1和} byte类型。

此外,Unicode表示的变量编码使其不适合内置数据类型。您显然已经意识到这一点,因为您建议应对字符串而不是机器字类型执行Unicode字符操作。这需要库支持,正如您所指出的那样,标准库不提供。这有很多原因,但主要是它不在标准库的范围内,就像没有标准库支持网络或图形一样。该库本质上不涉及从深度嵌入到超级计算机的所有目标平台通常不普遍支持的任何内容。所有这些事情都必须由系统或第三方库提供。

支持多种字符编码是关于系统/环境的互操作性,并且该库也不支持它。不兼容的编码系统之间的数据交换是一个应用程序问题,而不是系统问题。

  

“你如何以某种方式测试空白,可打印等等   没有两个问题:

     

1)签名扩展,

     

2)可变宽度字符问题

isspace()仅考虑较低的8位。它的定义明确指出,如果传递的参数不是可表示为unsigned char或等于宏EOF 的值,则结果是未定义的。如果按照预期使用它,则不会出现问题。问题在于它似乎不适合您应用它的目的。

  

毕竟,所有常用的Unicode编码都是可变宽度的,   程序员是否意识到:UTF-7,UTF-8,UTF-16   作为旧标准,如Shift-JIS

没有为Unicode定义isspace()。您需要一个旨在使用您正在使用的任何特定编码的库。这个问题What is the best Unicode library for C?可能是相关的。

答案 7 :(得分:0)

标志扩展问题很容易处理。您可以使用:

  • isspace((unsigned char) ch)
  • isspace(ch & 0xFF)
  • 使char成为无符号类型的编译器选项

就可变长度字符问题而言(我假设为UTF-8),这取决于您的需求。

如果您只是处理ASCII空白字符\t\n\v\f\r,那么isspace将正常工作;非ASCII UTF-8代码单元将被简单地视为非空格。

但是如果你需要识别额外的Unicode空格字符\x85\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000,那就更多了。你可以按照

的方式编写一个函数
bool isspace_utf8(const char* pChar)
{
    uint32_t codePoint = decode_char(*pChar);
    return is_unicode_space(codePoint);
}

其中decode_char将UTF-8序列转换为相应的Unicode代码点,is_unicode_space对于类别为Z的字符或Cc字符返回true空间。 iswspace可能会或可能不会帮助后者,具体取决于您的C ++库支持Unicode的程度。最好使用专用的Unicode库来完成这项工作。

  

实际上大多数字符串都使用多字节编码,例如UTF-7,   UTF-8,UTF-16,SHIFT-JIS等

没有程序员会使用UTF-7或Shift-JIS作为内部表示,除非他们喜欢痛苦。坚持使用ŬTF-8,-16或-32,并且只能根据需要进行转换。