Что означает получение адреса переменной массива?
Сегодня я прочитал фрагмент C, который меня действительно смущает:
#include <stdio.h>
int
main(void)
{
int a[] = {0, 1, 2, 3};
printf("%d\n", *(*(&a + 1) - 1));
return 0;
}
По-моему, &a + 1
не имеет смысла, но работает без ошибок.
Может кто-нибудь объяснить, что это значит, спасибо.
И охватывает ли Библия K & R C?
UPDATE0:
Прочитав ответы, я понимаю, что эти два выражения в основном меня смущают:
Ответы
Ответ 1
Разрежьте его.
a
имеет тип int [4]
(массив из 4 int). Его размер составляет 4 * sizeof(int)
.
&a
имеет тип int (*)[4]
(указатель на массив из 4 int).
(&a + 1)
также имеет тип int (*)[4]
. Он указывает на массив из 4 int, который запускает 1 * sizeof(a)
bytes (или 4 * sizeof(int)
bytes) после начала a
.
*(&a + 1)
имеет тип int [4]
(массив из 4 int). Он начинает 1 * sizeof(a)
байты (или 4 * sizeof(int)
байты после начала a
).
*(&a + 1) - 1
имеет тип int *
(указатель на int), потому что массив *(&a + 1)
распадается на указатель на его первый элемент в этом выражении. Он укажет на int, который запустит 1 * sizeof(int)
байт до начала *(&a + 1)
. Это то же значение указателя, что и &a[3]
.
*(*(&a + 1) - 1)
имеет тип int
. Поскольку *(&a + 1) - 1
- это то же самое значение указателя, что и &a[3]
, *(*(&a + 1) - 1)
эквивалентно a[3]
, которое было инициализировано на 3
, так что это число, напечатанное printf
.
Ответ 2
Сначала немного напоминания (или что-то новое, если вы этого не знали раньше): для любого массива или указателя p
и index i
выражение p[i]
точно такое же, как *(p + i)
.
Теперь, надеюсь, поможет вам понять, что происходит...
Массив a
в вашей программе хранится где-то в памяти, точно там, где это не имеет значения. Чтобы получить местоположение, где хранится a
, т.е. Получить указатель на a
, вы используете адрес-оператора &
как &a
. Важно узнать здесь, что указатель сам по себе не означает ничего особенного, главное - базовый тип указателя. Тип a
равен int[4]
, т.е. a
представляет собой массив из четырех элементов int
. Тип выражения &a
является указателем на массив из четырех int
или int (*)[4]
. Скобки важны, потому что тип int *[4]
представляет собой массив из четырех указателей на int
, что совсем другое.
Теперь, чтобы вернуться к исходной точке, p[i]
совпадает с *(p + i)
. Вместо p
имеем &a
, поэтому наше выражение *(&a + 1)
совпадает с (&a)[1]
.
Теперь, что объясняет, что означает *(&a + 1)
и что он делает. Теперь давайте немного подумаем о макете памяти массива a
. В памяти это выглядит как
+---+---+---+---+
| 0 | 1 | 2 | 3 |
+---+---+---+---+
^
|
&a
Выражение (&a)[1]
рассматривает &a
, поскольку это массив массивов, которого он определенно не является, и доступ ко второму элементу в этом массиве, который будет за пределами границ. Это, конечно, технически - это поведение undefined. Давайте продолжим с ним на мгновение, и рассмотрим, как это будет выглядеть в памяти:
+---+---+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | . | . | . | . |
+---+---+---+---+---+---+---+---+
^ ^
| |
(&a)[0] (&a)[1]
Теперь помните, что тип a
(который совпадает с (&a)[0]
и поэтому означает, что (&a)[1]
также должен быть этим типом) - это массив из четырех int
. Поскольку массивы естественным образом распадаются на указатели на первый элемент, выражение (&a)[1]
совпадает с &(&a)[1][0]
, а его тип - указателем на int
. Поэтому, когда мы используем (&a)[1]
в выражении, которое дает нам компилятор, это указатель на первый элемент во втором (несуществующем) массиве &a
. И снова мы приходим к уравнению p[i]
equals *(p + i)
: (&a)[1]
является указателем на int
, it p
в выражении *(p + i)
, поэтому полное выражение *((&a)[1] - 1)
и смотрит на макет памяти выше вычитания одного int
из указателя, заданного (&a)[1]
, дает нам элемент до (&a)[1]
, который является последним элементом в (&a)[0]
, то есть он дает нам (&a)[0][3]
, который совпадает с a[3]
.
Итак, выражение *(*(&a + 1) - 1)
совпадает с выражением a[3]
.
Он длинный, и проходит через опасную территорию (что с индексированием за пределами границ), но из-за силы арифметики указателя все это получается в конце. Я не рекомендую вам писать такой код, хотя нужно, чтобы люди действительно знали, как эти преобразования работают, чтобы расшифровать его.
Ответ 3
&a + 1
будет указывать на память сразу после последнего элемента a
или лучше сказать после массива a
, так как &a
имеет тип int (*)[4]
(указатель на массив из четырех int
's), Построение такого указателя допускается стандартным, но не разыменованием. В результате вы можете использовать его для последующей арифметики.
Итак, результат *(&a + 1)
равен undefined. Но тем не менее *(*(&a + 1) - 1)
является чем-то более интересным. Эффективно оценивается последний элемент в a
. Подробное объяснение см. В fooobar.com/questions/211893/.... И просто замечание - этот хак может быть заменен более понятной и понятной конструкцией: a[sizeof a / sizeof a[0] - 1]
(конечно, он должен применяться только к массивам, а не к указателям).
Ответ 4
Лучше всего доказать это себе:
$ cat main.c
#include <stdio.h>
main()
{
int a[4];
printf("a %p\n",a);
printf("&a %p\n",&a);
printf("a+1 %p\n",a+1);
printf("&a+1 %p\n",&a+1);
}
И вот адреса:
$ ./main
a 0x7fff81a44600
&a 0x7fff81a44600
a+1 0x7fff81a44604
&a+1 0x7fff81a44610
Первые 2 - это один и тот же адрес. Третий 4
больше (это sizeof(int)
). Четвертый - 0x10 = 16
больше (это sizeof(a)
)
Ответ 5
Если у вас есть объект типа T, например
T obj;
то объявление
T *p = &obj;
инициализирует указатель p
адресом памяти, занимаемой объектом obj
Выражение p + 1
указывает на память после объекта obj
. Значение выражения p + 1
равно значению &obj plus sizeof( obj )
, эквивалентному
( T * )( ( char * )&obj + sizeof( obj ) )
Итак, если у вас есть массив, указанный в вашем сообщении int a[] = {0, 1, 2, 3};
, вы можете переписать его объявление с помощью typedef следующим образом:
typedef int T[4];
T a = { 0, 1, 2, 3 };
sizeof( T )
в этом случае равен sizeof( int[4] )
и, в свою очередь, равен 4 * sizeof( int )
Выражение &a
дает адрес объема памяти, занимаемого массивом. Выражение &a + 1
дает адрес памяти после массива, а значение выражения равно &a + sizeof( int[4] )
С другой стороны, имя массива, используемое в выражениях - за редким исключением, например, с использованием имени массива в операторе sizeof
, неявно преобразуется в указатель на его первый элемент.
Таким образом, выражение &a + 1
указывает на воображаемый элемент типа int[4]
после реального первого элемента a
. Выражение *(&a + 1)
дает этот воображаемый элемент. Но поскольку этот элемент представляет собой массив с типом int[4]
, тогда это выражение преобразуется в указатель на его первый элемент типа int *
Этот первый элемент следует за последним элементом массива a
. И в этом случае выражение *(&a + 1) - 1
дает адрес этого последнего элемента массива a
В результате разыменования в *(*(&a + 1) - 1)
вы получите значение последнего элемента массива a
, поэтому будет выведено число 3
.
Ответ 6
Обратите внимание, что следующее эквивалентно, но одинаково неприятно:
printf("%d\n", (&a)[1][-1]);
В этом случае, на мой взгляд, более явное, что происходит:
указывается указатель на массив a
-
указатель используется так, как если бы он был массивом: массив таких элементов, как
a, то есть массивы из 4 целых чисел, используется 1-й элемент этого массива.
-
Так как a на самом деле не массив, а только один элемент (состоящий из
четыре подэлемента!) это индексирует кусок памяти непосредственно после
-
[-1] читает целое число непосредственно перед памятью непосредственно
после a, который является последним подэлементом
Ответ 7
*(*(&a + 1) - 1)
является неудобным и опасным способом обращения к последнему элементу массива. & a - адрес массива типа int [4]. (& a + 1) дает следующий массив int [4] после текущего адресата a. Разрушив его, используя * (& a + 1), вы делаете его в * int, а с добавлением -1 вы указываете на последний элемент a. Затем этот последний элемент разыменовывается и, следовательно, возвращается значение 3 (в вашем примере).
Это хорошо работает, если тип элементов массива имеет ту же длину, что и выравнивание целевого ЦП. Рассмотрим случай, когда у вас есть массив типа uint8 и длина 5: uint8 ar [] = {1,2,3,4,5};
Если вы сделаете то же самое сейчас (в 32-битной архитектуре), вы будете обращаться к unsed дополняющему байту после значения 5. Таким образом, ar [5] имеет адрес, который выровнен до 4 байтов. Отдельные элементы ar сравниваются с одиночными байтами. То есть, адрес ar [0] совпадает с адресом ar, адрес ar [1] является одним байтом после ar (а не 4 байта после ar),..., адресом ar [4 ] ar плюс 5 байтов и, следовательно, не выровнены с 4 байтами. Если вы делаете (& a + 1), вы получаете адрес следующего массива uint8 [5], который выровнен до 4 байтов, т.е. Ar плюс 8 байтов. Если вы берете этот адрес ar плюс 8 байт и переходите один байт назад, вы читаете в ar plus 7, который не используется.