【问题标题】:How do pointer-to-pointers work in C? (and when might you use them?)指针在 C 中是如何工作的? (你什么时候可以使用它们?)
【发布时间】:2010-10-28 04:42:54
【问题描述】:

指针指向指针在 C 中如何工作?
您什么时候可以使用它们?

【问题讨论】:

  • 不,不是作业......只是想知道......因为我在阅读 C 代码时看到了很多。
  • 指向指针的指针不是某事的特例,所以我不明白你对 void** 的不理解。
  • 对于 2D 数组,最好的例子是命令行 args “prog arg1 arg2”被存储为 char**argv。如果调用者不想分配内存(被调用函数会分配内存)
  • 您在 Git 2.0 中有一个很好的“指向指针”用法示例:请参阅 my answer below

标签: c pointers


【解决方案1】:

让我们假设一台具有 8 位地址的 8 位计算机(因此只有 256 字节的内存)。这是该内存的一部分(顶部的数字是地址):

  54   55   56   57   58   59   60   61   62   63   64   65   66   67   68   69
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
|    | 58 |    |    | 63 |    | 55 |    |    | h  | e  | l  | l  | o  | \0 |    |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+

您可以在这里看到,在地址 63 处,字符串“hello”开始。所以在这种情况下,如果这是内存中唯一出现的“hello”,那么,

const char *c = "hello";

... 将 c 定义为指向(只读)字符串“hello”的指针,因此包含值 63。c 本身必须存储在某个位置:在上面的示例中位于位置 58。当然我们不仅可以指向字符,还可以指向其他指针。例如:

const char **cp = &c;

现在cp指向c,即包含c的地址(即58)。我们可以走得更远。考虑:

const char ***cpp = &cp;

现在cpp 存储cp 的地址。所以它的值是 55(基于上面的例子),你猜对了:它本身存储在地址 60。


至于为什么使用指向指针的指针:

  • 数组的名称通常产生其第一个元素的地址。因此,如果数组包含t 类型的元素,则对数组的引用具有t * 类型。现在考虑一个类型为t 的数组数组:对这个二维数组的引用自然会有(t *)* = t ** 类型,因此是指向指针的指针。
  • 尽管字符串数组听起来是一维的,但实际上它是二维的,因为字符串是字符数组。因此:char **
  • 如果要更改 t * 类型的变量,函数 f 将需要接受 t ** 类型的参数。
  • 许多其他原因不胜枚举。

【讨论】:

  • 是的好例子..我了解它们是什么..但是如何以及何时使用它们更重要..现在..
  • Stephan 很好地复制了 Kernighan & Richie 的 The C Programming Language 中的图表。如果您正在编写 C 语言,并且没有这本书并且对纸质文档很感兴趣,我强烈建议您购买它,(相当)适度的费用将很快在生产力方面收回成本。在其示例中往往非常清楚。
  • char* c = "hello" 应该是 const char* c = "hello"。此外,说“数组存储为第一个元素的地址”最多也是一种误导。数组存储为...数组。通常它的名字会产生一个指向它的第一个元素的指针,但并非总是如此。关于指向指针的指针,我只想说,当函数必须修改作为参数传递的指针(然后您将指针传递给指针)时,它们很有用。
  • 除非我误解了这个答案,否则它看起来是错误的。 c存放在58指向63,cp存放在55指向58,cpp在图中没有表示。
  • 看起来不错。比小问题是阻止我说:伟大的职位。解释本身非常好。改为赞成票。 (也许 stackoverflow 需要审查指针?)
【解决方案2】:

指向指针的指针在 C 中如何工作?

首先,指针是一个变量,与任何其他变量一样,但它保存变量的地址。

指向指针的指针是一个变量,与任何其他变量一样,但它保存变量的地址。该变量恰好是一个指针。

您什么时候使用它们?

当您需要返回指向堆上某些内存的指针时可以使用它们,但不使用返回值。

例子:

int getValueOf5(int *p)
{
  *p = 5;
  return 1;//success
}

int get1024HeapMemory(int **p)
{
  *p = malloc(1024);
  if(*p == 0)
    return -1;//error
  else 
    return 0;//success
}

你这样称呼它:

int x;
getValueOf5(&x);//I want to fill the int varaible, so I pass it's address in
//At this point x holds 5

