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

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

14 ответов

Давайте предположим, что 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 быть указателем на (только для чтения) строку "привет" и, таким образом, содержит значение 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 *,
  • Много других причин, которые слишком многочисленны, чтобы перечислять здесь.

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

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

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

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

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

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

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

*pp =  entry->next

https://stackru.com/images/6f875a0fc935f68186b3e5e63d40bdd05d156318.gif

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Диаграмма с двумя указателями

Согласно рисунку, 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

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

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

http://computer.howstuffworks.com/c32.htm;;

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

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

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

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

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

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

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

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

dpointer[5][6]

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

Как это работает: это переменная, которая может хранить другой указатель.

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

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

Я создал 5-минутное видео, которое объясняет, как работают указатели:

https://www.youtube.com/watch?v=3X-ray3tDjQ

ведра указателя

Там очень много полезных объяснений, но я не нашел просто краткое описание, так что..

В основном указатель является адресом переменной. Краткий сводный код:

     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 и т. Д.

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

Другие вопросы по тегам