关于C#:指针澄清的指针

Pointer to pointer clarification

我正在按照本教程关于指针如何工作。

让我引用相关段落:

1
2
    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;

Now we can set

1
    int **ipp = &ip1;

and ipp points to ip1 which points to i. *ipp is ip1, and **ipp is i, or 5. We can illustrate the situation, with our familiar box-and-arrow notation, like this:

enter image description here

If then we say

1
    *ipp = ip2;

we've changed the pointer pointed to by ipp (that is, ip1) to contain a copy of ip2, so that it (ip1) now points at j:

enter image description here

我的问题是:为什么在第二张图片中,ipp仍然指向ip1而不是ip2


忘记关于指向类比的第二个问题。指针真正包含的是内存地址。 &是"地址"运算符 - 即它返回对象内存中的地址。 *运算符为您提供指针引用的对象,即给定包含地址的指针,它返回该存储器地址处的对象。所以当你执行*ipp = ip2时,你正在做的是*ipp将对象放在ipp中保存的地址ip1,然后将ip1中存储的值分配给ip1,这是地址j

只是
& - >地址
* - >价值在


因为您更改了ipp指向的值而不是ipp的值。因此,ipp仍然指向ip1(ipp的值),ip1的值现在与ip2的值相同,因此它们都指向j

这个:

1
*ipp = ip2;

是相同的:

1
ip1 = ip2;


像C标签中的大多数初学者问题一样,这个问题可以通过回到第一原则来回答:

  • 指针是一种值。
  • 变量包含值。
  • &运算符将变量转换为指针。
  • *运算符将指针转换为变量。

(从技术上讲,我应该说"左值"而不是"变量",但我觉得将可变存储位置描述为"变量"更为明确。)

所以我们有变量:

1
2
int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

变量ip1包含一个指针。 &运算符将i转换为指针,指针值分配给ip1。所以ip1包含一个指向i的指针。

变量ip2包含指针。 &运算符将j转换为指针,并将该指针指定给ip2。所以ip2包含一个指向j的指针。

1
int **ipp = &ip1;

变量ipp包含一个指针。 &运算符将变量ip1转换为指针,并将指针值分配给ipp。所以ipp包含一个指向ip1的指针。

让我们总结一下这个故事:

  • i包含5
  • j包含6
  • ip1包含"指向i的指针"
  • ip2包含"指向j的指针"
  • ipp包含"指向ip1的指针"

现在我们说

1
*ipp = ip2;

*运算符将指针变回变量。我们获取ipp的值,这是"指向ip1的指针并将其转换为变量。什么变量?ip1当然!

因此,这只是另一种说法

1
ip1 = ip2;

所以我们获取ip2的值。它是什么?"指向j的指针"。我们将指针值赋给ip1,所以ip1现在是"指向j的指针"

我们只改变了一件事:ip1的值:

  • i包含5
  • j包含6
  • ip1包含"指向j的指针"
  • ip2包含"指向j的指针"
  • ipp包含"指向ip1的指针"

Why does ipp still point to ip1 and not ip2?

分配给变量时,变量会发生变化。计算作业;变量没有比赋值更多的变化!首先分配ijip1ip2ipp。然后分配给*ipp,正如我们所见,它意味着与"分配给ip1"相同。由于你没有第二次分配到ipp,它没有改变!

如果您想要更改ipp,则必须实际分配到ipp

1
ipp = &ip2;

例如。


希望这段代码可以提供帮助。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <stdio.h>
using namespace std;

int main()
{
    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;
    int** ipp = &ip1;
    printf("address of value i: %p
"
, &i);
    printf("address of value j: %p
"
, &j);
    printf("value ip1: %p
"
, ip1);
    printf("value ip2: %p
"
, ip2);
    printf("value ipp: %p
"
, ipp);
    printf("address value of ipp: %p
"
, *ipp);
    printf("value of address value of ipp: %d
"
, **ipp);
    *ipp = ip2;
    printf("value ipp: %p
"
, ipp);
    printf("address value of ipp: %p
"
, *ipp);
    printf("value of address value of ipp: %d
"
, **ipp);
}

它输出:

enter image description here


我个人的观点是,箭头指向这种方式的图片或使指针更难理解的图片。它确实使它们看起来像一些抽象的,神秘的实体。他们不是。

