【问题标题】:Is it safe to call the functions from <cctype> with char arguments?使用 char 参数从 <cctype> 调用函数是否安全?
【发布时间】:2011-10-31 03:44:00
【问题描述】:

C 编程语言说来自&lt;ctype.h&gt; 的函数遵循一个共同的要求:

ISO C99,7.4p1:

在所有情况下,参数都是int,其值应表示为unsigned char,或应等于宏EOF 的值。如果参数有任何其他值,则行为未定义。

这意味着下面的代码是不安全的:

int upper(const char *s, size_t index) {
  return toupper(s[index]);
}

如果此代码在char 具有与signed char 相同的值空间并且字符串中存在具有负值的字符的实现上执行,则此代码将调用未定义的行为。正确的版本是:

int upper(const char *s, size_t index) {
  return toupper((unsigned char) s[index]);
}

尽管如此,我看到许多 C++ 示例并不关心这种未定义行为的可能性。那么C++标准中有什么东西可以保证上面的代码不会导致未定义的行为,还是说所有的例子都是错的?

[附加关键字:ctype cctype isalnum isalpha isblank iscntrl isdigit isgraph islowwer isprint ispunct isspace isupper isxdigit tolower]

【问题讨论】:

  • 你问的是 C++ 但引用了 C99 吗?
  • 哦,C++98 早于 C99。尽管如此,C90 的文本几乎相同,C++98 从 C90 借用其标准库,所以是的,我故意引用 C 标准。
  • char 的符号是编译器特定的。尽管我对此表示怀疑,但也许其中一些“错误”的示例项目会迫使编译器将 char 视为无符号。
  • @cnicutar C++98 标准包含有关 的详细信息。遵循 C++ 标准完全删除了该部分,而是在 [cctype.syn] 中明确声明应参考 ISO C 第 7.4 节的定义,其中 ISO C 是撰写本文时的最新 C 标准(即所有情况下的 C99)。因此,根据过去 20 年的所有 C++ 标准,OP 中的引用是正确的(也是唯一的)引用。这也是 10 年前发布 OP 时的情况。 :)

标签: c++ c character undefined-behavior language-lawyer


【解决方案1】:

不管怎样,Solaris Studio 编译器(使用stlport4)就是这样一种编译器套件,在这里会产生意想不到的结果。编译并运行:

#include <stdio.h>
#include <cctype>

int main() {
    char ch = '\xa1'; // '¡' in latin-1 locales + UTF-8
    printf("is whitespace: %i\n", std::isspace(ch));
    return 0;
}

给我:

kevin@solaris:~/scratch
$ CC -library=stlport4 whitespace.cpp && ./a.out 
is whitespace: 8

供参考:

$ CC -V
CC: Studio 12.5 Sun C++ 5.14 SunOS_i386 2016/05/31

当然,这种行为在 C++ 标准中有所记载,但绝对令人惊讶。


编辑:由于有人指出,由于整数溢出,上述版本在尝试分配 char ch = '\xa1' 时包含未定义的行为,所以这里有一个版本可以避免这种情况并仍然保留相同的输出:

#include <stdio.h>
#include <cctype>

int main() {
    char ch = -95;
    printf("is whitespace: %i\n", std::isspace(ch));
    return 0;
}

在我的 Solaris VM 上仍然打印 8:

kevin@solaris:~/scratch
$ CC -library=stlport4 whitespace.cpp && ./a.out 
is whitespace: 8

编辑 2:这里的程序可能看起来很正常,但由于 UB 在使用 std::isspace() 时会产生意想不到的结果:

#include <cstdio>
#include <cstring>
#include <cctype>

static int count_whitespace(const char* str, int n) {
    int count = 0;
    for (int i = 0; i < n; i++)
        if (std::isspace(str[i]))  // oops!
            count += 1;
    return count;
}

int main() {
    const char* batman = "I am batman\xa1";
    int n = std::strlen(batman);
    std::printf("%i\n", count_whitespace(batman, n));
    return 0;
}

而且,在我的 Solaris 机器上:

kevin@solaris:~/scratch
$ CC whitespace.cpp && ./a.out
3

请注意,根据您如何置换此程序,您可能会得到两个空白字符的预期结果;也就是说,几乎可以肯定会有一些编译器优化利用这个 UB 更快地给你错误的结果。

例如,如果您尝试通过在字符串中搜索(非多字节)空白字符来标记 UTF-8 字符串,您可以想象这会咬到您的脸。这样的程序在将str[i] 转换为unsigned char 时会正确运行。

【讨论】:

  • char 具有实现定义的签名。如果它已签名并且您将大于 127 的值存储到其中,例如 0xA1,则会调用整数溢出,这是未定义的行为。这个 UB 错误发生在您调用 ctype.h 函数之前。因此,除了溢出有符号整数是不安全的之外,这个答案什么也证明不了。那里没有消息。更改为 unsigned char 就可以了。
  • 编辑后,当您将 -95 设置为有符号字符时,它会在传递给 isspace 时转换为 int。你得到了从charint 的左值转换。标志被保留。所以这段代码传递给函数的是0xFFFFFFA1。该值不能表示为 unsigned char 或 EOF,因此代码调用 UB。同样,问题不在于 ctype.h 函数,而在于程序员使用char 类型来存储整数。为什么你会在现实世界的程序中将 -95 传递给 ctype 函数,我不知道。如果你做了很奇怪的事情,那么奇怪的事情就会发生。
  • OP 没有询问这是否是未定义的行为(我们知道它是)。 OP 询问是否存在在调用此未定义行为时会给您带来令人惊讶的结果的实现。 Solaris Studio 编译器恰好是这样的编译器套件,它会给您带来令人惊讶的结果,而我认为其他编译器(gccclang)可能会做的很好,即使它是 UB,也会给您一个理智的结果。
  • @Lundin 在char 对象中存储任意值不会调用未定义的行为,请参阅 n1570.pdf 中的 6.2.5p3。
  • @RolandIllig 我已经在之前的评论中回答了这个问题。您不能存储不适合的值,句号。
