Как этот кусок кода определяет размер массива без использования sizeof()? - программирование

Как этот кусок кода определяет размер массива без использования sizeof()?

Проходя через несколько вопросов на собеседовании с 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;

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

4b9b3361

Ответ 1

Когда вы добавляете 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 *.

Картинка может помочь:

int [5]  int (*)[5]     int      int *

+---+                   +---+
|   | <- &a             |   | <- a
| - |                   +---+
|   |                   |   | <- a + 1
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+
|   | <- &a + 1         |   | <- *(&a + 1)
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+

Это два представления одного и того же хранилища - слева мы рассматриваем его как последовательность 5-элементных массивов int, а справа - как последовательность int. Я также показываю различные выражения и их типы.

Помните, что выражение *(&a + 1) приводит к неопределенному поведению:

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

C 2011 Онлайн проект, 6.5.6/9

Ответ 2

Эта линия имеет наибольшее значение:

size = *(&a + 1) - a;

Как видите, сначала он берет адрес a и добавляет его к нему. Затем он разыменовывает этот указатель и вычитает из него исходное значение a.

Арифметика указателя в C заставляет это возвращать число элементов в массиве, или 5. Добавление одного и &a является указателем на следующий массив из 5 int после a. После этого этот код разыменовывает результирующий указатель и вычитает из a (тип массива, который распался на указатель), давая количество элементов в массиве.

Детали того, как работает арифметика указателей:

Скажем, у вас есть указатель xyz который указывает на тип int и содержит значение (int *)160. Когда вы вычитаете любое число из xyz, C указывает, что фактическая сумма, вычтенная из xyz равна числу, умноженному на размер типа, на который оно указывает. Например, если вы вычли 5 из xyz, полученное значение xyz будет равно xyz - (sizeof(*xyz) * 5) если арифметика указателя не будет применена.

Поскольку a является массивом из 5 типов int, результирующее значение будет 5. Однако это не будет работать с указателем, только с массивом. Если вы попробуете это с указателем, результат всегда будет 1.

Вот небольшой пример, который показывает адреса и как это не определено. В левой части отображаются адреса:

a + 0 | [a[0]] | &a points to this
a + 1 | [a[1]]
a + 2 | [a[2]]
a + 3 | [a[3]]
a + 4 | [a[4]] | end of array
a + 5 | [a[5]] | &a+1 points to this; accessing past array when dereferenced

Это означает, что код вычитает a из &a[5] (или a+5), давая 5.

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

Ответ 3

Хм, я подозреваю, что это что-то, что не сработало бы в первые дни 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 символов (char arr[][5]) фактически генерируется как массив массива из 8 символов (char arr[][8]), так что внешний массив хорошо выравнивается. Код, который мы обсуждаем, теперь сообщает о размере массива как 8, а не 5. Я не говорю, что определенный компилятор определенно сделает это, но это возможно.

Ответ 4

*(&a + 1) - a;

Является выражением различия двух адресов. В памяти это может выглядеть так:

|||||||||||  // each segment == memory sufficient for sizeof(type)
  ^    ^_______ address of *(&a + 1)
  |____________ address of a

дано:

int array[5];

Выражение:

sizeof(array)/sizeof(array[0]);

Эквивалентно приведенному выше выражению, также дающему 5, количество элементов в массиве. Это происходит потому, что sizeof макро мера байт. А также:

(count of bytes of all elements)/count of bytes for one element) == count of elements

Ответ 6

Хороший вопрос ;-) Если я смогу перевести его на общий язык, не уверен, что он хорошо написан, но взять адрес массива, добавить один (размер массива) и вычесть исходный адрес - чтобы убедиться, как он работает, проверьте перевод на ассемблере. В любом случае, возможно, не слишком переносимый, например, адрес первого целого должен быть & a [0], эти мелкие нюансы работают по-разному на разных платформах/компиляторах. В случае, если вы хотите поиграть с ассемблером или окном просмотра, это может сильно помочь (сложная арифметика с указателями иногда немного сложна). Но всегда pointer++ увеличивается по размеру, только когда он указывает на однобайтовый символ *, он увеличивается на 1.