指针相减

39

我被要求在大学作业中描述这些代码的功能

int main() {
    int t1[] = {0,0,1,1,1}, t2[] = {0,0,1,1,1};
    int *p1 = t1, *p2 = t2;

    while (!*p1++ || !*p2++);
    cout << (p1-t1) << endl;
    cout << (p2-t2) << endl;
}

我的理解是,创建了两个 int 类型的数组并填充了值,创建了两个指针并指向每个数组,然后我开始遇到问题。

while (!*p1++ || !*p2++);

在我看来,这句话的意思是当0移动*p1的位置一个单位或者当0移动*p2的位置一个单位时,我对这个假设并不太自信?

cout << (p1-t1) << endl;

然后我们转到 cout,我的理解是,我从t1的位置中减去p1的位置,其中p1由while定位,而t1指向数组中的第一个位置。 如果我的假设错误,请谅解,因为我只是在学习指针。


1
@Johntk:main 应该返回一个 int。一些系统期望当程序成功完成工作时,main 返回 0。当 main 返回 void 时,系统将尝试读取退出状态,得到的是垃圾值。 - GingerPlusPlus
5
这些代码表明程序员不称职,或是在编写恶搞代码。如果这些代码进入你的代码库,它表明你的代码审查流程需要重新设计。哦,它还会输出“5\n3\n”,但那不是重点。 - Yakk - Adam Nevraumont
4
我认为这项任务只是让我们思考和研究,并没有要求或教导我们像这样编码。 - JTK
4
我认为老师们应该在课前做出免责声明:"如果你写的代码像这样,我会解雇你,但现在我们将扮演编译器和执行器的角色。"并要求学生在寻求外部帮助时提醒他们。该代码似乎有效且不引发未定义行为(如果我错了,请不要怪我)。在我看来,这只是一个学术问题,旨在尝试理解黑魔法咒语。哦...语句。 - luk32
2
@JamesKanze - 即使我永远不会编写那样的代码,我仍然必须能够阅读它,因为其他人会编写它。当应用程序崩溃时,“我不知道这段代码做了什么”不是一个可以接受的答案。 - Pete Becker
显示剩余8条评论
6个回答

46

while循环实际上非常可怕。我从未在现实生活中见过这样的代码,并且会认为任何在现实生活中这样做的程序员都是疯子。我们需要逐步进行:

while (condition);

这里有一个带有空语句的while语句(仅一个";"的空语句)。条件被评估,如果为真,则执行该语句(因为空语句而不执行任何操作),然后我们重新开始。换句话说,条件将重复评估直到为假。

condition1 || condition2

这是一个“或”语句。首先评估第一个条件。如果它为真,则第二个条件不被评估,结果为“真”。如果它为假,则评估第二个条件,并相应地得到“真”或“假”的结果。

while (condition1 || condition2);

这段代码首先会评估第一个条件,如果它为真,则我们重新开始循环。如果为假,则我们评估第二个条件。如果第二个条件为真,则我们重新开始循环。如果两个条件都为假,则我们退出循环。请注意,只有在第一个条件为假时才会评估第二个条件。现在让我们看一下这些条件:

!*p1++
!*p2++

这与*(p1++) == 0和*(p2++) == 0相同。每个条件在评估后都会增加p1或p2,无论结果如何。如果*p1或*p2为零,则每个条件为真,否则为假。现在我们检查每次迭代发生的情况:

p1 = &t1 [0], p2 = &t2 [0]
*p1++ == 0 is true, *p2++ == 0 is never evaluated, p1 = &t1 [1], p2 = &t2 [0].
*p1++ == 0 is true, *p2++ == 0 is never evaluated, p1 = &t1 [2], p2 = &t2 [0].
*p1++ == 0 is false, *p2++ == 0 is true, p1 = &t1 [3], p2 = &t2 [1].
*p1++ == 0 is false, *p2++ == 0 is true, p1 = &t1 [4], p2 = &t2 [2].
*p1++ == 0 is false, *p2++ == 0 is false, p1 = &t1 [5], p2 = &t2 [3].

t1等同于&t1[0]。p1 - t1 == &t1[5] - &t1[0] == 5。 t2等同于&t2[0]。p2 - t2 == &t2[3] - &t2[0] == 3。


