Проходя через несколько вопросов на собеседовании с C, я нашел вопрос о том, как найти размер массива в C без использования оператора sizeof, со следующим решением. Это работает, но я не могу понять, почему.
#include <stdio.h>
int main() {
int a[] = {100, 200, 300, 400, 500};
int size = 0;
size = *(&a + 1) - a;
printf("%d\n", size);
return 0;
}
Как и ожидалось, возвращается 5.
редактировать: люди указали на этот ответ, но синтаксис немного отличается, то есть метод индексации
size = (&arr)[1] - arr;
поэтому я считаю, что оба вопроса верны и имеют несколько иной подход к проблеме. Спасибо всем за огромную помощь и подробное объяснение!
c
arrays
size
language-lawyer
pointer-arithmetic
janojlic
источник
источник
&a + 1
не указывается ни на один действительный объект, поэтому он недействителен.*((*(&array + 1)) - 1)
безопасно использовать , чтобы получить последний элемент автоматического массива? , tl; dr*(&a + 1)
вызывает неопределенное поведение(ptr)[x]
такие же, как*((ptr) + x)
.Ответы:
Когда вы добавляете 1 к указателю, результатом является местоположение следующего объекта в последовательности объектов указательного типа (т. Е. В массиве). Если
p
указывает наint
объект, тоp + 1
будет указывать на следующийint
в последовательности. Еслиp
указывает на массив из 5 элементовint
(в данном случае это выражение&a
), тоp + 1
будет указывать на следующий массив из 5 элементовint
в последовательности.Вычитание двух указателей (при условии, что они оба указывают на один и тот же объект массива или один указывает один за последним элементом массива) дает число объектов (элементов массива) между этими двумя указателями.
Выражение
&a
возвращает адресa
и имеет типint (*)[5]
(указатель на массив из 5 элементовint
). Выражение&a + 1
возвращает адрес следующего 5-элементного массиваint
следующегоa
, а также имеет типint (*)[5]
. Выражение*(&a + 1)
разыменовывает результат&a + 1
, так что оно дает адрес первого,int
следующего за последним элементомa
, и имеет типint [5]
, который в этом контексте «распадается» на выражение типаint *
.Точно так же выражение
a
«распадается» на указатель на первый элемент массива и имеет типint *
.Картинка может помочь:
Это два представления одного и того же хранилища - слева мы рассматриваем его как последовательность из 5-элементных массивов
int
, а справа - как последовательностьint
. Я также показываю различные выражения и их типы.Помните, что выражение
*(&a + 1)
приводит к неопределенному поведению :C 2011 Онлайн проект , 6.5.6 / 9
источник
size = (int*)(&a + 1) - a;
этот код будет полностью действительным? : oЭта строка имеет наибольшее значение:
Как видите, он сначала берет адрес
a
и добавляет его к нему. Затем он разыменовывает этот указатель и вычитает из него исходное значениеa
.Арифметика указателя в C заставляет это возвращать количество элементов в массиве, или
5
. Добавление одного и&a
указателя на следующий массив через 5int
с послеa
. После этого этот код разыменовывает результирующий указатель и вычитаетa
(тип массива, который распался на указатель) из этого, давая количество элементов в массиве.Подробности о том, как работает арифметика указателей:
Скажем, у вас есть указатель,
xyz
который указывает наint
тип и содержит значение(int *)160
. Когда вы вычитаете любое число изxyz
, C указывает, что фактическая сумма, вычитаемая изxyz
этого числа, равна размеру, на который он указывает. Например, если вы вычли5
изxyz
, значениеxyz
результата будет,xyz - (sizeof(*xyz) * 5)
если арифметика указателя не применяется.Как
a
и массив5
int
типов, полученное значение будет 5. Однако это не будет работать с указателем, только с массивом. Если вы попробуете это с указателем, результат всегда будет1
.Вот небольшой пример, который показывает адреса и как это не определено. В левой части отображаются адреса:
Это означает, что код вычитает
a
из&a[5]
(илиa+5
), давая5
.Обратите внимание, что это неопределенное поведение, и его не следует использовать ни при каких обстоятельствах. Не ожидайте, что такое поведение будет одинаковым на всех платформах, и не используйте его в производственных программах.
источник
Хм, я подозреваю, что это что-то, что не сработало бы в первые дни C. Хотя это умно.
Делая шаги по одному:
&a
получает указатель на объект типа int [5]+1
получает следующий такой объект, предполагая, что есть массив этих*
эффективно преобразует этот адрес в указатель типа на int-a
вычитает два указателя int, возвращая количество экземпляров int между ними.Я не уверен, что это полностью законно (в данном случае я имею в виду юридическое сопровождение языка - не будет работать на практике), учитывая некоторые операции типа. Например, вам только «разрешено» вычитать два указателя, когда они указывают на элементы в одном массиве.
*(&a+1)
был синтезирован путем доступа к другому массиву, хотя и к родительскому массиву, поэтому фактически не является указателем на тот же массив, что иa
. Кроме того, хотя вам разрешено синтезировать указатель за последним элементом массива, и вы можете рассматривать любой объект как массив из 1 элемента, операция разыменования (*
) не «разрешена» для этого синтезированного указателя, даже если она не имеет поведения в этом случае!Я подозреваю, что в первые дни C (синтаксис K & R, кто-нибудь?) Массив распадался на указатель гораздо быстрее, поэтому он
*(&a+1)
мог бы только вернуть адрес следующего указателя типа int **. Более строгие определения современного C ++ определенно позволяют указателю на тип массива существовать и знать размер массива, и, вероятно, стандарты C последовали его примеру. Весь код функции C принимает в качестве аргументов только указатели, поэтому видимая техническая разница минимальна. Но я только догадываюсь здесь.Такой подробный вопрос о легальности обычно применяется к интерпретатору C или к инструменту типа lint, а не к скомпилированному коду. Интерпретатор может реализовать двумерный массив в виде массива указателей на массивы, потому что существует одна функция на этапе выполнения, которую нужно реализовать, и в этом случае разыменование +1 будет фатальным, и даже если это сработает, даст неправильный ответ.
Другая возможная слабость может заключаться в том, что компилятор C может выравнивать внешний массив. Представьте, что это был массив из 5 символов (
char arr[5]
), когда программа выполняет&a+1
ее, она вызывает поведение «массив массива». Компилятор может решить, что массив массива из 5 chars (char arr[][5]
) фактически сгенерирован как массив массива из 8 chars (char arr[][8]
), так что внешний массив хорошо выравнивается. Код, который мы обсуждаем, теперь сообщает о размере массива как 8, а не 5. Я не говорю, что определенный компилятор определенно сделает это, но это возможно.источник
sizeof(array)/sizeof(array[0])
дает количество элементов в массиве.&a+1
определено. Как отмечает Джон Боллинджер,*(&a+1)
это не так, поскольку он пытается разыменовать объект, который не существует.char [][5]
какchar arr[][8]
. Массив - это просто повторяющиеся объекты в нем; там нет отступов. Кроме того, это нарушило бы (ненормативный) пример 2 в C 2018 6.5.3.4 7, который говорит нам, что мы можем вычислить количество элементов в массиве сsizeof array / sizeof array[0]
.