Как указатель на указатели работает в C?

171

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

размотать
источник
43
Нет, не домашнее задание .... просто хотел знать .. потому что я часто вижу это, когда читаю код на Си.
1
Указатель на указатель не является частным случаем чего-либо, поэтому я не понимаю, что вы не понимаете в void **.
akappa
для двумерных массивов лучшим примером является командная строка args "prog arg1 arg2" сохраняется char ** argv. И если вызывающая сторона не хочет выделять память (вызываемая функция выделяет память)
результаты
1
У вас есть хороший пример использования «указатель на указатель» в Git 2.0: см. Мой ответ ниже
VonC

Ответы:

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

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: естественно, ссылка на этот 2D-массив будет иметь тип (t *)*= t **и, следовательно, является указателем на указатель.
  • Несмотря на то, что массив строк звучит одномерно, на самом деле он двумерный, поскольку строки являются символьными массивами. Следовательно: char **.
  • Функция fдолжна будет принимать аргумент типа, t **если она должна изменить переменную типа t *.
  • Много других причин, которые слишком многочисленны, чтобы перечислять здесь.
Stephan202
источник
7
да хороший пример .. я понимаю, что они есть .. но как и когда их использовать, важнее .. сейчас ..
2
Стефан проделал хорошую работу, воспроизведя, в основном, диаграмму на языке программирования C на языке Керниган и Ричи. Если вы программируете на C, у вас нет этой книги, и вы хорошо знакомы с бумажной документацией, я настоятельно рекомендую вам ее получить (довольно) скромные расходы очень быстро окупятся в производительности. Это имеет тенденцию быть очень ясным в своих примерах.
Дж. Полфер
4
char * c = "hello" должно быть const char * c = "hello". Кроме того, ошибочно говорить, что «массив хранится как адрес первого элемента». Массив хранится как ... массив. Часто его имя дает указатель на его первый элемент, но не всегда. Что касается указателей на указатели, я бы просто сказал, что они полезны, когда функция должна модифицировать указатель, переданный в качестве параметра (вместо этого вы передаете указатель на указатель).
Бастьен Леонар
4
Если я не неверно истолковал этот ответ, он выглядит неправильно. c хранится в 58 и указывает на 63, cp хранится в 55 и указывает на 58, а cpp не представлен на диаграмме.
Танатос
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

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

Брайан Р. Бонди
источник
32

Мне нравится этот пример кода "реального мира" указателя на использование указателя, в 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 года « Проблема двойного указателя Линуса Торвальдса » Филипа Буака .


Кумар отмечает в комментариях пост в блоге « Линус о понимании указателей », где Гриша Трубецкой объясняет:

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

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

VonC
источник
4
Это поможет лучше понять - grisha.org/blog/2013/04/02/linus-on-understanding-pointers
kumar
@kumar хорошая ссылка. я включил его в ответ для большей наглядности.
VonC
Это видео было важно для понимания вашего примера. В частности, я чувствовал растерянность (и воинственность), пока не нарисовал диаграмму памяти и не проследил за ходом программы. Тем не менее, это все еще кажется мне несколько загадочным.
Крис
@Chris Отличное видео, спасибо, что упомянули об этом! Я включил ваш комментарий в ответ для большей наглядности.
VonC
14

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

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

"Это очень долго?" Спросила Алиса, потому что она слышала много стихов в тот день.

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

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

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

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

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

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

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

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

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

Эдд
источник
1
Я должен был прочитать этот отрывок пару раз ... +1 за то, что заставил меня задуматься!
Рубен Стейнс
Вот почему Льюис Кэрролл не обычный писатель.
метароза
1
Так ... это будет так? name -> 'The Aged Aged Man' -> звонил -> 'Haddock's Eyes' -> song -> 'A-
Sitting
7

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

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

Алекс Балашов
источник
Хорошо объяснил для части «Почему»
Rana Deep
7

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

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

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

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

epatel
источник
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

Указатель на указатель это, ну, указатель на указатель.

Значительным примером someType ** является двумерный массив: у вас есть один массив, заполненный указателями на другие массивы, поэтому, когда вы пишете

dpointer [5] [6]

Вы получаете доступ к массиву, который содержит указатели на другие массивы в его 5-й позиции, получаете указатель (пусть fpointer его имя), а затем получаете доступ к 6-му элементу массива, на который ссылается этот массив (так, fpointer [6]).

akappa
источник
2
указатели на указатели не следует путать с массивами ранга 2, например, int x [10] [10], где вы пишете x [5] [6], вы получаете доступ к значению в массиве.
Пит Киркхам
Это всего лишь пример, где пустота ** подходит. Указатель на указатель - это только указатель, который указывает на указатель.
akappa
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;
}
resultsway
источник
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 и т. Д.

Поэтому я надеюсь, что это также поможет прояснить проблему :)

xxxvodnikxxx
источник