Неможливо зрозуміти такий спосіб обчислення площі числа


135

Я знайшов функцію, яка обчислює квадрат числа:

int p(int n) {
    int a[n]; //works on C99 and above
    return (&a)[n] - a;
}

Він повертає значення n 2 . Питання в тому, як це робити? Після невеликого тестування я виявив, що між (&a)[k]і (&a)[k+1]є sizeof(a)/ sizeof(int). Чому так?


6
Чи є у вас посилання на те, де ви знайшли цю інформацію?
R Саху

4
int p(n)? Це навіть компілюється?
barak manos

78
Це приголомшливо, тепер ніколи не використовуйте його знову і використовуйте замість n * n ...

26
або краще:int q(int n) { return sizeof (char [n][n]); }
оуа

17
@ouah припускаючи, що це питання стосується codegolf.stackexchange.com/a/43262/967, тому я не використовував sizeofце для збереження символів. Всі інші: це навмисно неясний код, це невизначена поведінка, відповідь @ ouah правильна.
екатмур

Відповіді:


117

Очевидно хак ... але спосіб сортування числа без використання *оператора (це була вимога конкурсу кодування).

(&a)[n] 

еквівалентний покажчику intна розташування

(a + sizeof(a[n])*n)

і таким чином весь вираз є

  (&a)[n] -a 

= (a + sizeof(a[n])*n -a) /sizeof(int)

= sizeof(a[n])*n / sizeof(int)
= sizeof(int) * n * n / sizeof(int)
= n * n

11
І як ви чітко натякаєте, але я відчуваю необхідність зробити явне, це в кращому випадку синтаксис. Операція множення все ще буде там; це уникнути просто оператора.
Томмі

Я зрозумів, що це відбувається позаду, але моє справжнє питання полягає в тому, чому (& a) [k] знаходиться за тією ж адресою, що і + k * sizeof (a) / sizeof (int)
Емануїл

33
Як старий кодер, я здивований тим, що компілятор може трактувати (&a)як вказівник на об'єкт, n*sizeof(int)коли nневідомо під час компіляції. З часом це була простою мовою ...
Флоріс

Це досить розумний злом, але те, чого ви б не бачили у виробничому коді (сподіваємось).
Джон Одом

14
Як бік, це також UB, оскільки він збільшує вказівник, щоб не вказувати ні на елемент базового масиву, ні просто на минуле.
Дедуплікатор

86

Щоб зрозуміти цей злом, спочатку потрібно зрозуміти різницю вказівника, тобто що відбувається, коли відняти два покажчики, що вказують на елементи одного масиву ?

Коли один покажчик віднімається від іншого, результатом є відстань (вимірюється в елементах масиву) між покажчиками. Отже, якщо pвказує на a[i]і qвказує на a[j], то p - qдорівнюєi - j .

C11: 6.5.6 Оператори добавок (p9):

Коли віднімаються два покажчики , обидва повинні вказувати на елементи одного і того ж об’єкта масиву або один минулий останній елемент об’єкта масиву; результат - різниця підписів двох елементів масиву . [...].
Іншими словами, якщо вирази Pі Qвказують відповідно на i-ий-й jелементи об’єкта масиву, вираз (P)-(Q)має значення заi−j умови, що значення вписується в об'єкт типу ptrdiff_t.

Тепер я очікую, що вам відомо про перетворення імені масиву в покажчик, aперетворює вказівник на перший елемент масиву a. &a- це адреса всього блоку пам'яті, тобто це адреса масиву a. Малюнок нижче допоможе вам зрозуміти ( прочитайте цю відповідь для детального пояснення ):

введіть тут опис зображення

Це допоможе вам зрозуміти, що чому aі &aмає таку саму адресу, і як (&a)[i]адреса i- го масиву (такого ж розміру, як і a).

Отже, заява

return (&a)[n] - a; 

еквівалентно

return (&a)[n] - (&a)[0];  

і ця різниця дасть кількість елементів між покажчиками (&a)[n]та (&a)[0], які є nмасивами кожного з n intелементів. Тому загальні елементи масиву n*n= n2 .


ПРИМІТКА:

C11: 6.5.6 Оператори добавок (p9):

Коли віднімаються два покажчики, обидва повинні вказувати на елементи одного і того ж об’єкта масиву або один минулий останній елемент об’єкта масиву ; результат - різниця підписів двох елементів масиву. Розмір результату визначається реалізацією , а його тип (тип підписаного цілого числа) ptrdiff_tвизначається у <stddef.h>заголовку. Якщо результат не є представним в об'єкті цього типу, поведінка не визначена.

Оскільки (&a)[n]ані вказівки на елементи того самого об’єкта масиву, ані один минулий останній елемент об’єкта масиву, (&a)[n] - aне викликатиме визначеної поведінки .

Також зауважте, що краще змінити тип повернення функції pна ptrdiff_t.


"обидва повинні вказувати на елементи одного об'єкта масиву", - що ставить для мене питання, чи цей "хак" не є UB. Арифметичний вираз вказівника посилається на гіпотетичний кінець неіснуючого об'єкта: чи це навіть дозволено?
Мартін Ба

Підводячи підсумок, a - це адреса масиву з n елементів, так & a [0] - це адреса першого елемента в цьому масиві, яка однакова a; крім того, & a [k] завжди буде вважатися адресою масиву з n елементів, незалежно від k, а оскільки & a [1..n] також є вектором, "розташування" його елементів є послідовним, тобто перший елемент знаходиться в положенні x, другий - у положенні x + (кількість елементів вектора a, який n) тощо. Маю рацію? Також це куповий простір, тож чи означає, що якщо я виділяю новий вектор з тих же n елементів, його адреса така ж, як (& a) [1]?
Емануїл

