Як цей фрагмент коду визначає розмір масиву, не використовуючи sizeof ()?


134

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

тому я вважаю, що обидва питання справедливі і мають дещо інший підхід до проблеми. Дякую всім за неосяжну допомогу та ретельне пояснення!


13
Ну, не можу його знайти, але схоже на строго кажучи. У додатку J.2 прямо вказано: операнд оператора unary * має недійсне значення - це не визначене поведінка. Тут &a + 1не вказується жоден дійсний об'єкт, тому він недійсний.
Євген Ш.



@AlmaDo добре, синтаксис дещо відрізняється, тобто частина індексації, тому я вважаю, що це питання все-таки справедливе самостійно, але я можу помилятися. Дякую, що вказали на це!
janojlic

1
@janojlicz Вони по суті ті ж, тому що (ptr)[x]це те саме, що *((ptr) + x).
СС Енн

Відповіді:


135

Коли ви додаєте 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


13
Текст "не використовується" є офіційним: C 2018 6.5.6 8.
Eric Postpischil

@EricPostpischil: Чи є у вас посилання на чернетку перед публікацією 2018 року (подібно до N1570.pdf)?
Джон Боде

1
@JohnBode: у цій відповіді є посилання на машину зворотного зв'язку . Я перевірив офіційний стандарт у своїй придбаній копії.
Eric Eric Postpischil

7
Тож якби хтось написав size = (int*)(&a + 1) - a;цей код був би цілком дійсним? : o
Gizmo

@Gizmo вони, ймовірно, спочатку цього не писали, тому що таким чином потрібно вказати тип елемента; оригінал, ймовірно, був написаний визначений як макрос для загального використання типів для різних типів елементів.
Левшенко

35

Цей рядок має найбільш важливе значення:

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.

Зауважте, що це не визначена поведінка, і її не слід застосовувати ні за яких обставин. Не очікуйте, що поведінка цього буде послідовною на всіх платформах, і не використовуйте це у виробничих програмах.


27

Хм, я підозрюю, що це щось, що не спрацювало б у перші дні С. Хоча це розумно.

Роблячи кроки по черзі:

  • &a отримує вказівник на об’єкт типу int [5]
  • +1 отримує наступний такий об'єкт, якщо вважати, що існує масив таких
  • * ефективно перетворює цю адресу в покажчик типу в int
  • -a віднімає два int покажчики, повертаючи підрахунок int між ними.

Я не впевнений, що це повністю легально (маю на увазі мовно-юрист юридичний - не буде це працювати на практиці), враховуючи деякі операції типу. Наприклад, ви можете "відняти" лише два покажчики, коли вони вказують на елементи в одному масиві. *(&a+1)був синтезований за допомогою доступу до іншого масиву, хоча і батьківського масиву, тому насправді не є вказівником на той самий масив, що і a. Крім того, хоча вам дозволено синтезувати покажчик минулого останнього елемента масиву, і ви можете ставитися до будь-якого об'єкта як до масиву з 1 елемента, операція dereferenferen ( *) не "дозволена" на цьому синтезованому покажчику, навіть якщо це не має поведінки в цьому випадку!

Я підозрюю, що на початку C (синтаксис K&R, хтось?) Масив набагато швидше розпадався на покажчик, тож *(&a+1)міг би лише повернути адресу наступного вказівника типу int **. Більш суворі визначення сучасного C ++ безумовно дозволяють вказівнику типу масиву існувати і знати розмір масиву, і, ймовірно, стандарти С дотримуються відповідності. Весь код функції C приймає лише покажчики як аргументи, тому технічна видима різниця мінімальна. Але я тут лише здогадуюсь.

Таке детальне запитання щодо законності зазвичай стосується перекладача С або інструменту типу "ворсинка", а не складеного коду. Інтерпретатор може реалізувати 2D масив як масив покажчиків на масиви, тому що є одна менша функція виконання, яку слід реалізувати, і в цьому випадку перенаправлення +1 було б фатальним, і навіть якщо він працював, він дав би неправильну відповідь.

Іншою можливою слабкістю може бути те, що компілятор C може вирівняти зовнішній масив. Уявіть, якби це був масив з 5 символів ( char arr[5]), коли програма виконує &a+1його, викликає поведінку "масив масиву". Компілятор може вирішити, що масив масиву з 5 символів ( char arr[][5]) насправді генерується як масив масиву з 8 символів ( char arr[][8]), так що зовнішній масив добре вирівнюється. Код, який ми обговорюємо, тепер повідомив би розмір масиву як 8, а не 5. Я не кажу, що певний компілятор напевно це зробив би, але це може.


Досить справедливо. Однак з причин, які важко пояснити, кожен використовує sizeof () / sizeof ()?
Джем Тейлор

5
Більшість людей це робить. Наприклад, sizeof(array)/sizeof(array[0])дає кількість елементів у масиві.
СС Енн

Компілятору C дозволено вирівнювати масив, але я не переконаний, після цього дозволяється змінювати тип масиву. Вирівнювання було б реальніше реалізовуватись, вставляючи байти прокладки.
Кевін

1
Віднімання покажчиків не обмежується лише двома вказівниками в одному масиві - вказівникам також дозволено знаходитись один до кінця масиву. &a+1визначено. Як зазначає Джон Боллінгер, *(&a+1)це не так, оскільки він намагається знеструмити об'єкт, який не існує.
Eric Eric Postpischil

5
Компілятор не може реалізувати char [][5]як char arr[][8]. Масив - це просто повторювані в ньому об'єкти; немає підкладки. Додатково, це порушить (ненормативний) приклад 2 у C 2018 6.5.3.4 7, який говорить нам, що ми можемо обчислити кількість елементів у масиві sizeof array / sizeof array[0].
Eric Eric Postpischil
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.