C指针算术

9

这是一段我不理解的代码,它只是将一个字符串反转。

#include <stdio.h>

void strrev(char *p)
{
  char *q = p;
  while(q && *q) ++q;
  for(--q; p < q; ++p, --q)
    *p = *p ^ *q,
    *q = *p ^ *q,
    *p = *p ^ *q;
}

int main(int argc, char **argv)
{
  do {
    printf("%s ",  argv[argc-1]); strrev(argv[argc-1]);
    printf("%s\n", argv[argc-1]);
  } while(--argc);

  return 0;
}

我不明白的唯一一段代码是这一行:while(q && *q) ++q;,它用于寻找eos。既然q永远不会为0,那么与while(*q) ++q;不是一样吗?作者如何确保q*q将为0?
该代码来自于这个问题:如何在C或C++中原地反转字符串?

4
它可以防止函数将空指针参数推迟。 - Paulo Bu
1
请注意,该程序未遵守规范,因为函数的“命名空间” str* 是保留的。 - unwind
15
那段代码实在是糟糕透顶,你最好直接删除它。 - David Heffernan
3个回答

33

David Heffernan的评论是正确的。那段代码很糟糕。

你所询问的代码的目的是跳过引用q(如果它为空)。因此,代码的作者认为q可能为空。 q何时可能为空?最明显的是:如果p为空。

因此,让我们看看当p为空时,代码会做什么。

void strrev(char *p) // Assumption: p is null
{
  char *q = p; // now q is null
  while(q && *q) ++q; // The loop is skipped, so q and p are both still null.
  for(--q; 

所以我们要做的第一件事是将 q 减小,它此时为 null。很可能这会导致一个环绕操作并且我们最终得到一个包含最大可能指针的 q。

    p < q; 

由于null比除了null以外的所有内容都要小,而且q不再是null,所以这是真的。我们进入循环...

    ++p, --q)
    *p = *p ^ *q,

及时取消对空引用的引用。

    *q = *p ^ *q,
    *p = *p ^ *q;
}

顺便提一下,Coverity中我们把这个叫做“前向空指针缺陷”——即代码路径表明一个值可能为空,然后同一代码路径后面却假定它不为空的模式。这是非常普遍的。

那么,如果我们给它一个null作为参数,这段代码就会完全失效。它还有其他的错误方式吗?如果我们给它一个空字符串会发生什么?

void strrev(char *p) // Assumption: *p is 0
{
  char *q = p; // *q is 0
  while(q && *q) ++q; // The second condition is not met so the body is skipped.
  for(--q; // q now points *before valid memory*.
       p < q  // And we compare an invalid pointer to a valid one.

在C语言中,当你从指向有效内存的指针中减去一个单位并将其与另一个指针比较时,我们是否有一个保证该比较是合理的?因为这让我感到非常危险。我不太了解C标准,无法确定这是否是未定义的行为。

此外,此代码使用可怕的“异或交换两个字符”的技巧。为什么会有人这样做呢?它生成更大、速度更慢的机器码,并且更难读懂、理解和维护。如果要交换两个东西,就直接交换它们。

此外,这个函数还使用逗号操作符将多个语句放在一个语句中,以避免在for循环体周围使用大括号的恐惧。这种奇怪的做法有什么目的呢?代码的目的不是展示你知道多少运算符,而是首先要向代码阅读者传达信息。

此函数还修改了其形式参数,这使得它很难调试。


4
一次糟糕的代码审查。在读完这篇文章后,“令人震惊”的形容词似乎已经是一种褒义词了 :) - Paulo Bu
6
C语言确保指针比较时只有在两个指针都指向同一对象或者是同一个聚合对象的成员时,才能安全地进行有意义的比较。特殊情况下,对于数组中指向最后一个元素之后的指针,为了比较目的它们被认为是数组的一部分,但是不能实际进行解引用操作。而对于指向数组开始之前的指针,则行为是未定义的。 - This isn't my real name
5
有关C语言中指针的加减和比较操作的详细信息,请参阅N1570,第6.5.6节的第8段(PDF页面111,标记为93页),第6.5.8节的第4和第5段(PDF页面113/114,标为95/96页)以及第6.5.9节的第5和第6段。 - This isn't my real name
@ElchononEdelson:这正是我怀疑的,感谢您的确认和参考资料。 - Eric Lippert
"很可能会出现这种情况" - 是的,但值得指出的是这是未定义的行为。 - klutt
1
“我们有保证吗?” - 没有。UB https://dev59.com/9FEG5IYBdhLWcg3wduUf#65649290 - klutt

5
代码
while(q && *q)

是的,这是一个简写形式,表示

while(q != NULL && *q != '\0')

因此,您正在测试q(一开始等于p)是否为NULL。这意味着使用NULL参数调用的函数将不会在此while循环中崩溃。(但它仍然会在第二个循环中崩溃)。


1
OP 询问是否可以使用 while(*q) - haccks
2
不,问题是 while (q && *q) 和 while(*q) 是否相同。 - Wojtek Surowka
好的。这个回答是否说明了 while(q && *q)while(q != NULL && *q != '\0') 的简写形式? - haccks

1
while(*q) ++q;”和“while(q && *q)”不是一样的吗?因为“q”永远不会是“0”。“while(q && *q)”用于确保“q”不是“NULL”,且“*q”不是空字符。如果“q”不指向“NULL”,则使用“while(*q)”也是合法的。
void string_rev(char *p)
{
   char *q = p;

   if(q == NULL)
       return;

   while(*q) ++q;
   for(--q; p < q; ++p, --q)
       *p = *p ^ *q,
       *q = *p ^ *q,
       *p = *p ^ *q;

}  

作者如何确定代码中的 q 或 *q 将会是 0?
在 while(q && *q) 的情况下,如果 p 指向 NULL 并且循环在进入循环体之前终止,则 q 可能是 NULL 指针。否则,在整个操作过程中它不能是 NULL 指针。现在循环终止取决于 *q。一旦 q 到达字符串的末尾,*q 就变成了 0,并且循环终止。

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