Адрес указателя в многомерном массиве C
Я возился с многомерными массивами и указателями. Я смотрел программу, которая печатает содержимое и адреса простого массива. Здесь объявление массива:
int zippo[4][2] = { {2,4},
{6,8},
{1,3},
{5,7} };
В настоящее время я понимаю, что zippo
- это указатель, и он может содержать адрес пары других указателей. По умолчанию zippo
содержит адрес указателя zippo[0]
, а также может содержать адреса указателей zippo[1]
, zippo[2]
и zippo[3]
.
Теперь сделайте следующее утверждение:
printf("zippo[0] = %p\n", zippo[0]);
printf(" *zippo = %p\n", *zippo);
printf(" zippo = %p\n", zippo);
На моей машине это дает следующий вывод:
zippo[0] = 0x7fff170e2230
*zippo = 0x7fff170e2230
zippo = 0x7fff170e2230
Я прекрасно понимаю, почему zippo[0]
и *zippo
имеют одинаковое значение. Они оба указателя, и оба они сохраняют адрес (по умолчанию) целого числа 2 или zippo[0][0]
. Но что с zippo
также разделяет один и тот же адрес памяти? Не следует ли zippo
хранить адрес указателя zippo[0]
? Whaaaat?
Ответы
Ответ 1
Когда вы объявляете многомерный массив, компилятор рассматривает его как одномерный массив. Многомерные массивы - это просто абстракция, облегчающая нашу жизнь. У вас есть недоразумение: это не один массив, указывающий на 4 массива, его всегда просто один сплошной блок памяти.
В вашем случае выполните:
int zippo[4][2]
На самом деле это то же самое, что делать
int zippo[8]
С математикой, необходимой для 2D-адресации, обработанной для вас компилятором.
Подробнее см. в этом руководстве по массивам на С++.
Это совсем не так:
int** zippo
или
int* zippo[4]
В этом случае вы создаете массив из четырех указателей, которые могут быть выделены для других массивов.
Ответ 2
Когда выражение массива появляется в большинстве контекстов, его тип неявно преобразуется из "N-element array of T" в "указатель на T", а его значение устанавливается так, чтобы указывать на первый элемент в массиве. Исключения из этого правила заключаются в том, что выражение массива является операндом операторов sizeof
или address-of (&
), или когда массив является строковым литералом, который используется в качестве инициализатора в объявлении.
Таким образом, выражение zippo
"распадается" от типа int [4][2]
(4-элементный массив из 2-х элементовных массивов int) до int (*)[2]
(указатель на 2-элементный массив int). Аналогично, тип zippo[0]
равен int [2]
, который неявно преобразуется в int *
.
Учитывая объявление int zippo[4][2]
, в следующей таблице показаны типы различных выражений массива с участием zippo и любых неявных преобразований:
Expression Type Implicitly converted to Equivalent expression
---------- ---- ----------------------- ---------------------
zippo int [4][2] int (*)[2]
&zippo int (*)[4][2]
*zippo int [2] int * zippo[0]
zippo[i] int [2] int *
&zippo[i] int (*)[2]
*zippo[i] int zippo[i][0]
zippo[i][j] int
&zippo[i][j] int *
*zippo[i][j] invalid
Обратите внимание, что zippo
, &zippo
, *zippo
, zippo[0]
, &zippo[0]
и &zippo[0][0]
все имеют одинаковое значение; все они указывают на базу массива (адрес массива совпадает с адресом первого элемента массива). Однако типы различных выражений различаются.
Ответ 3
zippo
не является указателем. Это массив значений массива. zippo
и zippo[i]
для i
в 0..4 могут "разлагаться" на указатель в определенных случаях (в частности, в контекстах значений). Попробуйте выполнить печать sizeof zippo
для примера использования zippo
в контексте без значения. В этом случае sizeof
сообщит размер массива, а не размер указателя.
Имя массива в контекстах значений распадается на указатель на его первый элемент. Таким образом, в контексте значений zippo
совпадает с &zippo[0]
и, следовательно, имеет тип "указатель на массив [2] of int
"; *zippo
, в контексте значений это то же самое, что и &zippo[0][0]
, то есть "указатель на int
". Они имеют одинаковое значение, но разные типы.
Я рекомендую прочитать Массивы и указатели для ответа на ваш второй вопрос. Указатели имеют одинаковое "значение", но указывают на разное количество пространства. Попробуйте напечатать zippo+1
и *zippo+1
, чтобы увидеть это более четко:
#include <stdio.h>
int main(void)
{
int zippo[4][2] = { {2,4}, {6,8}, {1,3}, {5,7} };
printf("%lu\n", (unsigned long) (sizeof zippo));
printf("%p\n", (void *)(zippo+1));
printf("%p\n", (void *)(*zippo+1));
return 0;
}
Для моего запуска он печатает:
32
0xbffede7c
0xbffede78
Сообщив, что sizeof(int)
на моей машине равен 4, а второй и третий указатели не равны по значению (как и ожидалось).
Кроме того, спецификатор формата "%p"
нуждается в void *
в функциях *printf()
, поэтому вы должны указывать свои указатели на void *
в ваших вызовах printf()
(printf()
- это вариационная функция, поэтому компилятор может " t сделайте автоматическое преобразование для вас здесь).
Изменить: Когда я говорю, что массив "распадается" на указатель, я имею в виду, что имя массива в контексте значения эквивалентно указателю. Таким образом, если у меня есть T pt[100];
для некоторого типа T
, то имя pt
имеет тип T *
в контекстах значений. Для операторов sizeof
и унарных &
имя pt
не сводится к указателю. Но вы можете сделать T *p = pt;
— это совершенно верно, потому что в этом контексте pt
имеет тип T *
.
Обратите внимание, что это "разложение" происходит только один раз. Итак, скажем, у нас есть:
int zippo[4][2] = { {2,4}, {6,8}, {1,3}, {5,7} };
Затем zippo
в контексте значения распадается на указатель типа: указатель на массив [2] из int
. В коде:
int (*p1)[2] = zippo;
тогда как
int **p2 = zippo;
выведет предупреждение о несовместимости указателей.
С zippo
, как указано выше,
int (*p0)[4][2] = &zippo;
int (*p1)[2] = zippo;
int *p2 = zippo[0];
все допустимы. Они должны печатать одно и то же значение при печати с помощью printf("%p\n", (void *)name);
, но указатели отличаются тем, что они указывают на всю матрицу, строку и одно целое число соответственно.
Ответ 4
Важно то, что int zippy[4][2]
не является тем же типом объекта, что и int **zippo
.
Так же, как int zippi[5]
, zippy
- это адрес блока памяти. Но компилятор знает, что вы хотите обратиться к восьми ячейкам памяти, начиная с zippy
с двухмерным синтаксисом, но хотите адресовать пять мест памяти, начиная с zippi
с одномерным синтаксисом.
zippo
совсем другое. Он содержит адрес блока памяти, достаточно большой, чтобы содержать два указателя, и если вы укажете их на некоторые массивы целых чисел, вы можете разыменовать их с помощью синтаксиса доступа к двумерному массиву.
Ответ 5
Очень хорошо объяснил Рид, я добавлю еще несколько моментов, чтобы упростить его, когда мы обращаемся к zippo
или zippo[0]
или zippo[0][0]
, мы все еще имеем в виду тот же базовый адрес массива zippo
. Причиной того, что массивы всегда являются непрерывным блоком памяти, а многомерные массивы - это многократные одномерные массивы, которые постоянно размещаются.
Когда вам нужно увеличивать каждую строку, вам нужен указатель int *p = &zippo[0][0]
, а выполнение p++
увеличивает указатель на каждую строку.
В вашем примере id его массив 4 X 2, при выполнении p++
его, указатель в настоящее время указывает на второй набор из 4 элементов.