使用char参数从<cctype>调用函数是否安全?</cctype>

时间:2011-08-20 09:53:28

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

C编程语言表示<ctype.h>中的函数遵循一个共同的要求:

ISO C99,7.4p1:

  

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

这意味着以下代码不安全:

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

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

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]

3 个答案:

答案 0 :(得分:1)

对于它的价值,Solaris Studio编译器(使用items(id))就是一个这样的编译器套件,它会在这里产生意想不到的结果。编译并运行:

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

当然,这种行为与C ++标准中记载的一样,但它确实令人惊讶。

编辑:因为有人指出上述版本在尝试由于整数溢出而分配$ CC -V CC: Studio 12.5 Sun C++ 5.14 SunOS_i386 2016/05/31 时包含未定义的行为,这里有一个版本可以避免这种情况并且仍然保留相同的输出:

char ch = '\xa1'

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

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

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

编辑2:这是一个可能看起来很健全的程序,但由于UB使用kevin@solaris:~/scratch $ CC -library=stlport4 whitespace.cpp && ./a.out is whitespace: 8 而给出了意想不到的结果:

std::isspace()

并且,在我的Solaris机器上:

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

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

例如,如果你试图通过在字符串中搜索(非多字节)空白字符来尝试标记UTF-8字符串,那么你可以想象这会咬你。在将kevin@solaris:~/scratch $ CC whitespace.cpp && ./a.out 3 投射到str[i]时,此类程序会正常运行。

答案 1 :(得分:0)

有时大多数人都错了。我认为就是这样。话虽如此,没有什么可以阻止标准库实现者定义大多数人期望的行为。所以也许这就是大多数人不关心的原因,因为他们从来没有真正看到过这个错误造成的错误。

答案 2 :(得分:0)

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

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

值得注意的是,unsigned char在所有已知的实现中从来都不是8位,除了一些使用16位字节的奇怪的奇怪DSP。使用“扩展”符号表时,实现从7位字符更改为8位字符,或使用char。请注意wchar_t从一开始就使用C语言,所以假设wchar_t在某些时候用于UTF8这样的事情可能是不正确的(虽然理论上可行)。

现在,如果char已签名,并且您在其中存储的值大于char或小于CHAR_MAX,则根据C116.5§5调用未定义的行为。期。因此,如果您有一个CHAR_MIN数组,并且其中的任何项都违反了类型边界,那么您已经有了未定义的行为。即使字符类型必须捕获表示,未定义的行为也可能导致代码以其他方式行为不端,例如不正确的优化。

ctype.h函数允许char作为参数,但除非参数为EOF以允许int,否则其行为应该与使用字符类型一样。 7.4§1中的文本主要是说“如果你将一些随机EOF传递给这个函数,它既不是char,也不是EOF,那么行为是未定义的”

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

7.4中引用的ctype.h限制引起的未定义行为的示例更像是char