返回 char 字符串指针之间的差异时,强制转换和取消引用的顺序有多重要?

When returning the difference between pointers of char strings, how important is the order of casting and dereferencing?

出于教育目的(是 42 是)我正在重写 strncmp,一位同学刚来问我为什么要以这种方式转换我的返回值。我的建议是先进行类型转换,然后再取消引用。我的逻辑是我想将 char 字符串视为 unsigned char 字符串并取消引用它。

int strncmp(const char *s1, const char *s2, size_t n)
{
    if (n == 0)
        return (0);
    while (*s1 == *s2 && *s1 && n > 1)
    {
        n--;
        s1++;
        s2++;
    }
    return (*(unsigned char *)s1 - *(unsigned char *)s2);
}

他的做法是先取消引用,然后再进行类型转换,以确保 returns 两个无符号字符之间的区别。像这样:

return ((unsigned char)*s1 - (unsigned char)*s2);

在讨论之后(我同意他的看法,我的演员阵容很奇怪)我们查找了一些生产就绪实现的源代码,令我们惊讶的是,Apple 似乎 cast/dereference 的顺序与我相同:

https://opensource.apple.com/source/Libc/Libc-167/gen.subproj/i386.subproj/strncmp.c.auto.html

因此问题是:这种情况有什么区别?为什么选择一个而不是另一个?

(我已经找到了以下内容;但它指定了 casting/dereferencing 不同大小的数据类型,而在 chars/unsigned 字符的情况下应该无关紧要吧?

In C, if I cast & dereference a pointer, does it matter which one I do first?)

补码系统(几乎所有系统)上,它不会有什么不同。

第一个例子--*(unsigned char *)x--将简单地将存储在该位置的数据的二进制值解释为unsigned char,因此如果存储在该位置的十进制值是-1,那么存储的十六进制值(假设 CHAR_BIT=8)是 0xFF 然后它将被简单地解释为 255 因为它符合十六进制表示。

第二个示例(假设 char 在此编译器上已签名)--(unsigned char)*x-- 将首先获取存储在该位置的值,然后将其转换为未签名。所以我们得到 -1 并将其转换为 unsigned char,标准规定要将负符号数转换为无符号值,您将比该类型可存储的最大值多加一到负值尽可能多,直到您的值在其范围内。所以你得到 -1 + 256 = 255

但是,如果您以某种方式使用 补码 系统,情况就会有所不同。

同样,使用*(unsigned char *)x,我们将-1的十六进制表示重新解释为unsigned char,但这次的十六进制表示是0xFE,这将被解释作为 254 而不是 255.

回到 (unsigned char)*x,它仍然只需要执行 -1 + 256 就可以得到 255 的最终结果。

综上所述,我不确定 char 的第 8 位是否可以用于 C 标准的字符编码。我知道它没有用在 ASCII 编码的字符串中,这也是您最有可能使用的字符串,因此在比较实际字符串时您可能不会遇到任何负值。


从有符号到无符号的转换可以在 C11 标准的第 6.3.1.3 节中找到:

  1. When a value with integer type is converted to another integer type other than _Bool, if the value can be represented by the new type, it is unchanged.

  2. Otherwise, if the new type is unsigned, the value is converted by repeatedly adding or subtracting one more than the maximum value that can be represented in the new type until the value is in the range of the new type.

And why choose one over the other?

下面以一种有趣的方式与非 2 的补码有所不同。

// #1
return (*(unsigned char *)s1 - *(unsigned char *)s2);
// *2
return ((unsigned char)*s1 - (unsigned char)*s2);

整数非 2 的补码编码(几乎绝迹了这些日子)​​,有一个位模式 -0 陷阱表示 .

如果代码在 s1 指向时使用 (unsigned char)*s1,则 -0 将变为无符号 0,否则可能会发生陷阱。

随着 -0 成为 unsigned char,这将失去与 空字符 的算术区别 - [=47= 末尾的字符]刺.
在 C 中,空字符 是 "byte with all bits set to 0".

为了防止这种情况,使用 (*(unsigned char *)s1

C 需要它:

7.24.1 String function conventions
For all functions in this subclause, each character shall be interpreted as if it had the type unsigned char (and therefore every possible object representation is valid and has a different value). C17dr § 7.24.1.3

为此,OP 的代码有一个错误。有了非 2 的赞美,*s1 不应该像 -0.

一样停止循环
// while (*s1 == *s2 && *s1 && n > 1)
while ((*(unsigned char *)s1 == (*(unsigned char *)s2 && (*(unsigned char *)s1 && n > 1)

对于迂腐的人来说,char 可能与 int 大小相同。一些图形处理器已经做到了这一点。在这种情况下,为防止溢出,可以使用以下方法。也适用于通常的 8 位 char

// return (*(unsigned char *)s1 - *(unsigned char *)s2);
return (*(unsigned char *)s1 > *(unsigned char *)s2) - 
       (*(unsigned char *)s1 < *(unsigned char *)s2);

备选

int strncmp(const char *s1, const char *s2, size_t n) {
  const unsigned char *u1 = (const unsigned char *) s1;
  const unsigned char *u2 = (const unsigned char *) s2;
  if (n == 0) {
      return (0);
  }
  while (*u1 == *u2 && *u1 && n > 1) {
      n--;
      u1++;
      u2++;
  }
  return (*u1 > *u2) - (*u1 < *u2);
}