C中使用字符相减实现strcmp函数

32

我前段时间看到了这个 strcmp 的实现方式,有一个纯粹出于教育目的的问题。为什么需要将输入转换为16位整数,进行计算,然后再转换回8位呢?在8位中进行减法操作有什么问题吗?

int8_t strcmp (const uint8_t* s1, const uint8_t* s2)
{
  while ( *s1 && (*s1 == *s2) )
  {
    s1++; 
    s2++;
  }

  return (int8_t)( (int16_t)*s1 - (int16_t)*s2 );
}
注意:代码假设为16位的int类型。 编辑: 有人提到C语言默认会将类型转换为int(假设为32位)。即使代码明确声明要强制转换成16位的int,是否仍然是这种情况?

3
不符合标准规范,请勿使用。 - too honest for this site
关于你的EDIT,是的,情况仍然如此。从概念上讲,这些值被转换为int16_t,然后转换为int(如果intint16_t宽,这可能不是这样,尽管它至少与int16_t一样宽)。 - Ian Abbott
也许重点是保证不可移植性。在没有本地硬件类型的系统上,int8_t和int16_t不存在。 - Pete Becker
1
如果 s1 是 255,s2 是 1 呢?那么 (int16_t)*s1 - (int16_t)*s2 的结果是 254,而 (int8_t)254 得到的是一个实现定义的值,可能是 -2。尽管 *s1 > *s2,但 -2 < 0,所以这个结果是错误的。 - user253751
(int16_t)*s1 - (int16_t)*s2中的强制类型转换是不必要的,因为所有小于int的类型在进行任何算术运算之前都必须提升为int。 - phuclv
5个回答

24

strcmp(a,b)函数应返回:

  • <0,如果string a < string b
  • >0,如果string a > string b
  • 0,如果string a == string b

实际上,在同一位置上两个字符串的第一个字符不同时(0,字符串终止符也可以)进行比较测试。

由于函数接受两个uint8_t(无符号字符),因此开发人员可能担心对两个无符号字符进行比较将得到介于0255之间的数字,因此永远不会返回负值。例如,118 - 236会返回-118,但在8位上它会返回138

因此,程序员决定将其强制转换为有符号整数(16位)int_16

这样可能可以起作用,并给出正确的负/正值(前提是函数返回int_16而不是int_8)。

(*编辑:来自@zwol的评论,整数提升是不可避免的,因此这个int16_t强制转换是不必要的)

然而,最终的int_8转换破坏了逻辑。由于返回值可能介于-255255之间,因此在将其转换为int_8后,其中一些值将看到它们的符号被反转。

例如,执行255 - 0会得到正的255(在16位上,所有较低的8位都设置为1,MSB为0),但在int_8世界中(8位有符号整数)这是负的-1,因为我们只有最后的低8位设置为二进制11111111或十进制-1

绝对不是一个好的编程示例。

来自苹果公司的工作函数更好。

for ( ; *s1 == *s2; s1++, s2++)
    if (*s1 == '\0')
        return 0;
return ((*(unsigned char *)s1 < *(unsigned char *)s2) ? -1 : +1);

(Linux用汇编代码实现这个功能...)


无限清晰! 然而,出于教育目的,还有一个最后的问题。当从int_16向下转换为int_8时,LSB或MSB位是否被保留,还是取决于实现? - madski
1
int16_t 转换为 int8_t 的结果是实现定义的。大多数实现只是丢弃高位。 - Ian Abbott
2
无论是否将其转换为int16_t,实际的减法都将在int上执行,因为存在整数提升。 int允许与int16_t相同类型,但不允许int8_t相同类型,因此整数提升是不可避免的;(在标准内)没有办法强制C实际对[u]int8_t数量进行算术运算。(请注意,这里有一个int8_tchar之间的区别;在CHAR_BIT >= 16的实现中,intchar可能是相同的类型;但是,这样的实现根本无法提供int8_t!) - zwol
1
@zwol - 糟糕,我通常不会阅读C语言问题,但是通过后门进入了这个问题。 <g> - Pete Becker
Apple函数看起来比OP的更糟,因为多个返回语句是不必要的。此外,它似乎滥用了无符号溢出。 - Lundin
显示剩余6条评论

