Как указатель на указатели работает в C? - PullRequest
160 голосов
/ 22 мая 2009

Как работают указатели на указатели в C? Когда бы вы их использовали?

Ответы [ 14 ]

341 голосов
/ 22 мая 2009

Давайте предположим, что 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 начинается строка "привет". Таким образом, в этом случае, если это единственное появление «привет» в памяти, то

const char *c = "hello";

... определяет c как указатель на (только для чтения) строку "hello" и, таким образом, содержит значение 63. c должен быть сам где-то сохранен: в примере выше в местоположении 58. Конечно, мы можем указывать не только на персонажей, но и на другие указатели. E.g.:

const char **cp = &c;

Теперь cp указывает на c, то есть содержит адрес c (58). Мы можем пойти еще дальше. Рассмотрим:

const char ***cpp = &cp;

Теперь cpp хранит адрес cp. Таким образом, оно имеет значение 55 (на основе приведенного выше примера), и вы уже догадались: оно само хранится по адресу 60.


Что касается , почему каждый использует указатели на указатели:

  • Имя массива обычно дает адрес его первого элемента. Поэтому, если массив содержит элементы типа t, ссылка на массив имеет тип t *. Теперь рассмотрим массив массивов типа t: естественно, ссылка на этот 2D-массив будет иметь тип (t *)* = t ** и, следовательно, будет указателем на указатель.
  • Несмотря на то, что массив строк звучит одномерным, на самом деле он двумерный, поскольку строки являются символьными массивами. Отсюда: char **.
  • Функция f должна будет принять аргумент типа t **, если она должна изменить переменную типа t *.
  • Многие другие причины, которых слишком много, чтобы перечислять здесь.
41 голосов
/ 22 мая 2009

Как работают указатели на указатели в 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

Есть и другие применения, например, аргумент main () каждой программы на C имеет указатель на указатель на argv, где каждый элемент содержит массив символов, которые являются параметрами командной строки. Вы должны быть осторожны, хотя, когда вы используете указатели указателей для указания на 2-мерные массивы, лучше вместо этого использовать указатель на 2-мерный массив.

Почему это опасно?

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. Но когда вы знаете заранее, вы бы использовали двумерный массив.

26 голосов
/ 12 марта 2014

Мне нравится этот пример кода "реального мира" указателя на использование указателя, в Git 2.0 commit 7b1004b :

Линус однажды сказал:

Я бы хотел, чтобы больше людей понимали действительно базовый низкоуровневый код. Не большие, сложные вещи, такие как поиск по имени без блокировки, но просто хорошее использование указателей на указатели и т. Д.
Например, я видел слишком много людей, которые удаляли односвязную запись списка, отслеживая запись «prev», а затем удаляли запись, выполняя что-то вроде

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

и всякий раз, когда я вижу такой код, я просто говорю: «Этот человек не понимает указателей». И это, к сожалению, довольно часто.

Люди, которые понимают указатели, просто используют " указатель на указатель ввода " и инициализируют его адресом list_head. И затем, проходя по списку, они могут удалить запись без использования каких-либо условий, просто выполнив команду

*pp =  entry->next

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

Применение этого упрощения позволяет нам потерять 7 строк этой функции даже при добавлении 2 строк комментария.

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

Крис указывает в комментариях к видео 2016 года " Проблема двойного указателя Линуса Торвальдса " Филип Буак .


kumar указывает в комментариях сообщение в блоге " Линус о понимании указателей ", где Гриша Трубецкой объясняет:

Представьте, что у вас есть связанный список, определенный как:

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

Существует особый случай выше - в начале итерации нет предыдущей записи (prev is NULL), поэтому для удаления первой записи в списке необходимо изменить саму голову (строка 9 ).

Линус говорил, что приведенный выше код можно упростить, сделав предыдущий элемент указателем на указатель, а не просто указатель .
Код выглядит следующим образом:

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 не следует считать атомарным (включая операторы ++ и --)!

13 голосов
/ 07 июня 2009

При рассмотрении указателей на курс программирования в университете нам дали два совета о том, как начать изучать их. Первым было посмотреть Pointer Fun With Binky . Во-вторых, подумать о проходе Гэддокс * Глаз из «Льюиса Кэрролла» Через Зазеркалье

«Вам грустно», - сказал рыцарь с тревожным тоном: «Позвольте мне спеть вам песню, чтобы утешить вас».

«Это очень долго?» - спросила Алиса, потому что она слышала много поэзии в тот день.

«Это долго, - сказал Рыцарь, - но это очень, очень красиво. Все, кто слышит меня, поют это - или это вызывает слезы на их глазах, или же еще…

«Или что еще?» Сказала Алиса, потому что Рыцарь сделал внезапную паузу.

«Или, иначе, нет. Название песни называется «Глаза пикши».

«О, это название песни, не так ли?» Сказала Алиса, пытаясь почувствовать интерес.

«Нет, ты не понимаешь», сказал Рыцарь, выглядя немного недовольным. «Так называется это имя. На самом деле его зовут «Старец в возрасте».

«Тогда мне следовало сказать« Так называется песня »?» - поправилась Алиса.

«Нет, ты не должен: это совсем другое дело! Песня называется «Ways And Means», но это только то, как она называется, вы знаете! »

«Ну, что же тогда за песня?» - спросила Алиса, которая к этому времени была совершенно сбита с толку.

«Я шел к этому», сказал Рыцарь. «Эта песня действительно звучит« сидя на воротах », а мелодия - мое собственное изобретение»

11 голосов
/ 22 мая 2009

Вы можете прочитать это: Указатели на указатели

Надеюсь, это поможет прояснить некоторые основные сомнения.

7 голосов
/ 22 мая 2009

Когда требуется ссылка на указатель. Например, когда вы хотите изменить значение (указанный адрес) переменной-указателя, объявленной в области видимости вызывающей функции внутри вызываемой функции.

Если вы передадите один указатель в качестве аргумента, вы будете изменять локальные копии указателя, а не исходный указатель в области вызова. С указателем на указатель вы модифицируете последний.

7 голосов
/ 22 мая 2009

Указатель на указатель также называется handle . Часто его используют, когда объект можно переместить в память или удалить. За блокировку и разблокировку использования объекта часто отвечает один, поэтому он не будет перемещен при доступе к нему.

Он часто используется в среде с ограниченным объемом памяти, т.е. в Palm OS.

computer.howstuffworks.com Ссылка >>

www.flippinbits.com Ссылка >>

6 голосов
/ 01 сентября 2016

Рассмотрим приведенный ниже рисунок и программу, чтобы лучше понять эту концепцию .

Double pointer diagram

Согласно рисунку, 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 голосов
/ 22 мая 2009

У вас есть переменная, которая содержит адрес чего-либо. Это указатель.

Тогда у вас есть другая переменная, которая содержит адрес первой переменной. Это указатель на указатель.

4 голосов
/ 22 мая 2009

это указатель на значение адреса указателя. (это ужасно, я знаю)

в основном, это позволяет вам передавать указатель на значение адреса другого указателя, так что вы можете изменить, куда указывает другой указатель из подфункции, например:

void changeptr(int** pp)
{
  *pp=&someval;
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...