Является ли `* ((* (& array + 1)) – 1)` безопасным для использования, чтобы получить последний элемент автоматического массива?

Предположим, я хочу получить последний элемент автоматического массива, размер которого неизвестен. Я знаю, что я могу использовать оператор sizeof для получения размера массива и получения последнего элемента соответственно.

Использует *((*(&array + 1)) - 1) безопасно?

Подобно:

 char array[SOME_SIZE] = { ... }; printf("Last element = %c", *((*(&array + 1)) - 1)); 
 int array[SOME_SIZE] = { ... }; printf("Last element = %d", *((*(&array + 1)) - 1)); 

так далее

Я считаю, что это неопределенное поведение по причинам, которые Питер упоминает в своем ответе .

Существует огромная дискуссия о *(&array + 1) . С одной стороны, разыменование &array + 1 кажется законным, поскольку он только меняет тип с T (*)[] обратно на T [] , но, с другой стороны, он по-прежнему является указателем на неинициализированную, неиспользуемую и нераспределенную память.

Мой ответ основывается на следующем:

C99 6.5.6.7 (Семантика аддитивных операторов)

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

Поскольку &array не является указателем на объект, который является элементом массива, то в соответствии с этим это означает, что код эквивалентен:

 char array_equiv[1][SOME_SIZE] = { ... }; /* ... */ printf("Last element = %c", *((*(&array_equiv[0] + 1)) - 1)); 

То есть &array является указателем на массив из 10 символов, поэтому он ведет себя так же, как указатель на первый элемент массива длины 1, где каждый элемент представляет собой массив из 10 символов.

Теперь, что вместе с пунктом, который следует (уже упомянутый в других ответах, этот точный отрывок явно украден из ответа ameyCU ):

C99 Раздел 6.5.6.8 –

[…]
если выражение P указывает на последний элемент объекта массива, выражение (P) +1 указывает […]
Если результат указывает один за последним элементом объекта массива, он не должен использоваться как операнд унарного * оператора, который оценивается.

Делает это довольно ясно, что это UB: это эквивалентно разыменованию указателя, который указывает один за последним элементом array_equiv .

Да, в реальном мире это, вероятно, работает, так как на самом деле исходный код на самом деле не разыменовывает местоположение памяти, это в основном преобразование типа из T (*)[] в T [] , но я уверен, что из строгая стандартная точка зрения соответствия, это неопределенное поведение.

Нет.

&array имеет указатель типа на char[SOME_SIZE] (в приведенном первом примере). Это означает, что &array + 1 указывает на память сразу после конца array . Разыменование того, что (как в (*(&array+1)) дает неопределенное поведение.

Нет необходимости анализировать дальше. Когда есть какая-либо часть выражения, которое дает неопределенное поведение, все выражение имеет значение.

Я не думаю, что это безопасно.

Из стандарта, как @dasblinkenlight цитируется в его ответе (теперь удалено) есть также что-то, что я хотел бы добавить:

C99 Раздел 6.5.6.8 –

[…]
если выражение P указывает на последний элемент объекта массива, выражение (P) +1 указывает […]
Если результат указывает один за последним элементом объекта массива, он не должен использоваться как операнд унарного * оператора, который оценивается.

Так как это говорит, мы не должны делать это *(&array + 1) поскольку он будет проходить мимо последнего элемента массива, и поэтому * не следует использовать.

Также хорошо известно, что указатели разыменования, указывающие на несанкционированное расположение памяти, приводят к неопределенному поведению .

Вероятно, это безопасно, но есть некоторые оговорки.

Предположим, что мы имеем

 T array[LEN]; 

Тогда &array имеет тип T(*)[LEN] .

Далее, &array + 1 снова имеет тип T(*)[LEN] , указывая только на конец исходного массива.

Далее, *(&array + 1) имеет тип T[LEN] , который может быть неявно преобразован в T* , все еще указывая только на конец исходного массива. (Таким образом, мы НЕ разыменовали неверную ячейку памяти: оператор * не оценивается).

Далее, *(&array + 1) - 1 имеет тип T* , указывая на последнее местоположение массива.

Наконец, мы разыскиваем это (что является законным, если длина массива не равна нулю): *(*(&array + 1) - 1) дает последний элемент массива, значение типа T

Обратите внимание, что единственный раз, когда мы на самом деле разыскиваем указатель, находится на этом последнем шаге.

Теперь потенциальные оговорки.

Во-первых, *(&array + 1) формально появляется как попытка разыменовать указатель, указывающий на недопустимое расположение памяти. Но это действительно так. Это характер указателей на массивы: эта формальная разыскание только изменяет тип указателя, на самом деле не приводит к попытке получить значение из ссылочной позиции. То есть array имеет тип T[LEN] но он может быть неявно преобразован в type &T , указывая на первый элемент массива; &array – указатель на тип T[LEN] , указывающий на начало массива; *(&array+1) снова имеет тип T[LEN] который может быть неявно преобразован в type &T Ни в коем случае не указатель на самом деле разыменован.

Во-вторых, &array + 1 на самом деле может быть недопустимым адресом, но это действительно не так: справочное руководство My C ++ 11 прямо говорит мне, что «получение указателя на элемент один за концом массива гарантированно работает» , и аналогичное заявление также сделано в K & R, поэтому я считаю, что это всегда было стандартным поведением.

Наконец, в случае массива нулевой длины выражение разыскивает ячейку памяти непосредственно перед массивом, которая может быть нераспределенной / недействительной. Но этот вопрос также возникнет, если использовать более традиционный подход с использованием sizeof() без предварительного тестирования для ненулевой длины.

Короче говоря, я не верю, что существует какое-либо неопределенное или зависящее от реализации поведение этого выражения.

Имхо, который может работать, но, вероятно, неразумный. Вы должны тщательно просмотреть свой дизайн sw и спросить себя, почему вы хотите получить последнюю запись массива. Является ли содержимое массива полностью неизвестным вам или можно определить структуру в терминах c structs и union. Если это так, избегайте сложных операций указателя в массиве символов, например, и правильно определяйте данные в коде c, в структурах и объединениях, где это возможно.

Поэтому вместо:

  printf("Last element = %c", *((*(&array + 1)) - 1)); 

Возможно :

  printf("Checksum = %c", myStruct.MyUnion.Checksum); 

Это уточняет ваш код. Последнее письмо в вашем массиве ничего не значит для человека, не знакомого с тем, что в этом массиве. myStruct.myUnion.Checksum имеет смысл для всех. Изучение структуры myStruct может объяснить всю структуру данных кому угодно. Пожалуйста, используйте что-то подобное, если оно может быть объявлено таким образом. Если вы находитесь в редкой ситуации, вы не можете, изучите выше ответы, они имеют смысл, я думаю

  • array – массив типа int[SOME_SIZE]
  • &array – указатель типа int(*)[SOME_SIZE]
  • &array + 1 – это int(*)[SOME_SIZE] указатель типа int(*)[SOME_SIZE]
  • *(&array + 1) является int[SOME_SIZE] типа int[SOME_SIZE]

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

Путаница, похоже, связана с идеей о том, что разыменование &array + 1 создает сквозной int* который звучит так, как будто он должен быть разумным, даже если стандарт технически запрещает его.

Но это не то, что происходит: разыменование происходит, пытаясь создать lvalue (по существу, ссылку) на несуществующий объект типа int[SOME_SIZE] , что действительно не должно звучать разумно.


Даже если это было определено поведение, а не использовать тайный трюк, гораздо лучше сделать что-то четкое,

 template< typename T, size_t N > T& last(T (&array)[N]) { return array[N-1]; } // ... int array[SOME_SIZE] = { ... }; printf("Last element = %d", last(array)); 

а)

