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]
答案 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 char
和unsigned char
。 signed 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
。