Похоже, что ваш настоящий вопрос таков:
Какой вариант использования указателя на указатель?
Указатель на указатель имеет тенденцию обнаруживаться, когда имеет массив некоторого типа T, а сам T является указателем на что-то еще. Например,
- Что за строка в C? Как правило, это
char *
.
- Хотели бы вы время от времени массив строк? Конечно.
- Как бы вы объявили один?
char *x[10]
: x
- это массив из 10 указателей на char
, он же 10 строк.
В этот момент вам может быть интересно, куда входит char **
. Он входит в картину из очень тесной связи между арифметикой указателей и массивами в C. Имя массива, x
(почти) всегда преобразуется в указатель на его первый элемент.
- Какой первый элемент? A
char *
.
- Какой указатель на первый элемент? A
char **
.
В C массив E1[E2]
определен равным *(E1 + E2)
. Обычно E1
- это имя массива, скажем, x
, которое автоматически преобразуется в char **
, а E2
- это некоторый индекс, скажем 3. (Это правило также объясняет, почему 3[x]
и x[3]
тоже самое.)
Указатели на указатели также отображаются, когда требуется динамически размещенный массив некоторого типа T
, который сам по себе является указателем . Для начала давайте представим, что мы не знаем, что такое тип Т.
- Если нам нужен динамически распределенный вектор T, какой тип нам нужен?
T *vec
.
- Почему? Поскольку мы можем выполнять арифметику указателей в C, любой
T *
может служить основой непрерывной последовательности T
в памяти.
- Как мы можем выделить этот вектор, скажем, из
n
элементов? vec = malloc(n * sizeof(T))
;
Эта история верна для абсолютно любого типа T
, и поэтому это верно для char *
.
- Какой тип
vec
, если T
равен char *
? char **vec
.
Указатели на указатели также отображаются, когда у вас есть функция, которая должна изменить аргумент типа T, сама указатель .
- Посмотрите на объявление для
strtol
: long strtol(char *s, char **endp, int b)
.
- О чем это все?
strtol
преобразует строку из базы b
в целое число. Он хочет сказать вам, как далеко в строку он попал. Возможно, он может вернуть структуру, содержащую long
и char *
, но это не так, как объявлено.
- Вместо этого он возвращает свой второй результат, передавая адрес строки, которую он изменяет перед возвратом.
- Что за строка? Ах да,
char *
.
- Так, каков адрес строки?
char **
.
Если вы достаточно долго бродите по этому пути, вы также можете столкнуться с типами T ***
, хотя вы почти всегда можете реструктурировать код, чтобы избежать их.
Наконец, указатели на указатели появляются в некоторых хитрых реализациях связанных списков . Рассмотрим стандартное объявление двусвязного списка в C.
struct node {
struct node *next;
struct node *prev;
/* ... */
} *head;
Это работает нормально, хотя я не буду воспроизводить здесь функции вставки / удаления, но у него есть небольшая проблема. Любой узел можно удалить из списка (или вставить перед ним новый узел) без ссылки на заголовок списка. Ну, не совсем любой узел. Это не относится к первому элементу списка, где prev
будет нулевым. Это может быть немного раздражающим в некоторых видах кода на C, когда вы больше работаете с самими узлами, чем со списком в качестве концепции. Это достаточно распространенное явление в низкоуровневом системном коде.
Что если мы переписаем node
так:
struct node {
struct node *next;
struct node **prevp;
/* ... */
} *head;
В каждом узле prevp
указывает не на предыдущий узел, а на указатель next
предыдущих узлов. Как насчет первого узла? Это prevp
очков на head
. Если вы вытянете такой список (а у вас есть , чтобы нарисовать его, чтобы понять, как это работает), вы увидите, что вы можете удалить первый элемент или вставить новый узел перед первым элементом без явного ссылка head
по имени.