Не могу понять, как вычислить квадрат числа

Я нашел функцию, которая вычисляет квадрат числа:

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

Возвращает значение n2. Вопрос в том, как он это делает? После небольшого тестирования я обнаружил, что между (&a)[k] и (&a)[k+1] находится sizeof(a)/sizeof(int). Почему это?


person Emanuel    schedule 07.01.2015    source источник
comment
У вас есть ссылки на то, где вы нашли эту информацию?   -  person R Sahu    schedule 08.01.2015
comment
int p(n)? Это даже компилируется?   -  person barak manos    schedule 08.01.2015
comment
@barakmanos Ага. ideone.com/QRHvxf   -  person Paul Roub    schedule 08.01.2015
comment
@barakmanos отлично компилируется в gcc.   -  person lurker    schedule 08.01.2015
comment
Это потрясающе, теперь никогда не используйте это снова и вместо этого используйте n * n ...   -  person    schedule 08.01.2015
comment
или лучше: int q(int n) { return sizeof (char [n][n]); }   -  person ouah    schedule 08.01.2015
comment
@ouah - это тот же ответ, что и тот, который я разместил в коде гольфа в прошлом году (codegolf.stackexchange.com/a/18283). /14485)   -  person Mark Lakata    schedule 08.01.2015
comment
Язык C отражает многие особенности PDP-11. Например, операторы ++/-- соответствуют режимам адресации. В PDP и [особенно] VAX все виды арифметических операций могли выполняться с использованием режимов адресации. Отсюда и происхождение подобных трюков.   -  person user3344003    schedule 08.01.2015
comment
кажется, исходит из конкурса загадочного кода C, никогда не делайте такие вещи в рабочем коде!   -  person Arne Burmeister    schedule 08.01.2015
comment
@ouah, если предположить, что этот вопрос относится к codegolf.stackexchange.com/a/43262/967, по которой я этого не сделал используйте sizeof для сохранения символов. Все остальные: это намеренно неясный код, это неопределенное поведение, ответ @ouah правильный.   -  person ecatmur    schedule 08.01.2015
comment
Арифметика указателя в этом коде вызывает неопределенное поведение.   -  person Rufflewind    schedule 08.01.2015
comment
Вероятность переполнения была бы меньше, если бы массив имел тип char, а не int.   -  person Adrian McCarthy    schedule 08.01.2015
comment
а вот и драконы: получение указателя за пределами массива (за исключением одной позиции после последней) является неопределенным поведением, даже если вы не уважаете его.   -  person bolov    schedule 09.01.2015
comment
Разве это не съест память, когда вы используете большие значения n, даже если только на мгновение?   -  person Octopus    schedule 09.01.2015
comment
Хороший. O(n^2) для расчета n^2   -  person Khaled.K    schedule 14.01.2015


Ответы (5)


Очевидно, хак... но способ возведения числа в квадрат без использования оператора * (это было требованием конкурса по кодированию).

(&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
person Mark Lakata    schedule 07.01.2015
comment
И, как вы ясно подразумеваете, но я чувствую необходимость сделать это явным, в лучшем случае это синтаксический взлом. Операция умножения все еще будет там; это просто оператор, которого избегают. - person Tommy; 08.01.2015
comment
Я понял, что это происходит, но мой реальный вопрос в том, почему (&a)[k] находится по тому же адресу, что и + k * sizeof(a) / sizeof(int) - person Emanuel; 08.01.2015
comment
Как старый чудак, я ошеломлен тем фактом, что компилятор может рассматривать (&a) как указатель на объект n*sizeof(int), когда n неизвестен во время компиляции. Раньше C был простым языком... - person Floris; 08.01.2015
comment
Это довольно умный хак, но то, что вы не увидите в рабочем коде (надеюсь). - person John Odom; 08.01.2015
comment
Кроме того, это также UB, потому что он увеличивает указатель, чтобы он не указывал ни на элемент базового массива, ни только на предыдущий. - person Deduplicator; 08.01.2015
comment
@Floris: Действительно, и это (и еще пара) является очень серьезной причиной, по которой многие люди все еще придерживаются C90 даже сегодня. - person alecov; 08.01.2015
comment
@hackks - это было взято из задачи codegolf, чтобы написать функцию, которая возводит число в квадрат без использования символа * в коде. Вы можете решить, что это значит. Это глупый вызов. - person Mark Lakata; 09.01.2015
comment
@МаркЛаката; Оператор * имеет более одного значения в C. Вы должны специально указать оператор умножения. На первый взгляд я подумал, что вы говорите об операторе косвенности, но понял, когда перешел по ссылке. - person haccks; 09.01.2015
comment
И что более важно, это не устраняет умножение, а просто заставляет компилятор скрывать его от вас. Более честным решением проблемы было бы написать функцию, имитирующую умножение простейшего двоичного числа с использованием сдвига и сложения. - person ddyer; 14.01.2015

Чтобы понять этот прием, сначала вам нужно понять разницу в указателях, т. е. что происходит, когда вычитаются два указателя, указывающие на элементы одного и того же массива?

Когда один указатель вычитается из другого, результатом является расстояние (измеряемое в элементах массива) между указателями. Итак, если 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.

person haccks    schedule 07.01.2015
comment
оба должны указывать на элементы одного и того же объекта массива, что вызывает у меня вопрос, не является ли этот хак UB в конце концов. Арифметическое выражение указателя относится к гипотетическому концу несуществующего объекта: допустимо ли это вообще? - person Martin Ba; 08.01.2015
comment
Подводя итог, a — это адрес массива из n элементов, поэтому &a[0] — это адрес первого элемента в этом массиве, который совпадает с a; кроме того, &a[k] всегда будет считаться адресом массива из n элементов, независимо от k, а поскольку &a[1..n] также является вектором, расположение его элементов последовательно, то есть первый элемент находится в позиции x, второй находится в позиции x + (количество элементов вектора a, равное n) и так далее. Я прав? Кроме того, это пространство кучи, значит ли это, что если я выделю новый вектор из тех же n элементов, его адрес будет таким же, как (&a)[1]? - person Emanuel; 08.01.2015
comment
@Эмануэль; &a[k] — это адрес kго элемента массива a. Именно (&a)[k] всегда будет считаться адресом массива из k элементов. Итак, первый элемент находится в позиции a (или &a), второй — в позиции a + (количество элементов массива a, равное n)*(размер элемента массива) и так далее. И обратите внимание, что память для массивов переменной длины выделяется в стеке, а не в куче. - person haccks; 08.01.2015
comment
@МартинБа; Это вообще разрешено? Нет. Это запрещено. Это УБ. Смотрите редактирование. - person haccks; 08.01.2015
comment
Так будет ли (&a)[n]-(&a)[0] правильным способом сделать это? - person Floris; 08.01.2015
comment
@Флорис; № (&a)[n] нарушает стандартное правило: оба должны указывать на элементы одного и того же объекта массива или один после последнего элемента объекта массива. Вы можете сделать это правильно, изменив тело функции на return sizeof (char [n][n]);, как это предлагается в этот комментарий. - person haccks; 08.01.2015
comment
@hacks хорошее совпадение между характером вопроса и вашим никнеймом - person Dimitar Tsonev; 14.01.2015
comment
@ДимитарЦонев; Ой! Да :) - person haccks; 14.01.2015
comment
Кажется, я не понимаю, почему вы говорите, что мы не в ладах. Разве в этом случае мы не проходим мимо последнего элемента объекта массива (последним элементом является (&a)[n-1], и мы спрашиваем (&a)[n], поэтому один прошедший ) - person Falanwe; 14.01.2015
comment
@Фаланве; Последний элемент a[n-1]. a[n] - это один после последнего элемента массива. (&a)[1] - это указатель на последний элемент (массив из n int) массива. - person haccks; 14.01.2015

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 арифметики указателей).

