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

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

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

Ответы [ 3 ]

132 голосов
/ 15 мая 2019

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

32 голосов
/ 15 мая 2019

Эта строка наиболее важна:

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.

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

25 голосов
/ 15 мая 2019

Хм, я подозреваю, что это не сработало бы в ранние времена 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. Я не говорю, что определенный компилятор определенно сделает это, но это возможно.

...