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

Is it safe to call the functions from <cctype> with char arguments?

本文关键字:lt gt cctype 安全 参数 char 调用 函数 是否 使用      更新时间:2023-10-16

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

ISO C99, 7.4p1:

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

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

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

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

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

然而,我在c++中看到许多不关心这种未定义行为的可能性的例子。那么c++标准中是否有任何东西可以保证上面的代码不会导致未定义的行为,或者所有的例子都是错误的?

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

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

#include <stdio.h>
#include <cctype>
int main() {
    char ch = 'xa1'; // '¡' in latin-1 locales + UTF-8
    printf("is whitespace: %in", 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: %in", std::isspace(ch));
    return 0;
}

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

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

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

#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 batmanxa1";
    int n = std::strlen(batman);
    std::printf("%in", count_whitespace(batman, n));
    return 0;
}

并且,在我的Solaris机器上:

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

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

如果您试图通过在字符串中搜索(非多字节)空白字符来标记一个UTF-8字符串,您可以想象这会让您难堪。当将str[i]转换为unsigned char时,这样的程序会正常运行。

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

char类型背后的历史是,它最初是用于描述7位ASCII字符的类型。同时,C语言缺少一个单独的8位整数类型。因此,在80年代标准之前的日子里,一些编译器将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以允许EOF 7.4§1的文本主要是说"如果你传递一些随机的int给这个函数,它既不是一个char,也不是EOF,行为是未定义的"

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

7.4中引用的ctype.h限制导致的未定义行为的一个例子是toupper(666)