int *p;    
get1024HeapMemory(&p);//I want to fill the int* variable, so I pass it's address in
//At this point p holds a memory address where 1024 bytes of memory is allocated on the heap

还有其他用途,例如每个 C 程序的 main() 参数都有一个指向 argv 指针的指针,其中每个元素都包含一个字符数组,这些字符是命令行选项。您必须小心,但当您使用指针的指针指向二维数组时,最好使用指向二维数组的指针。

为什么它很危险?

void test()
{
  double **a;
  int i1 = sizeof(a[0]);//i1 == 4 == sizeof(double*)

  double matrix[ROWS][COLUMNS];
  int i2 = sizeof(matrix[0]);//i2 == 240 == COLUMNS * sizeof(double)
}

这是一个正确完成的指向二维数组的指针示例:

int (*myPointerTo2DimArray)[ROWS][COLUMNS]

如果您想为 ROWS 和 COLUMNS 支持可变数量的元素,则不能使用指向二维数组的指针。但是,如果您事先知道,您将使用二维数组。

【讨论】:

    【解决方案3】:

    我喜欢这个在 Git 2.0 中使用指针的“真实世界”代码示例,commit 7b1004b

    Linus 曾经说过:

    我真的希望更多的人了解真正核心的低级编码。不是大而复杂的东西,比如无锁名称查找,但只是很好地使用了指针到指针等。
    例如,我见过太多人通过跟踪“prev”条目来删除单链表条目,然后删除该条目,执行以下操作:

    
       if (prev)
         prev->next = entry->next;
       else
         list_head = entry->next;
    

    每当我看到这样的代码时,我都会说“这个人不懂指针”。遗憾的是,这很常见。

    了解指针的人只需使用“指向入口指针的指针”,并使用 list_head 的地址对其进行初始化。然后当他们遍历列表时,他们可以在不使用任何条件的情况下删除条目,只需执行一个

    *pp =  entry->next
    

    应用这种简化后,即使添加了 2 行注释,我们也会从这个函数中丢失 7 行代码。

    - struct combine_diff_path *p, *pprev, *ptmp;
    + struct combine_diff_path *p, **tail = &curr;
    

    Chris 在 2016 年视频“Linus Torvalds's Double Pointer Problem”中指出in the comments


    kumar 指出in the comments 的博文“Linus on Understanding Pointers”,其中Grisha Trubetskoy 解释说:

    假设你有一个链表定义为:

       typedef struct list_entry {
           int val;
           struct list_entry *next;
       } list_entry;
    

    您需要从头到尾对其进行迭代并删除其值等于 to_remove 值的特定元素。
    更明显的方法是:

       list_entry *entry = head; /* assuming head exists and is the first entry of the list */
       list_entry *prev = NULL;
       
       while (entry) { /* line 4 */
           if (entry->val == to_remove)     /* this is the one to remove ; line 5 */
               if (prev)
                  prev->next = entry->next; /* remove the entry ; line 7 */
               else
                   head = entry->next;      /* special case - first entry ; line 9 */
       
           /* move on to the next entry */
           prev = entry;
           entry = entry->next;
       }
    

    我们在上面做的是:

    • 遍历列表,直到条目为 NULL,这意味着我们已经到达列表的末尾(第 4 行)。
    • 当我们遇到要删除的条目时(第 5 行),
    • 我们将当前下一个指针的值赋给前一个,
    • 从而消除当前元素(第 7 行)。

    上面有一个特殊情况 - 在迭代开始时没有前一个条目(prevNULL),因此要删除列表中的第一个条目,您必须修改 head 本身(第 9 行)。

    Linus 的意思是可以通过使前一个元素成为指向指针的指针而不只是指针来简化上述代码
    代码如下所示:

       list_entry **pp = &head; /* pointer to a pointer */
       list_entry *entry = head;
    
       while (entry) {
           if (entry->val == to_remove)
               *pp = entry->next;
           else
                pp = &entry->next;
           entry = entry->next;
       }
    

    上面的代码与前面的变体非常相似,但请注意我们不再需要注意列表第一个元素的特殊情况,因为pp 的开头不是NULL。简单而聪明。

    此外,该线程中的某人评论说这更好的原因是因为*pp = entry->next 是原子的。 它肯定不是原子的
    上面的表达式包含两个取消引用运算符(*->)和一个赋值,这三件事都不是原子的。
    这是一个常见的误解,但遗憾的是,C 中几乎没有任何内容假定是原子的(包括++-- 运算符)!

    【讨论】:

    • @kumar 很好的参考。我已将其包含在答案中以提高知名度。
    • This video 对我理解你的例子至关重要。尤其是我感到困惑(和好战),直到我画了一张记忆图并追踪了程序的进度。话虽如此,对我来说还是有点神秘。
    • @Chris 很棒的视频,感谢您提及!我已将您的评论包含在答案中以提高知名度。
    • @VonC 此代码不起作用如果有两个或多个value(s) 等于to_remove 的条目。因为在删除第一个之后,pp = &entry->next; 在不应该执行的时候被执行。由于这个错误,pp 将结束指向下一个指针的内存位置 刚刚删除的节点 并且在删除之后,*pp = entry->next; 将写入被删除节点之后的元素的地址(entry-> next) 指向先前删除的节点 next 指针,用于切断链表中的 link(s)
    【解决方案4】:

    在介绍大学编程课程的指针时,我们得到了两个关于如何开始学习它们的提示。首先是查看Pointer Fun With Binky。第二个是考虑刘易斯卡罗尔的Through thelooking-Glass

    中的Haddocks' Eyes段落

    “你很伤心,”骑士焦急地说:“让我给你唱一首歌来安慰你。”

    “很长吗?”爱丽丝问,因为那天她听了很多诗。

    “它很长,”骑士说,“但它非常非常漂亮。每个听到我唱歌的人——要么让他们热泪盈眶,要么——”

    “不然呢?”爱丽丝说,因为骑士突然停顿了一下。

    “否则它不会,你知道的。这首歌的名字叫《黑线鳕的眼睛》。”

    “哦,这就是这首歌的名字,是吗?”爱丽丝说,试图让自己感兴趣。

    “不,你不明白,”骑士说,看起来有点恼火。 “这个名字就是这么叫的。这个名字真的是‘老人’。”

    “那我应该说‘这首歌就是这么叫的’?”爱丽丝纠正了自己。

    “不,你不应该这样做:那完全是另一回事!这首歌叫‘方法和手段’:但这只是它的名字,你知道的!”

    “那么,那首歌是什么?”爱丽丝说,此时她已经完全不知所措了。

    “我就是这么想的,”骑士说。 “这首歌真的是‘A-sitting On A Gate’:这首歌是我自己发明的。”

    【讨论】:

    • 我不得不把那段话读了几遍……+1 让我思考!
    • 这就是刘易斯·卡罗尔不是普通作家的原因。
    • 所以...会这样吗? name -> 'The Aged Aged Man' -> 叫 -> 'Haddock's Eyes' -> 歌曲 -> 'A-sitting On A Gate'
    【解决方案5】:

    Pointers to Pointers

    既然我们可以有指向 int 的指针,指向 char 的指针,指向我们定义的任何结构的指针,实际上指向 C 中的任何类型的指针,我们可以做到这一点也就不足为奇了有指向其他指针的指针。

    【讨论】:

      【解决方案6】:

      考虑下图和程序以更好地理解这个概念

      如图所示,ptr1是一个单指针,其地址为num变量。

      ptr1 = #
      

      类似地,ptr2 是一个指向指针(双指针) 的指针,它的地址是指针ptr1

      ptr2 = &ptr1;
      

      指向另一个指针的指针称为双指针。在这个例子中 ptr2 是一个双指针。

      上图中的值:

      Address of variable num has : 1000
      Address of Pointer ptr1 is: 2000
      Address of Pointer ptr2 is: 3000
      

      示例:

      #include <stdio.h>
      
      int main ()
      {
         int  num = 10;
         int  *ptr1;
         int  **ptr2;
      
         // Take the address of var 
         ptr1 = &num;
      
         // Take the address of ptr1 using address of operator &
         ptr2 = &ptr1;
      
         // Print the value
         printf("Value of num = %d\n", num );
         printf("Value available at *ptr1 = %d\n", *ptr1 );
         printf("Value available at **ptr2 = %d\n", **ptr2);
      }
      

      输出:

      Value of num = 10
      Value available at *ptr1 = 10
      Value available at **ptr2 = 10
      

      【讨论】:

        【解决方案7】:

        当需要引用指针时,使用指向指针的指针。例如,当您希望修改在被调用函数内的调用函数作用域中声明的指针变量的值(指向的地址)时。

        如果您将单个指针作为参数传入,您将修改指针的本地副本,而不是调用范围内的原始指针。使用指向指针的指针,您可以修改后者。

        【讨论】:

        • 很好地解释了“为什么”部分
        【解决方案8】:

        指向指针的指针也称为句柄。它的一种用法通常是当对象可以在内存中移动或删除时。一个人通常负责锁定和解锁 object 的使用,因此在访问它时不会移动它。

        常用于内存受限的环境,即Palm OS。

        computer.howstuffworks.com Link>>

        www.flippinbits.com Link>>

        【讨论】:

          【解决方案9】:

          它是指向指针地址值的指针。 (我知道这很糟糕)

          基本上,它允许您将一个指针传递给另一个指针的地址值,因此您可以从子函数中修改另一个指针指向的位置,例如:

          void changeptr(int** pp)
          {
            *pp=&someval;
          }
          

          【讨论】:

          【解决方案10】:

          你有一个包含某物地址的变量。那是一个指针。

          然后你有另一个变量包含第一个变量的地址。那是一个指向指针的指针。

          【讨论】:

            【解决方案11】:

            指向指针的指针就是指向指针的指针。

            someType** 的一个有意义的例子是一个二维数组:你有一个数组,里面填充了指向其他数组的指针,所以当你写的时候

            dpointer[5][6]

            您访问包含指向第 5 个位置的其他数组的指针的数组,获取指针(让 fpointer 指向他的名字),然后访问引用该数组的数组的第 6 个元素(因此,fpointer[6])。

            【讨论】:

            • 指向指针的指针不应与 rank2 的数组混淆,例如 int x[10][10] 您在其中写入 x[5][6] 您访问数组中的值。跨度>
            • 这只是一个适合使用 void** 的示例。指向指针的指针只是指向指针的指针。
            【解决方案12】:

            它是如何工作的: 它是一个可以存储另一个指针的变量。

            您什么时候使用它们: 许多用途其中之一是如果您的函数想要构造一个数组并将其返回给调用者。

            //returns the array of roll nos {11, 12} through paramater
            // return value is total number of  students
            int fun( int **i )
            {
                int *j;
                *i = (int*)malloc ( 2*sizeof(int) );
                **i = 11;  // e.g., newly allocated memory 0x2000 store 11
                j = *i;
                j++;
                *j = 12; ;  // e.g., newly allocated memory 0x2004 store 12
            
                return 2;
            }
            
            int main()
            {
                int *i;
                int n = fun( &i ); // hey I don't know how many students are in your class please send all of their roll numbers.
                for ( int j=0; j<n; j++ )
                    printf( "roll no = %d \n", i[j] );
            
                return 0;
            }
            

            【讨论】:

              【解决方案13】:

              5-minute video 解释指针的工作原理:


              【讨论】:

                【解决方案14】:

                有很多有用的解释,但我没有找到一个简短的描述,所以..

                基本上指针是变量的地址。 简短摘要代码:

                     int a, *p_a;//declaration of normal variable and int pointer variable
                     a = 56;     //simply assign value
                     p_a = &a;   //save address of "a" to pointer variable
                     *p_a = 15;  //override the value of the variable
                
                //print 0xfoo and 15 
                //- first is address, 2nd is value stored at this address (that is called dereference)
                     printf("pointer p_a is having value %d and targeting at variable value %d", p_a, *p_a); 
                

                也可以在主题What means reference and dereference中找到有用的信息

                我不太确定,指针什么时候有用,但通常在你做一些事情时有必要使用它们manual/dynamic memory allocation- malloc, calloc, etc.

                所以我希望它也有助于澄清问题:)

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2010-12-06
                  • 2020-05-13
                  • 1970-01-01
                  • 2010-10-14
                  • 2015-10-31
                  • 1970-01-01
                  • 2016-01-16
                  相关资源
                  最近更新 更多