与计算机中的其他所有内容一样,指针是数字。名称"指针"只是一种说"含有地址的变量"的奇特方式。

因此,让我通过解释计算机实际工作原理来解决问题。

我们有一个int,它的名字是i,值是5.它存储在内存中。就像存储在内存中的所有内容一样,它需要一个地址,否则我们将无法找到它。假设i在地址0x12345678处结束,其伙伴j在其之后结束。假设32位CPU,其中int是4个字节,指针是4个字节,那么变量存储在物理内存中,如下所示:

1
2
3
Address     Data           Meaning
0x12345678  00 00 00 05    // The variable i
0x1234567C  00 00 00 06    // The variable j

现在我们想指出这些变量。我们创建一个指向int,int* ip1和一个int* ip2的指针。就像计算机中的所有内容一样,这些指针变量也会在内存中的某处分配。让我们假设它们在j之后立即在内存中的下一个相邻地址处结束。我们设置指针以包含先前分配的变量的地址:ip1=&i;("将i的地址复制到ip1")和ip2=&j。这些行之间发生的事情是:

1
2
3
Address     Data           Meaning
0x12345680  12 34 56 78    // The variable ip1(equal to address of i)
0x12345684  12 34 56 7C    // The variable ip2(equal to address of j)

所以我们得到的只是一些包含数字的4字节内存块。在任何地方都没有任何神秘或神奇的箭头。

实际上,仅通过查看内存转储,我们无法判断地址0x12345680是否包含intint*。不同之处在于我们的程序选择如何使用存储在此地址的内容。 (我们程序的任务实际上只是告诉CPU如何处理这些数字。)

然后我们用int** ipp = &ip1;添加另一个间接层。再一次,我们只获得了一大块内存:

1
2
Address     Data           Meaning
0x12345688  12 34 56 80    // The variable ipp

这种模式似乎很熟悉。另一个包含数字的4个字节的块。

现在,如果我们有一个上述虚构的小RAM的内存转储,我们可以手动检查这些指针指向的位置。我们查看存储在ipp变量地址的内容,找到内容0x12345680。这当然是存储ip1的地址。我们可以去那个地址,检查那里的内容,找到i的地址,最后我们可以去那个地址找到数字5。

因此,如果我们获取ipp,*ipp的内容,我们将获得指针变量ip1的地址。通过写*ipp=ip2我们将ip2复制到ip1,它相当于ip1=ip2。无论哪种情况,我们都会得到

1
2
3
Address     Data           Meaning
0x12345680  12 34 56 7C    // The variable ip1
0x12345684  12 34 56 7C    // The variable ip2

(这些示例是为大端CPU提供的)


注意分配:

1
ipp = &ip1;

结果ipp指向ip1

所以对于ipp指向ip2,我们应该以类似的方式改变,

1
ipp = &ip2;

我们显然没有这样做。相反,我们正在更改ipp指向的地址处的值。
通过做下面的事情

1
*ipp = ip2;

我们只是替换ip1中存储的值。

ipp = &ip1,表示*ipp = ip1 = &i
现在,*ipp = ip2 = &j
因此,*ipp = ip2ip1 = ip2基本相同。


因为当你说

1
*ipp = ip2

你说的是'ipp'指向的对象指向ip2指向的内存方向。

你不是说ipp指向ip2


1
ipp = &ip1;

以后的分配没有改变ipp的值。这就是为什么它仍然指向ip1

使用*ipp(即使用ip1)执行的操作不会改变ipp指向ip1的事实。


My question is: Why in the second picture, ipp is still point to ip1 but not ip2?

你放了漂亮的照片,我打算做一个漂亮的ascii艺术:

就像@ Robert-S-Barnes在他的回答中所说:忘记指针,什么指向什么,但从记忆的角度来思考。基本上,int*表示它包含变量的地址,int**包含包含变量地址的变量的地址。然后,您可以使用指针的代数来访问值或地址:&foo表示address of foo*foo表示value of the address contained in foo

因此,作为关于处理内存的指针,实际制作"有形"的最佳方法是显示指针代数对内存的作用。

所以,这是你的程序的内存(为了示例的目的简化):

1
2
3
name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [   |   |   |   |   ]

当你做初始代码时:

1
2
int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

这是你的记忆的样子:

