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


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.