9
实际上,必须至少使用16位来进行区分¹,因为结果的范围是-255到255,这不适用于8位。然而,sfstewman正确地指出,由于隐式整数提升,它会发生。
最终转换为8位是错误的,因为它可能会溢出,因为范围仍然不适合8位。而且,strcmp确实应该返回普通的int
¹ 9就足够了,但位通常以8个一组。

我认为程序员只关心结果的符号,只要保留了这个符号就可以了。 - madski
1
操作数将始终被提升为int,它必须至少容纳16位的值,然后再进行减法运算,这使得操作数的8位特性变得无关紧要。请参阅整数提升规则(C11 6.1.1.3p2)。 - sfstewman
"执着模式": "至少必须用9个比特位来区分..." - ClickRick

3

输入数据是无符号8位的,为了避免截断和溢出/下溢的影响,应将其转换为至少9位有符号数,因此使用int16。


1
然后他们通过返回一个 int8_t 来破坏结果! - Ian Abbott

2
return (int8_t)( (int16_t)*s1 - (int16_t)*s2 );

这可能有以下两种情况:
  • 程序员对C语言中隐式类型提升的工作方式感到困惑。无论如何,两个操作数都将被隐式转换为int,而不管是否将其转换为int16_t。因此,如果int是32位的,那么这段代码就是无意义的。或者,如果int在特定系统上等同于int16_t,则根本不会进行转换。

  • 或者程序员非常了解类型提升的工作方式,并编写需要符合禁止隐式类型提升的标准(例如MISRA-C)的代码。在这种情况下,并且在给定系统上int是16位时,该代码是完全合理的:它强制进行显式类型提升以避免编译器/静态分析器的警告。

我猜第二种情况最有可能,这段代码旨在用于小型微控制器系统。


你的第二个猜测是正确的。它是针对MISRA-C编写的。 - madski
我不确定这样的标准如何“禁止”隐式类型提升,或者它只适用于某些表达式?(例如,在原始代码中,表达式*s1 == *s2也涉及隐式类型提升,至少在概念上是这样,尽管在这种情况下这样的提升对于==!=运算符没有影响。) - Ian Abbott
1
@IanAbbott:MISRA标准的目的是要求代码不仅在C语言规则下工作,而且还要编写成在具有与“int”大小无关的规则的语言中同样有效的方式。不幸的是,C语言规则中的一些怪癖使得编写行为与int大小无关的代码变得困难。例如,uint16_t x=65535; x*=x;将在所有定义了uint16_tint为16位或64位的平台上将x设置为1,但在某些int为32位的机器上,它可能会违反时间和因果律。 - supercat
@rici 如果您希望编写自己的 MISRA-C 兼容标准库函数,您将不得不重新定义标准函数定义,因为 C 标准库本身并不符合 MISRA 标准。在小型微控制器系统中进行 int8_t 强制转换并返回它是完全有意义的。 - Lundin
@rici 这里的强制类型转换不会溢出,因为比较的是两个字符,假设它们是标准的7位ASCII码。使用int8_t相对于int的优势在于:它可以在小型处理器上节省执行速度,并且在大多数CPU上可以节省1个字节的内存。此外,它使返回类型和符号确定:你知道它返回一个1字节的二进制补码有符号整数。如果使用int,则无法了解其格式:可能是从16到64位的任何形式,也可能是C标准允许的任何奇怪的符号。使用int的代码通常是不可移植的。 - Lundin
显示剩余2条评论

1
如果没有 int16_t 的限制,某些值会因溢出而导致两个数字之间的差异不同。在 int8_t 中,您的范围为-128到127,在 uint8_t 中,您的范围为0到255,在 int16_t 中,您的范围将为-32,768到32,767。
uint8_t 转换为 int8_t 会导致由于溢出而使大于127的值改变符号,因此这可以避免发生,但是输出应该是 int16_t,因为如果您有一个255 - 0的结果,它将是截断返回。

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