指针在C中如何工作?


171

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


43
不,不是功课。...只是想知道..coz,当我阅读C代码时,我会看到很多。

1
指针并不是某种特殊情况,所以我不了解您对void **的不了解。
akappa,

对于2D数组,最好的示例是在命令行中将参数args“ prog arg1 arg2”存储为char ** argv。如果不想要分配内存的调用者(被调用的函数将分配内存)
resultsway

1
您在Git 2.0中有一个“指针指向”用法的好例子:请参阅下面的答案
VonC 2014年

Answers:


359

假设一个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中。


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

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

7
是的,很好的例子..我了解它们是什么..但是如何以及何时使用它们更重要..现在..

2
Stephan基本上很好地再现了Kernighan&Richie的The C Programming Language中的图表。如果您正在使用C进行编程,而没有这本书,并且对纸质文档很满意,我强烈建议您使用它,(相当)适度的费用将很快收回成本。在其示例中,它往往非常清楚。
J. Polfer,2009年

4
char * c =“ hello”应该是const char * c =“ hello”。同样,说“将数组存储为第一个元素的地址”最多也是一种误导。数组存储为...数组。通常,其名称会产生指向其第一个元素的指针,但并非总是如此。关于指针的指针,我只想简单地说一下,当函数必须修改作为参数传递的指针(然后将指针传递给指针)时,它们很有用。
巴斯蒂安·莱纳德(BastienLéonard)2009年

4
除非我对这个答案有误解,否则它看起来是错误的。c存储在58处并指向63,cp存储在55处并指向58,并且cpp在图中未示出。
塔纳托斯(Thanatos),2009年

1
看起来挺好的。阻止我说的不是一个小问题:很棒的帖子。解释本身很好。改为投票。(也许stackoverflow需要检查指针?)
Thanatos,

46

指向指针的指针在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指针的指针,其中每个元素都包含一个作为命令行选项的char数组。但是,当您使用指针的指针指向二维数组时,一定要小心,最好使用指向二维数组的指针。

为什么很危险?

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支持可变数量的元素,则不能使用指向二维数组的指针。但是当您事先知道时,您将使用二维数组。


32

我喜欢在Git 2.0中提交指针使用情况的“现实世界”代码示例指针,提交7b1004b

莱纳斯曾经说过:

我实际上希望更多的人理解真正的核心低级编码。不是很大的,复杂的东西,例如无锁名称查找,而仅仅是很好地使用了指针对指针等
。例如,我看到太多的人通过跟踪“上一个”条目来删除单链接列表条目,然后删除条目,执行类似的操作

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

每当我看到这样的代码时,我都会说“这个人不理解指针”。不幸的是,这很普遍。

了解指针的人只需使用“ 指向入口指针的指针 ”,然后使用list_head的地址对其进行初始化。然后,当他们遍历列表时,只需执行一个

*pp =  entry->next

http://i.stack.imgur.com/bpfxT.gif

应用这种简化后,即使添加2行注释,我们也将从该函数中损失7行。

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

克里斯指出,在评论 2016年视频“ Linus Torvalds的的双指针问题 ”由菲利普Buuck


kumar 在评论博客文章“ 了解指针的Linus ”中指出,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;
}

我们在上面做的是:

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

上面有一个特殊情况-迭代开始时没有上一个条目(previs NULL),因此要删除列表中的第一个条目,您必须修改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;

    pp = &entry->next;
    entry = entry->next;
}

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

另外,该线程中的某人评论说,这样做更好的原因是因为*pp = entry->next是原子的。这当然不是原子的
上面的表达式包含两个解引用运算符(*->)和一个赋值,并且这三者都不是原子的。
这是一个普遍的误解,但是可惜,C语言中几乎没有任何东西应该被认为是原子的(包括++--运算符)!



@kumar很好的参考。我已将其包含在答案中以提高知名度。
VonC 2014年

该视频对于我理解您的示例至关重要。特别是在绘制内存图并跟踪程序进度之前,我一直感到困惑(好战)。话虽如此,对我来说还是有些神秘。
克里斯(Chris)

@Chris很棒的视频,谢谢你提到它!我已将您的评论包含在答案中,以提高知名度。
VonC

14

在介绍大学编程课程的指针时,我们获得了关于如何开始学习它们的两个提示。首先是查看Binky的Pointer Fun。第二个是考虑刘易斯·卡罗尔(Lewis Carroll)的《穿越玻璃》中哈多克斯之眼

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

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

“很长,”骑士说,“但它非常非常漂亮。每个听到我唱歌的人-要么把眼泪带到他们的眼中,要么-

“否则呢?” 爱丽丝说,因为骑士突然停了下来。

“否则,事实并非如此。这首歌的名字叫“ Haddocks'Eyes”。

“哦,那是这首歌的名字,对吗?”爱丽丝试图感到感兴趣。

“不,你不明白。”骑士有点生气。“这就是名字的名字。这个名字真的叫“老年男人”。

“那我应该说'这就是那首歌'吗?” 爱丽丝纠正了自己。

“不,你不应该:那是另一回事!这首歌叫做“ Ways And Means”:但这只是它的名字,你知道的!

“那么,那首歌是什么?” 爱丽丝说,那时他已经完全困惑了。

骑士说:“我来就是这样。” “这首歌的确是'坐在门上':这首歌是我自己的发明。”


1
我不得不读过这段经文……+1让我思考!
鲁宾·斯坦斯

这就是为什么刘易斯·卡洛尔不是普通作家。
metarose

1
所以...会这样吗?名称->“老年的老人”->叫->“哈多克的眼睛”->歌曲->“坐在一
扇门上


7

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

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


很好地解释了“为什么”的一部分
林蛙深


7

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

双指针图

如图所示,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

5

它是指向指针地址值的指针。(我知道那太可怕了)

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

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

抱歉,我知道那很糟糕。尝试阅读,呃,这:codeproject.com/KB/cpp/PtrToPtr.aspx
卢克·谢弗

5

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

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


3

指针的指针是指针的指针。

二维数组的一个有意义的示例是一个二维数组:您有一个数组,其中充满了指向其他数组的指针,因此在编写时

dpointer [5] [6]

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


2
指针的指针不应与rank2的数组混淆,例如int x [10] [10],在其中写入x [5] [6]时,您可以访问数组中的值。
皮特·柯坎

这仅是使用void **的示例。指向指针的指针仅仅是指向一个指针的指针。
2009年

1

工作原理:这是一个可以存储另一个指针的变量。

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

//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;
}


0

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

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

     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); 

在主题“意味着引用和取消引用”中也可以找到有用的信息。

而且我不确定何时可以使用指针,但是通常来说,在执行一些手动/动态内存分配(malloc,calloc等)时有必要使用它们

因此,我希望它也有助于澄清问题所在:)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.