谢谢,这里所有的回答都不错,但你最后的代码块真的帮助我更好地理解了正在发生的事情。 - JTK
10
我支持@GingerPlusPlus的评论。在一个条件语句中使用副作用通常不是一个好主意(尽管有一些例外惯用语会使用它们);而在条件语句中使用副作用会更糟糕。另外,使用!来与零进行比较,而不是明确表示,这会使代码更加晦涩难懂。 - James Kanze
2
除了极少数情况(例如在不等式比较附近使用并行结构),我认为我们应该始终使用 !var 或裸露的 var 来编写零比较,尤其是当 var 是指针时。原因基本上与为什么不应该编写显式的 == true== false 相同。 - zwol
2
@alex.forencich 如果你的意思是尽可能避免使用裸指针,那当然可以,但是超出数组末尾的指针非常常见和有用;随机访问迭代器通常只是裸指针的薄包装器,在这种情况下,容器的 end() 迭代器实际上就相当于这样一个指针。 - bcrist
2
这样的示例代码并不是为了展示好的编码风格,而是一个非常棒的调试和逻辑练习。 - Kik
显示剩余4条评论

14

你对 t1t2p1p2 的评估是正确的。

while (!*p1++ || !*p2++);
我不喜欢这种编码风格,因为很容易认为程序员是误打了分号。为了指示意图是空语句,应该以某种方式将其与其他内容区别开来(比如使用注释、放在单独的一行上或使用花括号)。

while 循环体进入条件为 true 。由于这是一个逻辑或表达式,只有在 *p1++*p2++ 都变成非零值时,while 循环才会终止。由于逻辑或运算符会发生“短路”(如果第一个表达式为真,则不会执行第二个表达式),p1p2 的变化情况如下:

iter  p1     *p1    p2     *p2    condition
----  --     ---    --     ---    ---------
 0    &t1[0]  0     &t2[0]  0     !*p1++ is true,  !*p2++ not evaluated
 1    &t1[1]  0     &t2[0]  0     !*p1++ is true,  !*p2++ not evaluated
 2    &t1[2]  1     &t2[0]  0     !*p1++ is false, !*p2++ is true
 3    &t1[3]  1     &t2[1]  0     !*p1++ is false, !*p2++ is true
 4    &t1[4]  1     &t2[2]  1     !*p1++ is false, !*p2++ is false

由于每次迭代都使用后置递增,因此p1以值&t1[5]结束,p2以值&t2[3]结束。

在同一数组中进行指针减法时,它会以数组元素数量为单位测量两个指针之间的距离。在大多数表达式中使用的数组名称将衰减为等于其第一个元素的指针的值。所以t1衰减为&t1[0]t2衰减为&t2[0]

因此:

p1 - t1 => 5
p2 - t2 => 3

3
"+1" 对于 “在同一数组内进行指针相减,可以通过数组元素的数量来度量两个指针之间的距离” 这一点提出了肯定。这在其他回答中并不是很清楚。 - dyesdyes

13

关键要注意的是如何评估表达式 (a || b)。首先,表达式 a 会被评估。如果 a 返回 true,则不会评估 b,因为任何与 True 进行逻辑或运算的结果都将是 True。这被称为短路逻辑。

以下代码的改进也有所帮助 -

int main(void){
    int t1[] = {0,0,1,1,1}, t2[] = {0,0,1,1,1};
    int *p1 = t1, *p2 = t2;

    cout << *p1 << " " << *p2 << endl;
    cout << p1 << " " << p2 << endl;
    while (!*p1++ || !*p2++) { 
        cout << *p1 << " " << *p2 << endl;
        cout << p1 << " " << p2 << endl;
    }   
    cout << (p1-t1) << endl;
    cout << (p2-t2) << endl;
    return 0;
}

输出:

0 0
0x7fff550709d0 0x7fff550709f0
0 0
0x7fff550709d4 0x7fff550709f0
1 0
0x7fff550709d8 0x7fff550709f0
1 0
0x7fff550709dc 0x7fff550709f4
1 1
0x7fff550709e0 0x7fff550709f8
5 // Final p1 - t1
3 // Final p2 - t2

!*p1++ 等同于 (!(*(p1++)))。这是一个后置递增运算符,它会将指针加一,但返回递增前的旧值。