1
2
3
name:    i   j ip1 ip2
addr:    0   1   2   3
mem : [  5|  6|  0|  1]

在那里你可以看到ip1ip2获取ij的地址,ipp仍然不存在。
不要忘记,地址只是以特殊类型存储的整数。

然后你声明并定义ipp,例如:

1
int **ipp = &ip1;

所以这是你的记忆:

1
2
3
name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  0|  1|  2]

然后,您将更改存储在ipp中的地址所指向的值,即
存储在ip1中的地址:

1
*ipp = ip2;

程序的记忆是

1
2
3
name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  1|  1|  2]

N.B。:因为int*是一种特殊类型,我更喜欢总是避免在同一行上声明多个指针,因为我认为int *x;int *x, *y;符号可能会产生误导。我更喜欢写int* x; int* y;

HTH


如果将取消引用运算符*添加到指针,则从指针重定向到指向对象。

例子:

1
2
3
4
5
int i = 0;
int *p = &i; // <-- N.B. the pointer declaration also uses the `*`
             //     it's not the dereference operator in this context
*p;          // <-- this expression uses the pointed-to object, that is `i`
p;           // <-- this expression uses the pointer object itself, that is `p`

因此:

1
2
*ipp = ip2; // <-- you change the pointer `ipp` points to, not `ipp` itself
            //     therefore, `ipp` still points to `ip1` afterwards.

你刚开始设定,

1
ipp = &ip1;

现在取消引用它,

1
2
*ipp = *&ip1 // Here *& becomes 1  
*ipp = ip1   // Hence proved


考虑每个变量如下所示:

1
type  : (name, adress, value)

所以你的变量应该像这样表示

1
2
3
4
5
int   : ( i ,  &i , 5 ); ( j ,  &j ,  6); ( k ,  &k , 5 )

int*  : (ip1, &ip1, &i); (ip1, &ip1, &j)

int** : (ipp, &ipp, &ip1)

由于ipp的值是&ip1所以结构:

1
*ipp = ip2;

将addess &ip1的值更改为ip2的值,这意味着ip1已更改:

1
(ip1, &ip1, &i) -> (ip1, &ip1, &j)

ipp仍然:

1
(ipp, &ipp, &ip1)

所以ipp的值仍然&ip1,这意味着它仍然指向ip1


如果您希望ipp指向ip2,则必须说ipp = &ip2;。但是,这会使ip1仍然指向i


*ipp = ip2;暗示:

ip2分配给ipp指向的变量。所以这相当于:

1
ip1 = ip2;

如果您希望ip2的地址存储在ipp中,只需执行以下操作:

1
ipp = &ip2;

现在ipp指向ip2


因为您正在更改*ipp的指针。它的意思是

  • ipp(可变名称)----进去吧。
  • inside ippip1的地址。
  • 现在*ipp所以去(内部的地址)ip1
  • 现在我们在ip1
    *ipp(即ip1)= ip 2。
    ip2包含地址j .so ip1内容将被包含ip2(即j的地址)取代,
    我们不会改变ipp内容。
    而已。


    ipp可以保存指向指针类型对象的指针(即,指向)。当你这样做

    1
    ipp = &ip2;

    那么ipp包含变量(指针)ip2的地址,它是指向指针的类型指针的(&ip2)。现在第二张图片中ipp的箭头将指向ip2

    维基说:
    *运算符是一个取消引用运算符,对指针变量进行操作,并返回一个等于指针地址值的l值(变量)。这称为取消引用指针。

    ipp上应用*运算符将其解析为指向int类型的指针的l值。解除引用的l值*ipp是指向int的类型指针,它可以保存int类型数据的地址。声明之后

    1
    ipp = &ip1;

    ipp保持ip1的地址,*ipp保持(指向)i的地址。您可以说*ippip1的别名。 **ipp*ip1都是i的别名。
    通过做

    1
     *ipp = ip2;

    *ippip2都指向同一位置,但ipp仍然指向ip1

    实际上*ipp = ip2;实际上是将ip2(j的地址)的内容复制到ip1(因为*ippip1的别名),实际上使两个指针ip1ip2指向同一个对象(j)。
    因此,在第二个图中,ip1ip2的箭头指向j,而ipp仍然指向ip1,因为未进行任何修改以更改ipp的值。