"strlen(s1) - strlen(s2)" 永远不会小于零

78

我目前正在编写一个需要经常比较字符串长度的C程序,因此我编写了以下辅助函数:

int strlonger(char *s1, char *s2) {
    return strlen(s1) - strlen(s2) > 0;
}

我注意到即使s1的长度比s2短,该函数仍然返回true。有人可以解释一下这个奇怪的行为吗?


27
这是一种Fortran-66式的写法,表示“返回s1的长度是否大于s2的长度”。 - Jonathan Leffler
11
@TimThomas:为什么你要在这个问题上设置悬赏?你说它没有得到足够的关注,但是看起来你对Alex Lockwood的回答非常满意。不确定还需要什么才能赢得悬赏奖励! :) - eggyal
11
这是一次意外,我不知道什么是赏金哈哈。-_- 有点尴尬... - Adrian Monk
5
我想对Alex Lockwood来说这是个好消息,因为他的出色回答会得到更多关注……所以大家请给Alex Lockwood的答案点赞!! :D - Adrian Monk
5
我认为@TimThomas最好将悬赏保持开放直到最后一个允许日期,这样他的问题也能得到一些关注。他不知情地失去了100点声望,让他有机会重新获得一些声望。 - Krishnabhadra
显示剩余3条评论
3个回答

177
你遇到的是在C语言中处理同时包含有符号和无符号量表达式时出现的奇特行为。
当执行一个操作时,其中一个操作数是有符号数而另一个是无符号数,C语言会将有符号参数隐式转换成无符号数,并假定这些数都是非负数。这种约定通常会导致关系运算符(如<>)出现非直观的行为。
关于你的辅助函数,请注意strlen返回size_t类型(一个无符号数),因此差值和比较都使用无符号算术进行计算。当s1短于s2时,差值strlen(s1) - strlen(s2)应该是负数,但实际上却变成了一个很大的无符号数,它大于0。因此,
return strlen(s1) - strlen(s2) > 0;

即使s1s2短,也会返回1。为修复您的函数,请改用以下代码:

return strlen(s1) > strlen(s2);

欢迎来到神奇的 C 世界! :)


其他示例

由于这个问题最近受到了很多关注,我想提供一些(简单的)示例,以确保我传达的思想正确。我将假设我们使用的是32位机器,采用二进制补码表示。

在 C 中处理无符号/有符号变量时需要理解的重要概念是:如果单个表达式中混合了无符号和有符号量,则有符号值会被隐式转换为无符号值

示例 #1:

考虑以下表达式:

-1 < 0U

由于第二个操作数是无符号的,因此第一个操作数被隐式转换为无符号数,因此表达式等同于以下比较:

4294967295U < 0U

当然,这是错误的。这可能不是您所期望的行为。

示例2:

考虑以下代码,尝试对数组 a 中的元素求和,数组长度由参数length给出:

int sum_array_elements(int a[], unsigned length) {
    int i;
    int result = 0;

    for (i = 0; i <= length-1; i++) 
        result += a[i];

    return result;
}

该函数旨在展示由于从有符号转换为无符号的隐式转换而导致bug产生的容易程度。把参数length传递为无符号似乎很自然;毕竟,谁会想要使用负长度呢?停止条件i <= length-1也似乎非常直观。然而,当使用参数length等于0运行时,这两个组合会产生意外结果。
由于参数length是无符号的,计算0-1将使用无符号算术进行,这等效于模加法。然后得到的结果是UMax<=比较也使用无符号比较执行,由于任何数字都小于或等于UMax,因此比较始终成立。因此,代码将尝试访问数组a的无效元素。
代码可以通过将length声明为int或将for循环的测试更改为i < length来修复。
结论:何时应该使用无符号?
我不想在这里陈述任何太具争议性的事情,但以下是我在用C编写程序时经常遵循的一些规则。
  • 不要仅因为数字是非负数而使用。容易犯错,这些错误有时候非常微妙(如示例#2所示)。

  • 在进行模算术时应该使用。

  • 在使用位表示集合时应该使用。这通常很方便,因为它允许您执行逻辑右移而不会出现符号扩展。

当然,可能会有一些情况下你决定违背这些"规则"。但是大多数情况下,遵循这些建议将使您的代码更易于使用,更少出现错误。

47
写得更少,使程序更正确,这是又一个很好的例子。 - Kerrek SB
3
必须进行类型转换,但是将无符号类型转换为有符号类型会丢失信息,也就是丧失一半的取值范围。 - user207421
7
严格地讲,这里的减法是在两个 size_t 值之间进行的,这些值是保证为无符号数的,并且无符号算术会对适当的二的幂进行模运算。唯一可能存在有符号/无符号转换的地方是在 result > 0 部分,在此部分中,result 是从两个大小值相减得出的 size_t 值。 - Jonathan Leffler
9
它不是“cast”,而是“convert”。术语“cast”仅指显式转换运算符,由括在圆括号中的类型名称组成。 转换运算符明确指定了一种转换方式;转换可以是显式或隐式的。 - Keith Thompson
2
我发现在我的代码中负整数非常罕见,因此我采取相反的方法,除非有具体的理由,否则我使用“unsigned int”。这样做的好处是所有操作都是明确定义的(甚至是“环绕”),尽管必须小心处理某些不等式。 - Joshua Green
显示剩余8条评论

25

strlen 返回一个 size_t 类型的值,它是一个 unsigned 类型的别名。

因此,

(unsigned) 4 - (unsigned) 7 == (unsigned) - 3

所有的unsigned值都大于或等于0。尝试将strlen返回的变量转换为long int


ptrdiff_t是正确的可移植转换。在64位系统上,long int通常是32位有符号整数(在64位系统上,指针是64位)。事实上,Visual C++和x86、x86_64的gcc都使用32位长整型。 - Mr Fooz
3
我认为 ptrdiff_t 是用于指针相减,而不是 size_t 值相减... - Mr Lister
4
"subtraction of size_t values" 没有 POSIX 类型;C 将其定义为 size_t,因为它是整数类型且类型匹配。你可以认为这是 off_t,但实际上它是用于文件偏移量的。因此,你只能得出这样的结论,即由于 size_t 需要保存平台所能处理的任何索引,因此它也可以表示任何指针值,因为它可以用于从 0 索引字节。因此,ptrdiff_t 需要与 size_t 具有相同的位数,使其成为 signed 版本的 size_t - Mike DeSimone

1

Alex Lockwood的答案是最佳解决方案(紧凑,清晰的语义等)。

有时明确转换为带符号的size_t形式: ptrdiff_t是有意义的,例如。

return ptrdiff_t(strlen(s1)) - ptrdiff_t(strlen(s2)) > 0;

如果您这样做,您需要确保 size_t 值适合于 ptrdiff_t 中(它少了一个尾数位)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接