Если оба операнда указателя и результат [из P + N] указывают на элементы одного и того же объекта массива или один за последним элементом объекта массива, оценка не должна приводить к переполнению;
[…]
если выражение P указывает на элемент объекта массива или один за последним элементом объекта массива, а выражение Q указывает на последний элемент одного и того же объекта массива, выражение ((Q) +1) – ( P) имеет то же значение, что и ((Q) – (P)) + 1 и as – ((P) – ((Q) +1)), и имеет значение 0, если выражение P указывает один за последним элементом объекта массива, хотя выражение (Q) +1 не указывает на элемент объекта массива.

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

Тогда нам нужно позаботиться об этой части:

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

Есть одна важная часть, которую другие ответы опущены, а именно:

Если операнд указателя указывает на элемент объекта массива

Это не факт. Оператор указателя, который мы разыменовываем, не является указателем на элемент объекта массива, он является указателем на указатель. Итак, вся эта статья совершенно не имеет значения. Но, также говорится:

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

Что это значит?

Это означает, что наш указатель на указатель на самом деле снова является указателем на массив – длины [1]. И теперь мы можем закрыть цикл, потому что, как сказано в первом абзаце, нам разрешено делать вычисления с одним из массива, поэтому нам разрешено делать вычисления с массивом, как если бы это был массив длины [2]!

В более графическом виде:

 ptr -> (ptr to int[10])[0] -> int[10] -> (ptr to int[10])[1] 

Таким образом, нам разрешено делать вычисления с (ptr до int [10]) [1], хотя это технически вне массива длины [1].

б)

Шаги, которые происходят, следующие:

array ptr типа int [SOME_SIZE] в первый массив элементов

&array ptr для ptr типа int [SOME_SIZE] для первого элемента массива

+ 1 ptr, один больше, чем ptr типа int [SOME_SIZE]) для первого массива элементов, до ptr типа int

Это еще не указатель на int [SOME_SIZE + 1], согласно C99 Section 6.5.6.8. Это еще не ptr + SOME_SIZE + 1

* Мы разыскиваем указатель на указатель. Теперь , после разыменования, у нас есть указатель в соответствии с разделом 6.5.6.8 C99, который проходит мимо элемента массива и который не может быть разыменован. Этот указатель допускается к существованию, и нам разрешено использовать на нем операторы, кроме унарного * оператора. Но мы пока не используем этот указатель.

-1 Теперь мы вычитаем один из ptr типа int в один после последнего элемента массива, позволяя ptr указывать на последний элемент массива.

* разыменование ptr до int до последнего элемента массива, что является законным.

с)

И последнее но не менее важное:

Если это было бы незаконным, то макрос offset тоже был бы незаконным, который определяется как:
((size_t)(&((st *)0)->m))