Как указатель на указатели работает в 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
Применение этого упрощения позволяет нам потерять 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 = #
// 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.
Это указатель на значение адреса указателя. (это ужасно, я знаю)
в основном, это позволяет вам передавать указатель на значение адреса другого указателя, так что вы можете изменить, куда указывает другой указатель из подфункции, например:
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-минутное видео, которое объясняет, как работают указатели:
Там очень много полезных объяснений, но я не нашел просто краткое описание, так что..
В основном указатель является адресом переменной. Краткий сводный код:
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 и т. Д.
Поэтому я надеюсь, что это также поможет для выяснения проблемных:)