【解决方案2】:

有时大多数人都错了。我想就是这样。话虽如此,没有什么可以阻止标准库实现者定义大多数人期望的行为。所以也许这就是大多数人不关心的原因,因为他们实际上从未见过由这个错误导致的错误。

【讨论】:

  • 他的修正版如何正确?将负值转换为 unsigned char 不会产生任何有意义的东西!
  • @john:从intchar 的转换不是UB,它只是在源值不在可表示为char 的范围内时产生一个实现定义的值。
  • 当然,从带有负值的char 转换为unsigned char 的任何实现都不会产生代表与原始char 相同字符的unsigned char 值显然是疯了。
  • @glgl 如果我们谈论跨平台程序,它仅存储为 -28 用于二进制补码表示。为了便于移植,你需要说*(unsigned char*)&amp;s[index],这样无论负值是什么,你最终都会得到 228,仅基于 bitpattern。
  • 任何使用纯 char 签名而不是二进制补码的实现都是无稽之谈。存在很多问题,例如“负零”存在但不是字符串终止符,以及将字节复制为 char 的数组成为有损操作。规范在这些问题上相当模糊,因此进行这样的实现是非常不明智的。
【解决方案3】:

char 类型背后的历史是它最初是用于描述 7 位 ASCII 字符的类型。同时,C 缺乏单独的 8 位整数类型。所以在八十年代的准标准时代,一些编译器使char 无符号 - 因为在符号表中使用负索引没有意义,而其他编译器使char 有符号,以使其与所有其他整数类型。

到了标准化 C 的时候,两个版本都存在。不幸的是,委员会决定让它保持这种状态,将决定权留给编译器。相反,他们添加了另外两种类型:signed charunsigned charsigned char 是有符号整数类型的一部分,unsigned char 是无符号整数类型的一部分,char 两者都不是,尽管它必须具有与 signed charunsigned char 相同的表示形式。 (这在 C11 6.2.5 中都有描述)

值得注意的是,char 在所有已知实现中都只有 8 位,除了一些使用 16 位字节的奇异 DSP。当使用“扩展”符号表时,要么实现从 7 位字符更改为 8 位字符,要么使用 wchar_t。请注意,wchar_t 从一开始就在 C 语言中,因此假设 char 在某些时候用于 UTF8 之类的东西可能是不正确的(尽管理论上是可能的)。

现在,如果 char 已签名,并且您在其中存储了一个大于 CHAR_MAX 或小于 CHAR_MIN 的值,则按照 C11 6.5 §5 调用未定义的行为。时期。因此,如果您有一个 char 数组并且其中的任何项目都违反了类型边界,那么您已经存在未定义的行为。尽管字符类型必须捕获表示,但未定义的行为可能会导致代码在其他方面出现错误行为,例如不正确的优化。

ctype.h 函数允许 EOF 作为参数,但在其他方面应该表现得好像使用字符类型一样,即使参数是 int 以允许 EOF7.4 §1 中的文字主要是说“如果你将一些随机的int 传递给这个函数,它既不是与 char 相同的表示形式,也不是 EOF,那么行为是未定义的”

但是如果你传递一个char,你已经调用了有符号整数上溢/下溢,你甚至在调用函数之前就已经有未定义的行为——这与 ctype.h 函数或任何其他函数无关。因此,您对发布的“上层”函数不安全的假设是不正确的 - 此代码与使用 char 类型的任何其他代码没有什么不同。

由 7.4 中引用的 ctype.h 限制导致的未定义行为示例宁可类似于 toupper(666)

【讨论】:

  • C11 6.2.5p3 似乎允许在任一字符类型的对象中保存任意“字符”,这与 6.5p5 的未定义行为相矛盾。
  • 在您对 7.4p1 的引用中,它应该是“unsigned char”而不是“char”。在CHAR_BIT 为8 且char 已签名的平台上,调用toupper(192) 不会调用未定义的行为。 (您的报价另有说明。)
  • @RolandIllig 否,6.2.5p3 指向 基本执行字符集,这是 5.2.1 中定义的正式术语。粗略表示大写和小写的英文字母,加上数字和标点符号等。这又大致表示 7 位 ASCII - 无论如何,基本执行字符集可以容纳 7 位。除此之外,它说其他字符可以以实现定义的方式存储“但应在该类型可以表示的值范围内”。这意味着您仍然不允许上溢/下溢。
  • @RolandIllig 关于 7.4,我没有引用它。是的,toupper(192) 很好,我没有说别的。但是char ch=192; 可能是未定义的行为,无论您是否将 char 传递给函数。这个答案的重点是指出这里只有两个问题:1)溢出/下溢char或任何其他有符号整数是UB。 2) 将一些随机垃圾值传递给 ctype.h 函数是 UB。您问题中的代码两者都没有,因此非常安全。
  • 在您的回答中,您写道:“现在,如果 char 已签名,并且您在其中存储一个大于 CHAR_MAX 或小于 CHAR_MIN 的值,您将调用未定义的行为,按照C11 6.5 §5. 期间。” ——我不同意这种说法。在这种情况下,标准的相关部分是§6.3.1.3 ¶3,它声明结果是实现定义的或引发了实现定义的信号。在您对其他答案之一的评论中,您自己指出了这一点,但之后您似乎没有更新自己的答案。
猜你喜欢
  • 2013-05-15
  • 2020-12-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-02-03
  • 1970-01-01
  • 2020-04-02
相关资源
最近更新 更多