循环中的表达式会被计算 5 次。

  1. 在第一次迭代中,p1 被递增。由于递增前当前的 *p1 值为 0,对 0 取反返回 1。由于短路运算,其余部分不再计算。因此只有 p1 被递增。

  2. 下一轮循环同样发生了相同的事情。

现在,我们有 p1 = t1 + 2 indices,和 p2 = t2

  1. 在第三轮迭代中,*p1 的当前值不再是 0。因此,p1p2 均被递增。

  2. 下一轮循环同样发生了相同的事情。

请注意,在前四轮迭代中,要么 p1,要么 p2 指向了一个 0 值,因此左侧或右侧的非运算结果都是 True,因此 while 循环继续。

  1. 第五轮迭代中,两个指针均被递增,但由于两者都不再指向 0 值,循环退出。

因此,p1 会被递增 5 次,而 p2 会被递增 3 次。

总结 - p1 - t1 的值将包含连续出现在 t1t2 开头的 0 的数量加 1(2 + 2 + 1)。p2 - t2 的值将计算为连续出现在 t2 开头的 0 的数量加 1(2 + 1)。


6

首先:

while (!*p1++ || !*p2++);

这意味着当的内容是0时,不断循环将1添加到中,直到变成非零为止。然后,在的内容为0时,不断循环添加1到和。如果任何时候的内容再次变为0,则逻辑重复(我知道这很困惑)。
基本上,在while(first || second)样式测试中,只有在第一部分失败时才测试第二部分。并且指针会增加,无论测试是否通过。
关于(p1-t1),你的假设是正确的。该计算为您提供了t1和p1之间整数的数量(因为它们是int指针)。因为t1是数组的开头,所以实际计算为指向p1的数组中的索引(偏移量)。
注1:如果p1和t1是char指针,则对它们进行减法运算将为您提供它们之间字符的数量。如果它们是float指针,则对它们进行减法运算将为您提供它们之间的浮点数等等。指针算术按照它们指向的数据类型的单位进行加减。
注2:严格来说,t1是一个数组类型。在指针上下文中使用它时,它会折叠成指针。例如,在指针算术中或将其分配给指针变量时。如果这使您感到困惑,请不要担心,大多数时候它只是作为指针工作,因为编译器会根据上下文自动进行转换。

说t1是一个int指针是不正确和令人困惑的。它可以被隐式转换为int指针,但这并不是同一回事。 - Slava
@Slava 我想保持解释简单,所以我会添加一条注释来给出更详细的解释。 - Galik
此外,您的语句“每次循环都将1添加到p1和p2”也是不正确的,因为您忽略了短路。 - Slava
@Slava 是的,我仍在努力想如何恰当地表达那个意思。 - Galik
“…不是一直循环,每次将p1和p2都加1” - 不完全是这样的。它使用p1进行扫描,在t1数组中查找非零值。对于每个这样的非零值,在t2中跳过一个零值。最终结果类似于“计算t2中前导零的数量;跳过在t1中该数量的非零元素,以及任何零元素”。 - anatolyg
我认为措辞没有问题,但解释有误。它不会每次将1添加到p1和p2,只有在第一个条件为false时才会增加p2。 - Slava

2

这个关系可以帮助你更好地理解while循环中的条件:

arr[ i ] == * ( arr + i )

进行指针减法(如果指针类型相同),结果是两个元素之间的距离(以数组元素为单位)。

假设p1p2都是T*类型的指针。那么计算得到的值为:

( p2 - p1 ) == ( addr( p2 ) - addr( p1 ) ) / sizeof( T )

2

关于这个问题在控制台上会输出什么,答案是在while循环的结尾没有分号的情况下会输出0 0。

这个循环有什么意义呢?

首先你使用了 OR 运算符,这意味着如果 p1 或 p2 指向的值为 0,则会执行该块。因此,在 p1 指向第三个元素(p1-t1)之前,它将给出 t1 中已经穿过的元素数,而 (p2-t2) 将为 0,因为 (p1-t1) 返回 true,所以不会检查第二个条件。当 p1 指向 1 时,它将开始递增 p2,直到它指向 t2 的第三个元素,这就是结束。

我相信这就是这个任务所要求的全部内容。


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