Как * (& arr + 1) - arr работает, чтобы указать размер массива

int arr[] = { 3, 5, 9, 2, 8, 10, 11 };      
int arrSize = *(&arr + 1) - arr;
std::cout << arrSize;

Я не могу понять, как это работает. Так что любой может мне с этим помочь.

Ответов (6)

Если «нарисовать» массив вместе с указателями, он будет выглядеть примерно так:

+--------+--------+-----+--------+-----+
| arr[0] | arr[1] | ... | arr[6] | ... |
+--------+--------+-----+--------+-----+
^        ^                       ^
|        |                       |
&arr[0]  &arr[1]                 |
|                                |
&arr                             &arr + 1

Тип выражений &arr и &arr + 1 есть int (*)[7] . Если мы разыменуем любой из этих указателей, мы получим значение типа int[7], и, как со всеми массивами, оно превратится в указатель на свой первый элемент.

Итак, что происходит, мы берем разницу между указателем на первый элемент &arr + 1 (разыменование действительно делает этот UB, но все равно будет работать с любым нормальным компилятором) и указателем на первый элемент &arr .

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


Было бы полезно знать, что массив естественным образом распадется до указателя на свой первый элемент, то есть выражение arr распадется на &arr[0], который будет иметь тип int * .

Кроме того, для любого указателя (или массива) p и индекса i выражение *(p + i) в точности равно p[i] . На *(&arr + 1) самом деле это то же самое, что и (&arr)[1] (что делает UB более заметным).

Вам необходимо изучить, что это за тип &arr выражения и как это влияет на + 1 работу с ним.

Арифметика указателя работает в «сырых единицах» указанного типа; &arr - это адрес вашего массива , поэтому он указывает на объект типа «массив 7 int». Добавление 1 к этому указателю фактически добавляет размер типа к адресу - поэтому 7 * sizeof(int) он добавляется к адресу.

Однако во внешнем выражении (вычитание arr ) операнды являются указателями на int объекты 1 (не массивы), поэтому «единицы» просто sizeof(int) - что в 7 раз меньше, чем во внутреннем выражении. Таким образом, вычитание приводит к размеру массива.


1 Это потому, что в таких выражениях переменная массива (например, второй операнд arr ) распадается на указатель на свой первый элемент; кроме того, ваш первый операнд также является массивом, поскольку * оператор разыменовывает измененное значение указателя массива.


Примечание о возможном UB: другие ответы (и комментарии к ним) предполагают, что операция разыменования *(&arr + 1) вызывает неопределенное поведение. Однако, просматривая этот проект стандарта C++ 17 , есть самые расплывчатые предположения, что он не может:

6.7.2 Составные типы
...
3     … В целях арифметики указателей (8.5.6) и сравнения (8.5.9, 8.5.10) рассматривается указатель за концом последнего элемента массива x из n элементов. быть эквивалентным указателю на гипотетический элемент x [n].

Но я не буду здесь претендовать на статус «Language-Lawyer», поскольку в этом разделе нет явного упоминания о разыменовании такого указателя.

Как уже упоминалось, *(&arr + 1) запускает неопределенное поведение, потому что &arr + 1 это указатель на один за концом массива типа, int [7] и этот указатель впоследствии разыменовывается.

Альтернативный способ сделать это - преобразовать соответствующие указатели uintptr_t, вычесть и разделить размер элемента.

int arrSize = reinterpret_cast<int>((reinterpret_cast<uintptr_t>(&arr + 1) -
                                     reinterpret_cast<uintptr_t>(arr)) / sizeof *arr);

Или используя приведение в стиле C:

int arrSize = (int)(((uintptr_t)(&arr + 1) - (uintptr_t)arr) / sizeof *arr);

Если у вас есть подобное заявление

int arr[] = { 3, 5, 9, 2, 8, 10, 11 };

выражение &arr + 1 будет указывать на память после последнего элемента массива. Значение выражения равно значению выражения, arr + 7 где 7 - количество элементов в массиве, объявленном выше. Единственное отличие состоит в том, что у выражения &arr + 1 есть тип, int ( * )[7] а у выражения arr + 7 - тип int * .

Таким образом, из-за целочисленной арифметики разница ( arr + 7 ) - arr даст 7: количество элементов в массиве.

С другой стороны, разыменовывая выражение, &att + 1 имеющее тип, int ( * )[7] мы получим lvalue типа, int[7] который, в свою очередь, используется в выражении *(&arr + 1) - arr, преобразуется в указатель типа int * и имеет то же значение, как arr + 7 было указано выше. Таким образом, выражение даст количество элементов в массиве.

Единственная разница между этими двумя выражениями

( arr + 7 ) - arr

а также

*( &arr + 1 ) - arr

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

Это просто:

  1. arrэто просто указатель на 0-й элемент массива ( &arr[0]);
  2. &arr дает указатель на предыдущий указатель;
  3. &arr+1дает указатель на указатель на arr[0]+ sizeof(arr)*1;
  4. *(&arr + 1)превращает предыдущее значение в просто &arr[0]+sizeof(arr)*1;
  5. *(&arr + 1) - arrи вычитает указатель на arr[0]уход просто sizeof(arr)*1.

Таким образом, единственные уловки здесь заключаются в том, что статические массивы в C внутренне сохраняют всю информацию о своих статических типах, включая их общие размеры, и что, когда вы увеличиваете указатель на некоторое целочисленное значение, компиляторы C не просто добавляют к нему значение, а по какой-либо причине стандарты требуют увеличения указателей на значение sizeof() любого типа, которому назначен указатель, умноженное на указанное значение, поэтому *(&p+idx) дает тот же результат, что и p[idx] .

Язык C разработан для очень упрощенных компиляторов, поэтому внутри он полон таких маленьких уловок. Однако я бы не рекомендовал использовать их в производственном коде. Помните о других разработчиках, которым, возможно, придется читать и поддерживать ваш код позже и вместо этого использовать самые простые и очевидные вещи (например, очевидно, что это просто прямое использование sizeof ()).

Эта программа имеет неопределенное поведение. (&arr + 1) - действительный указатель, указывающий на «один за пределами» arr и имеющий тип int(*)[7], однако он не указывает на объект int [7], поэтому разыменование его недопустимо.

Так получилось, что ваша реализация предполагает, что есть второй int [7] после того, который вы объявляете, и вычитает местоположение первого элемента этого массива, который существует, из местоположения первого элемента фиктивного массива, созданного арифметикой указателя.