1
@Emanuel; &a[k]це адреса kth-го елемента масиву a. Це (&a)[k]те, що завжди буде вважатися адресою масиву kелементів. Отже, перший елемент знаходиться у положенні a(або &a), другий - у положенні a+ (кількість елементів масиву, aякий є n) * (розмір елемента масиву) тощо. І зауважте, що пам'ять для масивів змінної довжини виділяється на стеці, а не на купі.
hack

@MartinBa; Це навіть дозволено? Ні. Це заборонено. Її УБ. Дивіться редагування.
hack

1
@haccks приємний збіг між натурою і питанням - ваше ім'я
Димитър Цонев

35

aє (змінним) масивом n int.

&aє вказівником на (змінний) масив n int.

(&a)[1]- вказівник intодного intминулого елемента масиву. Цей покажчик є n intелементами після &a[0].

(&a)[2]є вказівником intодного intминулого останнього елемента масиву двох масивів. Цей покажчик є 2 * n intелементами після &a[0].

(&a)[n]- покажчик intодного intминулого елемента масиву nмасивів. Цей покажчик є n * n intелементами після&a[0] . Просто відніміть &a[0]або aі у вас є n.

Звичайно, це технічно невизначена поведінка, навіть якщо вона працює на вашій машині, оскільки (&a)[n]вона не вказує всередині масиву або проходить повз останній елемент масиву (як того вимагають правила C арифметики вказівника).


Ну, я це зрозумів, але чому це відбувається в С? У чому логіка цього?
Емануїл

@Emanuel немає більш жорсткої відповіді на те, що насправді ця арифметика вказівника корисна для вимірювання відстані (як правило, в масиві), [n]синтаксис оголошує масив, а масиви розкладаються на покажчики. Три окремо корисні речі з цим наслідком.
Томмі

1
@Emanuel якщо ви питаєте , чому хто - то буде робити це, немає ніяких підстав , і кожна причина НЕ в зв'язку з UB характером дії. І варто зауважити, що (&a)[n]це тип int[n], і це виражається як int*обумовлене масивами, що виражають як адресу їхнього першого елемента, на випадок, коли це не було зрозуміло в описі.
WhozCraig

Ні, я не мав на увазі, чому хтось це зробив, я мав на увазі, чому стандарт С поводиться так у цій ситуації.
Емануїл

1
@ Арифметика Еманюеля Пойнтера (і в цьому випадку підрозділ цієї теми: розмежування покажчиків ). Варто гуглювати, а також читати питання та відповіді на цьому сайті. він має багато корисних переваг і конкретно визначений у стандартах при правильному використанні. Щоб повністю зрозуміти це, ви повинні зрозуміти, як створюються типи в переліченому вами коді.
WhozCraig

12

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

int a[10];

int *p1 = &a[1];
int *p2 = &a[3];

printf( "%d\n", p2 - p1 ); 

Тепер розглянемо вираз

(&a)[n] - a;

У цьому виразі aє тип int *і вказує на його перший елемент.

Вираз &aмає типint ( * )[n] і вказує на перший рядок зображеного двовимірного масиву. Його значення відповідає значенню, aхоча типи різні.

( &a )[n]

є n-м елементом цього зображеного двовимірного масиву і має тип. int[n]Це - n-й рядок зображеного масиву. У виразі (&a)[n] - aвін перетворюється на адресу свого першого елемента і має тип `int *.

Так між (&a)[n]і aє n рядів з n елементів. Так різниця буде дорівнює n * n.


Так за кожним масивом стоїть матриця розміром n * n?
Емануїл

@Emanuel Між цими двома вказівниками є матриця nxn елементів. А різниця покажчиків дає значення, рівне n * n, тобто кількість елементів між покажчиками.
Влад з Москви

Але чому відстає ця матриця розміром n * n? Чи має це якесь використання в С? Я маю на увазі, це як C "виділило" більше масивів розміром n, не знаючи цього? Якщо так, чи можу я їх використовувати? Інакше навіщо формуватися ця матриця (я маю на увазі, вона повинна мати мету, щоб вона була там).
Емануїл

2
@Emanuel - Ця матриця є лише поясненням того, як працює арифметика вказівника в цьому випадку. Ця матриця не виділена, і ви не можете її використовувати. Як вже було зазначено кілька разів, 1) цей фрагмент коду - це хакер, який не має практичного використання; 2) вам потрібно дізнатися, як працює арифметика вказівника, щоб зрозуміти цей хак.
void_ptr

@Emanuel Це пояснює арифметику вказівника. Вираз (& a) [n] вказує на n- елемент зображеного двовимірного масиву завдяки арифметиці вказівника.
Влад з Москви

4
Expression     | Value                | Explanation
a              | a                    | point to array of int elements
a[n]           | a + n*sizeof(int)    | refer to n-th element in array of int elements
-------------------------------------------------------------------------------------------------
&a             | a                    | point to array of (n int elements array)
(&a)[n]        | a + n*sizeof(int[n]) | refer to n-th element in array of (n int elements array)
-------------------------------------------------------------------------------------------------
sizeof(int[n]) | n * sizeof(int)      | int[n] is a type of n-int-element array

Таким чином,

  1. Тип (&a)[n]İŞ int[n]покажчик
  2. Тип aİŞ intпокажчик

Тепер вираз (&a)[n]-aвиконує субстракцію вказівника:

  (&a)[n]-a
= ((a + n*sizeof(int[n])) - a) / sizeof(int)
= (n * sizeof(int[n])) / sizeof(int)
= (n * n * sizeof(int)) / sizeof(int)
= n * n
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.