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


How do pointer to pointers work in C?

如何指针指针在C中工作?
你什么时候使用它们?


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

1
2
3
4
  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"那么,

1
const char *c ="hello";

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

1
const char **cp = &c;

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

1
const char ***cpp = &cp;

现在cpp存储cp的地址。因此它具有值55(基于上面的示例),并且您猜对了它:它本身存储在地址60处。

至于为什么一个人使用指针指针:

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


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

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

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

你什么时候使用它们?

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

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
}

你这样称呼它:

1
2
3
4
5
6
7
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指针的指针,其中每个元素都包含一个chars数组,这些字符是命令行选项。你必须小心,但是当你使用指针指针指向2维数组时,最好使用指向2维数组的指针。

它为什么危险?

1
2
3
4
5
6
7
8
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)
}

以下是正确完成2维数组指针的示例:

1
int (*myPointerTo2DimArray)[ROWS][COLUMNS]

如果要为ROWS和COLUMNS支持可变数量的元素,则不能使用指向2维数组的指针。但是当你事先知道时,你会使用二维数组。


我喜欢这个指向指针用法的"真实世界"代码示例,在Git 2.0中,提交7b1004b:

Linus once said:

I actually wish more people understood the really core low-level kind of coding. Not big, complex stuff like the lockless name lookup, but simply good use of pointers-to-pointers etc.
For example, I've seen too many people who delete a singly-linked list entry by keeping track of the"prev" entry, and then to delete the entry, doing something like

1
2
3
4
if (prev)
  prev->next = entry->next;
else
  list_head = entry->next;

and whenever I see code like that, I just go"This person doesn't understand pointers". And it's sadly quite common.

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

1
*pp =  entry->next

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

Applying that simplification lets us lose 7 lines from this function even while adding 2 lines of comment.

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

克里斯在对菲利普·巴克的2016年视频"Linus Torvalds的双指针问题"的评论中指出。

kumar在博客文章"Linus on Understanding Pointers"的评论中指出,Grisha Trubetskoy解释说:

Imagine you have a linked list defined as:

1
2
3
4
typedef struct list_entry {
    int val;
    struct list_entry *next;
} list_entry;

You need to iterate over it from the beginning to end and remove a specific element whose value equals the value of to_remove.
The more obvious way to do this would be:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

What we are doing above is:

  • iterating over the list until entry is NULL, which means we’ve reached the end of the list (line 4).
  • When we come across an entry we want removed (line 5),

    • we assign the value of current next pointer to the previous one,
    • thus eliminating the current element (line 7).

There is a special case above - at the beginning of the iteration there is no previous entry (prev is NULL), and so to remove the first entry in the list you have to modify head itself (line 9).

What Linus was saying is that the above code could be simplified by making the previous element a pointer to a pointer rather than just a pointer.
The code then looks like this:

1
2
3
4
5
6
7
8
9
10
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;
}

The above code is very similar to the previous variant, but notice how we no longer need to watch for the special case of the first element of the list, since pp is not NULL at the beginning. Simple and clever.

Also, someone in that thread commented that the reason this is better is because *pp = entry->next is atomic. It is most certainly NOT atomic.
The above expression contains two dereference operators (* and ->) and one assignment, and neither of those three things is atomic.
This is a common misconception, but alas pretty much nothing in C should ever be assumed to be atomic (including the ++ and -- operators)!


在涵盖大学编程课程的指针时,我们得到了两个关于如何开始学习它们的提示。第一个是查看Pointer Fun With Binky。第二个是考虑刘易斯卡罗尔穿越镜子的黑道'之眼

"You are sad," the Knight said in an anxious tone:"Let me sing you a song to comfort you."

"Is it very long?" Alice asked, for she had heard a good deal of poetry that day.

"It's long," said the Knight,"but it's very, very beautiful. Everybody that hears me sing it - either it brings the tears to their eyes, or else -"

"Or else what?" said Alice, for the Knight had made a sudden pause.

"Or else it doesn't, you know. The name of the song is called ‘Haddocks' Eyes.’"

"Oh, that's the name of the song, is it?" Alice said, trying to feel interested.

"No, you don't understand," the Knight said, looking a little vexed."That's what the name is called. The name really is ‘The Aged Aged Man.’"

"Then I ought to have said ‘That's what the song is called’?" Alice corrected herself.

"No, you oughtn't: that's quite another thing! The song is called ‘Ways And Means’: but that's only what it's called, you know!"

"Well, what is the song, then?" said Alice, who was by this time completely bewildered.

"I was coming to that," the Knight said."The song really is ‘A-sitting On A Gate’: and the tune's my own invention."


你可能想读这个:指针指针

希望这有助于澄清一些基本的疑虑。


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

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


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

它经常用在内存受限的环境中,即Palm OS。

computer.howstuffworks.com Link>>

www.flippinbits.com Link>>


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

Double pointer diagram

根据该图,ptr1是一个单指针,它具有变量num的地址。

1
ptr1 = #

类似地,ptr2是指向指针(双指针)的指针,它具有指针ptr1的地址。

1
ptr2 = &ptr1;

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

上图中的值:

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

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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
"
, num );
   printf("Value available at *ptr1 = %d
"
, *ptr1 );
   printf("Value available at **ptr2 = %d
"
, **ptr2);
}

输出:

1
2
3
Value of num = 10
Value available at *ptr1 = 10
Value available at **ptr2 = 10

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

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


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

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

1
2
3
4
void changeptr(int** pp)
{
  *pp=&someval;
}


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

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

dpointer [5] [6]

你在包含指向其第五个位置的其他数组的指针的数组中访问,获取指针(让fpointer他的名字),然后访问引用该数组的数组的第6个元素(所以,fpointer [6])。


我创建了一个5分钟的视频来解释指针是如何工作的:

pointer buckets


这个怎么运作:
它是一个可以存储另一个指针的变量。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//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
"
, i[j] );

    return 0;
}

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

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

1
2
3
4
5
6
7
8
     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等。

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