person ouah    schedule 07.01.2015
comment
Ну, я понял, но почему это происходит в C? Какая логика стоит за этим? - person Emanuel; 08.01.2015
comment
@ Эмануэль, на самом деле нет более строгого ответа, чем то, что арифметика указателей полезна для измерения расстояния (обычно в массиве), синтаксис [n] объявляет массив, а массивы разлагаются на указатели. Три отдельно полезных вещи с таким последствием. - person Tommy; 08.01.2015
comment
@ Эмануэль, если вы спрашиваете, почему кто-то сделал бы это, то для этого мало причин и все причины не из-за характера действия UB. И стоит отметить, что (&a)[n] является типом int[n], а который выражается как int* из-за массивов, выражающихся как адрес их первого элемента, если это неясно в описании. - person WhozCraig; 08.01.2015
comment
Нет, я не имел в виду, почему кто-то это сделал, я имел в виду, почему стандарт C ведет себя так в этой ситуации. - person Emanuel; 08.01.2015
comment
@Emanuel Арифметика указателей (и в данном случае подраздел этой темы: различение указателей). Стоит гуглить, а также читать вопросы и ответы на этом сайте. он имеет много полезных преимуществ и конкретно определен в стандартах при правильном использовании. Чтобы полностью понять это, вы должны понять, как устроены типы в коде, который вы перечислили. - person WhozCraig; 08.01.2015

Если у вас есть два указателя, которые указывают на два элемента одного и того же массива, то их разница даст количество элементов между этими указателями. Например, этот фрагмент кода выведет 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.

person Vlad from Moscow    schedule 07.01.2015
comment
Значит, за каждым массивом стоит матрица размера n*n? - person Emanuel; 08.01.2015
comment
@Emanuel Между этими двумя указателями находится матрица из n x n элементов. А разница указателей дает значение, равное n * n, то есть сколько элементов находится между указателями. - person Vlad from Moscow; 08.01.2015
comment
Но почему эта матрица размером n*n позади? Есть ли в нем какое-либо применение в C? Я имею в виду, как будто C выделил больше массивов размера n без моего ведома? Если да, то могу ли я их использовать? Иначе зачем эта матрица формировалась (я имею в виду, что у нее должна быть цель, чтобы она была там). - person Emanuel; 08.01.2015
comment
@Emanuel - эта матрица является лишь объяснением того, как в этом случае работает арифметика указателей. Эта матрица не выделена и вы не можете ее использовать. Как уже было сказано несколько раз, 1) этот фрагмент кода является хаком, который не имеет практического применения; 2) вам нужно узнать, как работает арифметика указателей, чтобы понять этот хак. - person void_ptr; 08.01.2015
comment
@Emanuel Это объясняет арифметику указателя. Выражение ( &a )[n] является указателем на n-элемент отображаемого двумерного массива из-за арифметики указателя. - person Vlad from Moscow; 08.01.2015

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
person onlyice    schedule 14.01.2015