【问题标题】:Pointer to pointer clarification指针说明
【发布时间】:2014-03-03 12:27:12
【问题描述】:

我一直在关注这个tutorial,了解指向指针的指针 是如何工作的。

让我引用相关的段落:


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

现在我们可以设置

    int **ipp = &ip1;

ipp 指向ip1,后者指向i*ippip1**ippi,或 5。我们可以用我们熟悉的箭头符号来说明这种情况,如下所示:

如果那么我们说

    *ipp = ip2;

我们已将ipp(即ip1)指向的指针更改为包含ip2 的副本,因此它(ip1)现在指向j


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

【问题讨论】:

  • 请在定义时添加ipp的类型,这样你的问题就完成了;-)
  • 因为ipp指向ip1所以*ipp = ip2ip1 = ip2一样
  • 我们能不能别写离尖字太远的星号? int **ipp 远不如 int** ipp 直观,尤其是当 **ipp 意味着在类型声明之外完全不同的东西时。
  • @Darkhogg int **ipp 对我来说似乎很直观。这意味着,我正在制作 **ippint。这是真的。
  • @user54609 含义和个人直觉分开,考虑一下:ipp 的类型是int**,所以只写int** 而不是神奇的"imp 取消引用是一个int"理解

标签: c pointers


【解决方案1】:

暂时忘记指向类比。指针真正包含的是内存地址。 & 是“地址”运算符 - 即它返回对象在内存中的地址。 * 运算符为您提供指针所指的对象,即给定一个包含地址的指针,它返回该内存地址处的对象。所以当你做*ipp = ip2时,你所做的就是*ipp获取ipp中保存的地址处的对象,即ip1,然后将存储在ip2中的值分配给ip1,即j的地址。

简单
& --> 地址
* --> 值

【讨论】:

  • & 和 * 从未如此简单
  • 我认为造成混淆的主要原因是 * 运算符的歧义,在变量声明期间,它用于指示变量实际上是指向某种数据类型的指针。但是,另一方面,它也用于语句中访问由指针(解引用运算符)指向的变量的内容。
【解决方案2】:

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

这个:

*ipp = ip2;

等同于:

ip1 = ip2;

【讨论】:

  • 可能值得指出int *ip1 = &i*ipp = ip2; 之间的区别,即如果您从第一条语句中删除int,那么分配看起来非常相似,但* 正在做这两种情况有很大不同。
【解决方案3】:

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

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

(技术上我应该说“左值”而不是“变量”,但我觉得将可变存储位置描述为“变量”更清楚。)

所以我们有变量:

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

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

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

int **ipp = &ip1;

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

让我们总结一下到目前为止的故事:

  • i 包含 5 个
  • j 包含 6 个
  • ip1 包含“指向i 的指针”
  • ip2 包含“指向j 的指针”
  • ipp 包含“指向ip1 的指针”

现在我们说

*ipp = ip2;

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

所以这只是另一种说法

ip1 = ip2;

所以我们获取ip2 的值。它是什么? “指向j 的指针”。我们将该指针值分配给ip1,因此ip1 现在是“指向j 的指针”

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

  • i 包含 5 个
  • j 包含 6 个
  • ip1 包含“指向j 的指针”
  • ip2 包含“指向 j 的指针”
  • ipp 包含“指向ip1 的指针”

为什么ipp 仍然指向ip1 而不是ip2

当您分配给它时,变量会发生变化。计算作业;变量的变化不能多于赋值!您首先分配给ijip1ip2ipp。然后分配给*ipp,正如我们所见,这与“分配给ip1”的含义相同。由于您没有第二次分配给ipp,所以它没有改变!

如果您想更改ipp,那么您必须实际分配给ipp

ipp = &ip2;

例如。

【讨论】:

    【解决方案4】:

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

    #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\n", &i);
        printf("address of value j: %p\n", &j);
        printf("value ip1: %p\n", ip1);
        printf("value ip2: %p\n", ip2);
        printf("value ipp: %p\n", ipp);
        printf("address value of ipp: %p\n", *ipp);
        printf("value of address value of ipp: %d\n", **ipp);
        *ipp = ip2;
        printf("value ipp: %p\n", ipp);
        printf("address value of ipp: %p\n", *ipp);
        printf("value of address value of ipp: %d\n", **ipp);
    }
    

    它输出:

    【讨论】:

      【解决方案5】:

      我非常个人的看法是带有箭头的图片指向这个方向或使指针更难理解。它确实使它们看起来像一些抽象的、神秘的实体。他们不是。

      与计算机中的其他所有内容一样,指针是数字。 “指针”这个名字只是“包含地址的变量”的一种花哨方式。

      因此,让我解释一下计算机的实际工作原理。

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

      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=&amp;i;(“将 i 的地址复制到 ip1”)和ip2=&amp;j。两行之间发生的是:

      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 = &amp;ip1; 添加另一层间接性。同样,我们只是获得了一块内存:

      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。无论哪种情况,我们都会得到

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

      (这些示例是针对大端 CPU 给出的)

      【讨论】:

      • 虽然我同意你的观点,但将指针视为抽象的、神秘的实体是有价值的。指针的任何特定实现都只是数字,但您所描绘的实现策略并不是实现的要求,它只是一种通用策略。指针不必与 int 大小相同,指针不必是平面虚拟内存模型中的地址,等等;这些只是实现细节。
      • @EricLippert 我认为可以通过不使用实际内存地址或数据块来使这个示例更加抽象。如果它是一个表,说明类似location, value, variable 的内容,其中位置是1,2,3,4,5,值是A,1,B,C,3,则指针的相应概念可以很容易地解释,而无需使用箭头,这本质上是混乱的。无论选择何种实现,一个值都存在于某个位置,这是使用箭头建模时变得模糊的难题。
      • @EricLippert 根据我的经验,大多数在理解指针方面有问题的准 C 程序员,都是那些接受了抽象的人工模型的人。抽象没有有用,因为今天 C 语言的全部目的是它接近硬件。如果您正在学习 C 但不打算编写接近硬件的代码,那么您是在浪费时间。如果您不想了解计算机是如何工作的,而只想进行高级编程,那么 Java 等是一个更好的选择。
      • @EricLippert 是的,可能存在各种晦涩的指针实现,其中指针不一定对应于地址。但是画箭头也不能帮助你理解它们是如何工作的。在某些时候,您必须放弃抽象思维并进入硬件级别,否则您不应该使用 C。有许多更合适的现代语言旨在用于纯粹的抽象高级编程。
      • @Lundin:我也不是箭头图的忠实粉丝。箭头作为数据的概念是一个棘手的概念。我更喜欢抽象地思考它,但没有箭头。变量上的&amp; 运算符为您提供代表该变量的硬币。该硬币上的* 运算符会返回变量。不需要箭头!
      【解决方案6】:

      注意分配:

      ipp = &ip1;
      

      结果ipp 指向ip1

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

      ipp = &ip2;
      

      我们显然没有这样做。相反,我们正在更改ipp 指向的地址值
      通过执行以下操作

      *ipp = ip2;
      

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

      ipp = &amp;ip1,表示*ipp = ip1 = &amp;i
      现在,*ipp = ip2 = &amp;j.
      所以,*ipp = ip2 本质上与ip1 = ip2 相同。

      【讨论】:

        【解决方案7】:
        ipp = &ip1;
        

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

        您对*ipp 所做的事情,即ip1,不会改变ipp 指向ip1 的事实。

        【讨论】:

          【解决方案8】:

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

          你放了漂亮的图片,我会尝试制作漂亮的 ascii 艺术:

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

          因此,由于指针是关于处理内存的,因此真正使其“有形”的最佳方式是显示指针代数对内存的作用。

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

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

          当您编写初始代码时:

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

          你的记忆是这样的:

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

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

          然后你声明并定义ipp 如:

          int **ipp = &ip1;
          

          这是你的记忆:

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

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

          *ipp = ip2;
          

          程序的内存是

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

          注意:由于int* 是一种特殊类型,我更喜欢始终避免在同一行声明多个指针,因为我认为int *x;int *x, *y; 表示法可能会产生误导。我更喜欢写int* x; int* y;

          HTH

          【讨论】:

          • 以您的示例为例,ip2 的初始值应为3 而不是4
          • 哦,我刚刚更改了内存,使其与声明的顺序相匹配。我想我解决了这个问题?
          【解决方案9】:

          因为当你说

          *ipp = ip2
          

          您是说“ipp 指向的对象”指向ip2 指向的内存方向。

          你不是说ipp指向ip2

          【讨论】:

            【解决方案10】:

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

            例子:

            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`
            

            因此:

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

            【讨论】:

              【解决方案11】:

              如果你想让ipp 指向ip2,你必须说ipp = &amp;ip2;。但是,这会使ip1 仍然指向i

              【讨论】:

                【解决方案12】:

                一开始你就设置好了,

                ipp = &ip1;
                

                现在取消引用它,

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

                【讨论】:

                  【解决方案13】:

                  考虑如下表示的每个变量:

                  type  : (name, adress, value)
                  

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

                  int   : ( i ,  &i , 5 ); ( j ,  &j ,  6); ( k ,  &k , 5 )
                  
                  int*  : (ip1, &ip1, &i); (ip1, &ip1, &j)
                  
                  int** : (ipp, &ipp, &ip1)
                  

                  由于ipp 的值为&amp;ip1,所以指令:

                  *ipp = ip2;
                  

                  将地址&amp;ip1处的值更改为ip2的值,即ip1被更改:

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

                  ipp 仍然:

                  (ipp, &ipp, &ip1)
                  

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

                  【讨论】:

                    【解决方案14】:

                    因为您正在更改*ipp 的指针。这意味着

                    1. ipp(变量名)----进去吧。
                    2. ipp里面是ip1的地址。
                    3. 现在*ipp 所以去(内部地址)ip1

                    现在我们在ip1*ipp(即ip1) = ip2.
                    ip2包含j的地址。所以ip1的内容将被替换为ip2的包含(即j的地址), 我们不会更改ipp 内容。 而已。

                    【讨论】:

                      【解决方案15】:

                      *ipp = ip2; 暗示:

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

                      ip1 = ip2;
                      

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

                      ipp = &ip2;
                      

                      现在ipp 指向ip2

                      【讨论】:

                        【解决方案16】:

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

                        ipp = &ip2;  
                        

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

                        Wiki 说:
                        * 运算符是对指针变量进行操作的解引用运算符,并返回与指针地址处的值等效的 l-value(变量)。这称为取消引用指针。

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

                        ipp = &ip1;
                        

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

                         *ipp = ip2;  
                        

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

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

                        【讨论】:

                          猜你喜欢
                          • 2016-06-09
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 2019-06-11
                          • 2016-02-20
                          • 1970-01-01
                          • 2013-08-16
                          相关资源
                          最近更新 更多