Как указывает Джоэл в Подкаст Stack Overflow # 34, в Язык программирования C (он же: K & R), есть упоминание об этом свойстве массивов в C: a[5] == 5[a]
Джоэл говорит, что это из-за арифметики указателей, но я все еще не понимаю. Почему a[5] == 5[a]?
@Egon: Это очень творческий подход, но, к сожалению, компиляторы работают не так. Компилятор интерпретирует a[1] как серию токенов, а не как строки: * ({целое число} a {оператор} + {целое число} 1) совпадает с * ({целое число} 1 {оператор} + {целое число} a ), но не то же самое, что * ({целое число} a {оператор} + {оператор} +)
Язык C решил реализовать доступ к массиву исключительно как синтаксический сахар. Поэтому компилятор не может проверить, является ли левая часть указателем. Затем каким-то образом получается, что арифметика указателей делает полученную программу действительной, даже если это не так.
@EldritchConundrum: я не согласен с тем, что это недействительно. Сам Ричи говорит, что это так. Это может быть непреднамеренное последствие, но я считаю, что это все еще актуально.
Интересная вариация этого соединения проиллюстрирована в Нелогичный доступ к массиву, где у вас есть char bar[]; int foo[];, а foo[i][bar] используется в качестве выражения.
@EldritchConundrum, почему вы думаете, что «компилятор не может проверить, что левая часть является указателем»? Да, оно может. Это правда, что a[b] = *(a + b) для любых данных a и b, но разработчики языка добровольно решили, что + был определен как коммутативный для всех типов. Ничто не могло помешать им запретить i + p, но разрешить p + i.
@Andrey Они могли бы запретить i+p, но нарушение коммутативности вредит интуиции. Запрещение i[p] имело бы больший смысл, потому что скобки визуально предполагают доступ к массиву.
@EldritchConundrum, для меня в данном случае коммутативность вредит интуиции. В случае указателей оператор + означает смещение, а не сложение; его аргументы имеют разную природу и потому в них нет симметрии. Мы не можем писать i - p, не так ли?
@Andrey Обычно предполагается, что + будет коммутативным, поэтому, возможно, настоящая проблема состоит в том, чтобы сделать операции с указателями похожими на арифметические, вместо того, чтобы разрабатывать отдельный оператор смещения.
@ach Re "Мы не можем писать i - p": Вы предполагаете, что вычитание обычно коммутативно? ;-)
Это не только a[5] == 5[a], но даже &a[5] == &5[a], то есть два не просто имеют одинаковое значение, это один и тот же объект.
@ Питер, ты упускаешь мою точку зрения. Коммутативны не знаки операций, а обозначаемые ими операции. Использование + для обозначения смещения само по себе нормально, но смещение, в отличие от сложения, не коммутативно. Вы можете применить смещение на 7 шагов к северу к старому дубу, чтобы найти сокровище, но вы не можете применить смещение старого дуба к 7 шагам к северу.
@ach конечно можно; это простое векторное сложение по своей природе (вы можете сначала пройти вектор к дереву, а затем смещение или сначала смещение, а затем тот же вектор; он полностью коммутативен), в математике и в программировании (если мы рассмотрим адресное пространство - одномерный вектор). Вычитания, очевидно, нет: ни в природе, ни в математике, ни в программировании. Ни то и другое обстоятельство не вызывает удивления.
Примечание: не всегда полезно пытаться выяснить, почему C делает что-то определенным образом, если вы не помните / не учитываете его историю. C был создан для переноса Unix, Unix был создан для запуска C - это помогло распространить Unix на многие платформы. Таким образом, язык был в основном разработан для создания простого в реализации / переносимого компилятора. В наши дни синтаксис большинства языков разработан с различными целями, такими как удобочитаемость и согласованность или скорость реализации, или сокращение ошибок или всего вышеперечисленного), поэтому вы не найдете такие функции, которые имеют большой смысл.
Джоэл кто? * * *





Стандарт C определяет оператор [] следующим образом:
a[b] == *(a + b)
Следовательно, a[5] будет оценивать как:
*(a + 5)
и 5[a] оценит:
*(5 + a)
a - указатель на первый элемент массива. a[5] - это значение, которое на 5 элементы дальше от a, что совпадает с *(a + 5), и из математики начальной школы мы знаем, что они равны (сложение коммутативный).
Интересно, не похоже ли это на * ((5 * размер)) + a). Тем не менее, отличное объяснение.
Извините, что "оператор присваивания" сводит вас с ума, однако я спрашиваю о математической эквивалентности, не представляющей фрагмент кода, поэтому знак равенства правильный. Спасибо за ответы!
Почему учитывается sizeof (). Я думал, что указатель на «a» находится в начале массива (то есть: элемент 0). Если это правда, вам нужно только * (a + 5). Мое понимание должно быть неверным. Какая правильная причина?
Если у вас есть массив из 4-х байтовых целых чисел, a [1] - a [0] = 4 (разница в 4 байта между двумя указателями).
@Dinah: С точки зрения компилятора C, вы правы. Размер sizeof не нужен, и те выражения, которые я упомянул, ОДИНАКОВЫ. Однако компилятор будет учитывать sizeof при создании машинного кода. Если a - массив int, a[5] будет компилироваться в нечто вроде mov eax, [ebx+20] вместо [ebx+5].
@ Дина: A - это адрес, скажем, 0x1230. Если a был в 32-битном массиве int, тогда a [0] находится в 0x1230, a [1] находится в 0x1234, a [2] в 0x1238 ... a [5] в x1244 и т. д. Если мы просто добавим 5 к 0x1230, получаем 0x1235, что неверно.
@ Джеймс: бинго. Это то, что мне нужно было увидеть. Я продолжал видеть sizeof () и думать count () и сильно запутался. Не самый яркий момент для меня. Спасибо!
@ Дина; комментарий оператора присваивания был просто насмешливым комментарием о том, какой я анальный. ;-) ... Я знал, что вы имели в виду, и уверен, что все остальные тоже. Между прочим, отличный вопрос, я просто слушал подкаст SO, где об этом говорили.
Итак, в случае 5 [a] компилятор достаточно умен, чтобы использовать «* ((5 * sizeof (a)) + a)», а не «* (5 + (a * sizeof (5)))»? Примечание: я так думаю. Я пробовал это в GCC, и это сработало.
@ sr105: это особый случай для оператора +, где один из операндов является указателем, а другой - целым числом. Стандарт говорит, что результат будет типа указателя. Компилятор / должен быть / достаточно умным.
комментарии никогда не всплывали на моей памяти
Когда вы добавляете целое число к указателю, компилятор знает, на какой тип указывает указатель (так что, если a - это int *, это 4 байта или что-то еще ...), поэтому он может выполнять арифметические операции правильно. Обычно, если вы выполняете «p ++», тогда p следует настроить так, чтобы он указывал на следующий объект в памяти. «p ++» в основном эквивалентно «p = p + 1», поэтому определение добавления указателя выравнивает все. Также обратите внимание, что вы не можете выполнять арифметические операции с указателями типа void*.
@litb: Я понимаю вашу озабоченность и потенциально "вводит людей в заблуждение". Однако я хотел сохранить простоту ответа, поскольку в этом контексте массив распадается на указатель. Я заменил «быть указателем» на «вести себя как указатель». Надеюсь, все в порядке. Кстати, спасибо за комментарий.
myfunc(6291, 8)[Array];, где myfunc - это просто функция по модулю (что эквивалентно Array[3])
@Mehrdad Я думаю, что основная причина, по которой этот пост получил больше голосов, чем этот пост об эксплойте (который определенно заслуживает быть на вершине), заключается в том, что он решает относительно более простую проблему, и, следовательно, больше людей склонны это понимать. Анатомия эксплойта не так проста, и большинство людей просто пропустят его :)
«из математики начальной школы мы знаем, что они равны» - я понимаю, что вы упрощаете, но я с теми, кто считает, что это над упрощает. Не элементарно, что *(10 + (int *)13) != *((int *)10 + 13). Другими словами, здесь происходит нечто большее, чем арифметика в начальной школе. Коммутативность критически зависит от распознавания компилятором, какой операнд является указателем (и на какой размер объекта). Другими словами, (1 apple + 2 oranges) = (2 oranges + 1 apple), но (1 apple + 2 oranges) != (1 orange + 2 apples).
@LarsH: Ты прав. Я бы сказал, что это больше похоже на (10in + 10cm), чем на яблоки и апельсины (вы можете осмысленно конвертировать одно в другое).
@ Mehrdad: Достаточно честно. Возможно, лучшая аналогия - это дата и временной интервал, как в (May 1st 2010 + 3 weeks).
«Это прямой артефакт поведения массивов как указателей»: нет, массивы вообще не действуют как указатели.
«a» - это адрес памяти »: нет, не более чем x - это адрес памяти, если вы пишете int x;. Однако имя массива может разлагаться указывать на первый элемент этого массива.
@ Томалак, я понимаю. Есть много мест, где это было актуально, и мы это обсуждали. Однако, хотя вопрос конкретно касается причина, почему он работает именно так. Я не могу себе представить, что это было бы поведением 5[a], если бы в исходной реализации C указатели не были на самом деле двоичными файлами, представляющими адреса памяти, напрямую понятные процессору. Если мы хотим быть слишком педантичными, ответ (на этот и многие другие вопросы) таков: «Потому что стандарт определяет поведение оператора [] для типов int с одной стороны и типов массивов или указателей - с другой как таковых».
@Jim: Нет, это потому, что типы, а не значения, одинаковы. Кроме того, арифметика в начальной школе не может применяться вслепую к арифметическим операторам. Рассмотрим INT_MAX - 5 + 1 против INT_MAX + 1 - 5.
@ Джим: Вряд ли. Тип a и тип 99 определенно не совпадают в этом вопросе.
@Jim: Как это называется, когда вы редактируете свой комментарий, чтобы мой ответ выглядел глупо? Вам просто нужно просмотреть несколько комментариев, чтобы убедиться, что этот тип ДЕЙСТВИТЕЛЬНО имеет значение. (10 + (int *)13) != ((int *)10 + 13), и на это уже указывалось.
Кроме того, мое утверждение о том, что «арифметика в начальной школе не может применяться вслепую к арифметическим операторам», требует только одного примера, чтобы доказать, что необходимо дальнейшее рассмотрение, а не слепое применение. И я могу привести несколько примеров. Вот еще один случай, когда важен тип: T a = 7.0; double x = a / 2.0;. Очевидно, что то, является ли aint или double, имеет огромное значение для ответа.
Возможны другие примеры из-за ограниченного диапазона и точности типов с плавающей запятой. Пример, который я выбрал изначально, я выбрал потому, что он включает в себя целочисленное сложение, как и обсуждаемая проблема.
@BenVoigt На самом деле я думаю, что вашим примером должен быть double x = a / 2;. Если это 2.0, результатом будет double, независимо от того, является ли aint или double.
Что именно в арифметике начальной школы гласит, что добавление значений совершенно разных типов всегда должно быть коммутативным?
@hamstergene В математике начальной школы не говорится о типах. Моим ответом на вопрос OP для вас будет The One and Only True Answer: «потому что стандарт C так говорит».
@JohnMacIntyre Даже если он не увеличивается автоматически, разве это не должно быть *((5 * sizeof(*a)) + a) вместо *((5 * sizeof(a)) + a)?
@ Jean-BaptisteYunès Да. Технический ответ на вопрос: «потому что в спецификации языка сказано, что *(p+5) равен *(5+p), а a[b] равен *(a+b)». Однако причина того, что *(p+5) приравнивается к *(5+p), действительно согласуется с «математикой начальной школы».
Конечно, но в соответствии с элементарной математикой это не требование в арифметике указателей. Сумма "типизирована" с типом указателя, поэтому она не такая "естественная", так почему же вы хотите, чтобы она была коммутативной? Просто потому, что код, созданный в сборке, не имеет типа?
@ Jean-BaptisteYunès Это не требование. Это дизайнерское решение, которое разработчики языка C приняли, по-видимому, для того, чтобы не нарушать коммутативность оператора сложения. Конечно, когда вы разрабатываете язык, ничто не является обязательный в самом строгом смысле этого слова.
@ Jean-BaptisteYunès и Mehrdad Afshari: Возможно, стоит упомянуть, что в языках ассемблера мы иногда используем постоянный базовый адрес таблицы и вычисленное смещение для выбора элемента массива, а иногда у нас есть постоянное смещение для члена динамически выделенная структура. И оба типа доступа, const [var] и var [const], транслируются в одну и ту же инструкцию ЦП. Возможно, C, будучи довольно низкоуровневым среди языков высокого уровня, намеренно наследует эту эквивалентность.
Небольшая история может помочь объяснить, почему это так. Как отмечено здесь: gotw.ca/conv/003.htm C и C++ берут свое начало в BCPL. BCPL использовал ! (также известный как pling) в качестве оператора косвенного обращения, и он имел две формы: унарную и двоичную. Унарный !a имеет то же значение, что и *a в C / C++, то есть унарное косвенное обращение. Двоичный код a!b используется для поиска в массиве, что эквивалентно a[b] в C. Поскольку двоичный ! коммутативен в BCPL и имеет тот же эффект, что и !(a + b), я очень подозреваю, что именно поэтому косвенное обращение к массиву имеет такое же коммутативное поведение в C / C++.
Почему по стандарту синтаксически разрешено индексировать целочисленные литералы? Я не понимаю, как кто-то мог бы написать это намеренно. Стандарт, вероятно, позволяет это, потому что добавление проверки сделает парсер / лексер компилятора немного более сложным. Но я думаю, что в современном мире влияние скорости на компиляцию будет минимальным, в то время как обнаружение непреднамеренного поведения очень полезно. Более новые версии GCC даже предупреждают о провале коммутаторов, что фактически преднамеренно используется. Так что компиляторы ИМХО должны хотя бы об этом предупредить. GCC 8.2 не выдает предупреждения даже с -Wall.
@JanChristophTerasa Иногда не стоит дополнительных шагов, необходимых для искусственного ограничения чего-либо, только потому, что вы не думаете, что кто-то должен это использовать. Чтобы отказаться от бесполезного варианта, потребовалось бы много дополнительных писем. Но, возможно, мы сможем получить предупреждение для оператора "переходит к", while (0 <-- counter).
Простое и отличное объяснение!
@JohnMacIntyre Помните, что *(a + b) - это то же самое, что и *(b + a), поэтому *(5 + a) - это *(a + 5). a, являющийся указателем, подлежит арифметика указателя (в противном случае разыменование * недопустимо). В итоге: *(5 * sizeof(a)) + a)неправильно.
Потому что доступ к массиву определяется с помощью указателей. a[i] означает *(a + i), который является коммутативным.
Массивы не определены в терминах указателей, но доступ на них есть.
Я бы добавил: «Он равен *(i + a), который можно записать как i[a]».
Я бы посоветовал вам включить цитату из стандарта, которая выглядит следующим образом: 6.5.2.1: 2 Постфиксное выражение, за которым следует выражение в квадратных скобках [], является индексированным обозначением элемента объекта массива. Определение оператора индекса [] заключается в том, что E1 [E2] идентично (* ((E1) + (E2))). Из-за правил преобразования, которые применяются к бинарному оператору +, если E1 является объектом массива (эквивалентно указателем на начальный элемент объекта массива) и E2 является целым числом, E1 [E2] обозначает E2-й элемент E1 (отсчет с нуля).
Чтобы быть более правильным: массивы распадаются на указатели, когда вы обращаетесь к ним.
Нитпик: Нет смысла говорить, что «*(a + i) коммутативен». Однако *(a + i) = *(i + a) = i[a], потому что добавление коммутативен.
@AndreasRejbrand OTOH + - единственный бинарный оператор в выражении, поэтому довольно ясно, что вообще может быть коммутативным.
И конечно
("ABCD"[2] == 2["ABCD"]) && (2["ABCD"] == 'C') && ("ABCD"[2] == 'C')
Основная причина этого заключалась в том, что еще в 70-х годах, когда был разработан C, у компьютеров не было много памяти (64 КБ было много), поэтому компилятор C не выполнял много проверок синтаксиса. Следовательно, "X[Y]" был переведен на "*(X+Y)" довольно слепо.
Это также объясняет синтаксисы «+=» и «++». Все в форме "A = B + C" имело одну и ту же скомпилированную форму. Но если B был тем же объектом, что и A, тогда была доступна оптимизация на уровне сборки. Но компилятор был недостаточно умен, чтобы распознать это, поэтому разработчику пришлось (A += C). Точно так же, если C был 1, была доступна оптимизация на другом уровне сборки, и разработчик снова должен был сделать это явным, потому что компилятор не распознал это. (В последнее время это делают компиляторы, поэтому в наши дни этот синтаксис практически не нужен)
На самом деле это ложно; первый член "ABCD" [2] == 2 ["ABCD"] оценивается как истина, или 1, и 1! = 'C': D
@Jonathan: та же двусмысленность привела к редактированию оригинального названия этого поста. Являемся ли мы равными знаками математической эквивалентности, синтаксиса кода или псевдокода. Я утверждаю математическую эквивалентность, но поскольку мы говорим о коде, мы не можем избежать того, что рассматриваем все с точки зрения синтаксиса кода.
Разве это не миф? Я имею в виду, что операторы + = и ++ были созданы для упрощения компилятора? Некоторый код становится более понятным с ними, и это полезный синтаксис, независимо от того, что с ним делает компилятор.
+ = и ++ имеют еще одно важное преимущество. если левая сторона изменяет некоторую переменную во время оценки, изменение будет выполнено только один раз. а = а + ...; сделаю это дважды.
Слышал, что + = снижает вероятность ошибок, когда вы пишете имена переменных два раза, а не три ...
a = a + с объектами часто приводит к неоптимизированным копиям объектов, потому что он должен делать копию a. a + = не требует копии, он оценивается напрямую.
не преобразуется ли «ABCD» [2] в «CD»? если вы хотите, чтобы он разрешался в 'C', вам нужно использовать разыменование, то есть *("ABCD"[2]) == 'C')
Нет - «ABCD» [2] == * («ABCD» + 2) = * («CD») = 'C'. Разыменование строки дает вам символ, а не подстроку
«Так будет проще реализовать» имеет гораздо больше смысла, чем «математически это работает, поэтому, даже если это не служит никакой практической цели, давайте добавим это к языку» в качестве рационального.
Насколько я помню, Algol68 был источником комбинированных операторов арифметики и присваивания, как в foo +:= bar, произносимых как «foo плюс-и-становится bar». Я считаю, что причина заключалась в том, что это больше напоминало то, что мы хотели сделать в первую очередь, а именно «добавить bar в foo» (хотя почему мы не получили bar =:+ foo из этой логики, я не знаю).
@ ThomasPadron-McCarthy: Из здесь: «Во время разработки [Томпсон] постоянно боролся с ограничениями памяти: каждое добавление языка раздувало компилятор так, что он едва умещался, но каждое переписывание с использованием этой функции уменьшало его размер. Например, B представил обобщенные операторы присваивания, использующие x = + y для добавления y к x ... Томпсон пошел еще дальше, изобретя операторы ++ и - ... более сильной мотивацией для нововведения, вероятно, было его наблюдение, что перевод ++ x был меньше, чем x = x + 1 ».
@dave: Это x += 5;, а не x =+ 5;, потому что последний будет анализироваться как x = (+5);
@JamesCurran Я почти уверен, что он начинался как LHS =- RHS; и в конечном итоге был заменен на -=.
++ часто отображается на одну машинную инструкцию, в то время как x = x + 1 может быть больше единицы. x + = 3 отображается на меньшее количество машинных инструкций, что x = x + 3, поскольку известно, что один возьмет x один раз, прибавит к нему три и сбросит обратно. register int x = 3 относится к той же эпохе, когда компиляторы не были такими умными, как сегодня.
@JamesCurran, унарный + не существовал в раннем C.
@MilesRout: Возможно, нет, но унарный минус определенно сработал, что привело к той же проблеме.
Мини-компьютер PDP11 (PDP использовался для первой операционной системы C и UNIX) имел инструкции по сборке для + = - = ++ - поэтому, хотя в Algol могли быть предшественники, было немного сопоставления 1 к 1 между набором инструкций и языковыми возможностями.
@Vatine прав, это был =+ до +=. Язык программирования B (который, к моему удивлению, все еще используется), предок C, использует форму =+. IIRC, основная причина его изменения заключалась в том, что i=-1; был неоднозначным. Это не является двусмысленным для компилятора, но для читателей-людей, у которых возникли проблемы с пониманием того, должно ли это уменьшить i на 1 (и, следовательно, правильно написано), или это должно было назначить -1 на i (и, следовательно, ошибку в коде). Отказ от ответственности: мое воспоминание может быть ошибочным.
@JohnBode Цитируемое предложение, начинающееся со слов «более сильная мотивация к инновациям ...», является просто круговой аргументацией. Он не мог этого заметить, пока не ввел в него новшества. Дело в том, что PDP-11 имел инструкции как до инкремента, так и после декремента, или, возможно, наоборот, прошло 37 лет.
Итак, если ++ в значительной степени не нужен, неужели C++ в значительной степени не нужен? Я сам держусь за C###.
Хороший вопрос / ответы.
Просто хочу отметить, что указатели и массивы C не являются одно и тоже, хотя в этом случае разница не существенна.
Рассмотрим следующие объявления:
int a[10];
int* p = a;
В a.out символ a находится по адресу, который является началом массива, а символ p находится по адресу, где хранится указатель, а значение указателя в этой ячейке памяти является началом массива.
Нет, технически это не одно и то же. Если вы определите некоторый b как int * const и сделаете его указателем на массив, он все равно будет указателем, что означает, что в таблице символов b относится к области памяти, в которой хранится адрес, который, в свою очередь, указывает на то, где находится массив .
Очень хороший момент. Я помню, как у меня возникла очень неприятная ошибка, когда я определил глобальный символ как char s [100] в одном модуле, объявил его как extern char * s; в другом модуле. После объединения всего этого программа вела себя очень странно. Поскольку модуль, использующий объявление extern, использовал начальные байты массива в качестве указателя на char.
Изначально в BCPL, дедушке C, массив был указателем. То есть то, что вы получили, когда написали (я транслитерировал на C) int a[10], было указателем с именем «a», который указывал на достаточно места для хранения 10 целых чисел в другом месте. Таким образом, a + i и j + i имели одинаковую форму: добавляли содержимое пары ячеек памяти. На самом деле, я думаю, что BCPL был бестиповым, поэтому они были идентичны. И масштабирование типа sizeof не применялось, поскольку BCPL был ориентирован исключительно на слова (также на машинах с адресной адресацией).
Я думаю, что лучший способ понять разницу - это сравнить int*p = a; с int b = 5;. В последнем случае «b» и «5» являются целыми числами, но «b» - это переменная, а «5» - фиксированное значение. Точно так же «p» и «a» являются адресами символа, но «a» - это фиксированное значение.
Хотя этот «ответ» не отвечает на вопрос (и, следовательно, должен быть комментарием, а не ответом), вы можете резюмировать как «массив не является lvalue, а указатель - это».
Одна вещь, о которой, кажется, никто не упомянул о проблеме Дины с sizeof:
К указателю можно добавить только целое число, нельзя складывать два указателя вместе. Таким образом, при добавлении указателя на целое число или целого числа в указатель компилятор всегда знает, какой бит имеет размер, который необходимо учитывать.
Об этом есть довольно исчерпывающий разговор в комментариях к принятому ответу. Я сослался на упомянутый разговор в редактировании на исходный вопрос, но не затронул напрямую вашу очень серьезную озабоченность по поводу sizeof. Не уверен, как лучше всего это сделать в SO. Должен ли я внести еще одно изменение в ориг. вопрос?
Хочу отметить, что вы не можете использовать указатели Добавлять, но можете использовать указатели вычесть (возвращающие количество элементов между ними).
Не ответ, а просто пища для размышлений.
Если класс имеет перегруженный оператор индекса / индекса, выражение 0[x] не будет работать:
class Sub
{
public:
int operator [](size_t nIndex)
{
return 0;
}
};
int main()
{
Sub s;
s[0];
0[s]; // ERROR
}
Поскольку у нас нет доступа к классу int, это сделать нельзя:
class int
{
int operator[](const Sub&);
};
class Sub { public: int operator[](size_t nIndex) const { return 0; } friend int operator[](size_t nIndex, const Sub& This) { return 0; } };Вы действительно пробовали его скомпилировать? Есть набор операторов, которые не могут быть реализованы вне класса (т.е. как нестатические функции)!
ой ты прав. «operator[] должен быть нестатической функцией-членом с ровно одним параметром». Я был знаком с этим ограничением на operator=, но не думал, что это применимо к [].
Конечно, если вы измените определение оператора [], оно больше никогда не будет эквивалентным ... если a[b] равен *(a + b), и вы измените это, вам также придется перегрузить int::operator[](const Sub&);, а int не является классом ...
Это ... не ... C.
Дословно ответить на вопрос. Не всегда верно, что x == x
double zero = 0.0;
double a[] = { 0,0,0,0,0, zero/zero}; // NaN
cout << (a[5] == 5[a] ? "true" : "false") << endl;
отпечатки
false
На самом деле «нан» не равен самому себе: cout << (a[5] == a[5] ? "true" : "false") << endl; - это false.
@TrueY: Он заявил это специально для случая NaN (и в частности, что x == x не всегда верно). Я думаю, это было его намерением. Так что он технически правильный (и, возможно, как говорится, лучший из правильных!).
Вопрос в C, ваш код - это не C-код. В NAN также есть <math.h>, который лучше, чем 0.0/0.0, потому что 0.0/0.0 - это UB, когда __STDC_IEC_559__ не определен (большинство реализаций не определяют __STDC_IEC_559__, но в большинстве реализаций 0.0/0.0 все равно будет работать)
Для указателей в C мы имеем
a[5] == *(a + 5)
а также
5[a] == *(5 + a)
Следовательно, верно, что a[5] == 5[a].
Я просто обнаружил, что этот уродливый синтаксис может быть «полезным» или, по крайней мере, очень забавным, когда вы хотите иметь дело с массивом индексов, которые относятся к позициям в том же массиве. Он может заменить вложенные квадратные скобки и сделать код более читабельным!
int a[] = { 2 , 3 , 3 , 2 , 4 };
int s = sizeof a / sizeof *a; // s == 5
for(int i = 0 ; i < s ; ++i) {
cout << a[a[a[i]]] << endl;
// ... is equivalent to ...
cout << i[a][a][a] << endl; // but I prefer this one, it's easier to increase the level of indirection (without loop)
}
Конечно, я совершенно уверен, что в реальном коде этого нет, но мне все равно было интересно :)
Когда вы видите i[a][a][a], вы думаете, что i - это либо указатель на массив, либо массив указателя на массив или массив ... а a - это индекс. Когда вы видите a[a[a[i]]], вы думаете, что это указатель на массив или массив, а i - это индекс.
Ух ты! Очень круто использовать эту "глупую" фичу. Может пригодиться в алгоритмических соревнованиях в некоторых задачах))
Я думаю, что в других ответах чего-то не хватает.
Да, p[i] по определению эквивалентен *(p+i), который (поскольку сложение является коммутативным) эквивалентен *(i+p), который (опять же, по определению оператора []) эквивалентен i[p].
(А в array[i] имя массива неявно преобразуется в указатель на первый элемент массива.)
Но коммутативность сложения в этом случае не так уж очевидна.
Когда оба операнда относятся к одному и тому же типу или даже относятся к разным числовым типам, которые преобразованы в общий тип, коммутативность имеет смысл: x + y == y + x.
Но в данном случае мы говорим конкретно об арифметике указателей, где один операнд является указателем, а другой - целым числом. (Целое число + целое число - это другая операция, а указатель + указатель - ерунда.)
Описание оператора + в стандарте C (N1570 6.5.6) гласит:
For addition, either both operands shall have arithmetic type, or one operand shall be a pointer to a complete object type and the other shall have integer type.
С таким же успехом можно было бы сказать:
For addition, either both operands shall have arithmetic type, or the left operand shall be a pointer to a complete object type and the right operand shall have integer type.
в этом случае и i + p, и i[p] будут незаконными.
В терминах C++ у нас действительно есть два набора перегруженных операторов +, которые можно условно описать как:
pointer operator+(pointer p, integer i);
а также
pointer operator+(integer i, pointer p);
из которых действительно необходимо только первое.
Так почему это так?
C++ унаследовал это определение от C, который получил его от B (коммутативность индексации массивов явно упоминается в Ссылка пользователей на B 1972 г.), который получил его от BCPL (руководство от 1967 г.), которое вполне могло быть заимствовано из даже более ранних языков (CPL ? Алгол?).
Таким образом, идея о том, что индексирование массивов определяется в терминах сложения, и что сложение даже указателя и целого числа является коммутативным, восходит к языкам предков C.
Эти языки были гораздо менее типизированы, чем современный C. В частности, часто игнорировалось различие между указателями и целыми числами. (Ранние программисты на C иногда использовали указатели как целые числа без знака до того, как ключевое слово unsigned было добавлено в язык.) Таким образом, идея сделать добавление некоммутативным, поскольку операнды имеют разные типы, вероятно, не пришла бы в голову разработчикам этих языков. . Если пользователь хотел добавить две «вещи», будь то целые числа, указатели или что-то еще, язык не мог предотвратить это.
И с годами любое изменение этого правила нарушило бы существующий код (хотя стандарт ANSI C 1989 года мог быть хорошей возможностью).
Изменение C и / или C++ на указатель слева и целое число справа может нарушить какой-то существующий код, но не потеряет реальной выразительной силы.
Итак, теперь у нас есть arr[3] и 3[arr], означающие одно и то же, хотя последняя форма никогда не должна появляться за пределами IOCCC.
Фантастическое описание этой собственности. С точки зрения высокого уровня, я думаю, что 3[arr] - интересный артефакт, но его следует использовать редко, если вообще когда-либо. Принятый ответ на этот вопрос (<stackoverflow.com/q/1390365/356>), который я задал некоторое время назад, изменил мои представления о синтаксисе. Хотя часто технически нет правильного и неправильного способа сделать эти вещи, такие функции заставляют вас думать отдельно от деталей реализации. В этом другом способе мышления есть преимущества, которые частично теряются, когда вы зацикливаетесь на деталях реализации.
Сложение коммутативно. Для стандарта C было бы странно определять это иначе. Вот почему нельзя было так просто сказать: «Для сложения либо оба операнда должны иметь арифметический тип, либо левый операнд должен быть указателем на полный тип объекта, а правый операнд должен иметь целочисленный тип». - Это не имеет смысла для большинства людей, которые что-то добавляют.
@iheanyi: сложение обычно является коммутативным, и для него обычно используются два операнда одного типа. Добавление указателя позволяет добавлять указатель и целое число, но не два указателя. ИМХО, это уже достаточно странный частный случай, когда требование, чтобы указатель был левым операндом, не было бы значительным бременем. (В некоторых языках для конкатенации строк используется знак «+»; это определенно не коммутативно.)
Верно на примере строки! В этом свете это выглядит как языковое решение, исходящее от реализации, а не от дизайна.
@iheanyi: сложение чисел коммутативно, но это не значит, что сложение должно быть коммутативным с вещами, которые не являются числами. Ассемблеры нередко требовали, чтобы каждый адрес, включающий перемещаемый символ, имел точную форму «rel_symbol», «rel_symbol + number» или «rel_symbol - number», поскольку компоновщик ожидал бы список исправлений, каждый из которых идентифицирует «базовый» символ и место, где он используется (предварительно установленный код будет содержать число, которое будет добавлено к символу).
@iheanyi: Я думаю, что с точки зрения правил было бы проще сказать, что второй операнд оператора сложения должен быть числом, а тип результата будет соответствовать первому операнду, чем пытаться сказать, что "хотя бы один" операнд должен быть число. Между прочим, множество неприятностей, связанных с беззнаковыми типами, можно было бы устранить, если бы оператор сложения всегда возвращал тип своего левого операнда, вместо того, чтобы говорить, что с учетом uint32_t x=0; значение x-1 должно в некоторых реализациях давать 4294967295, а в других - 1.
@supercat, это еще хуже. Это означало бы, что иногда x + 1! = 1 + x. Это полностью нарушило бы ассоциативное свойство сложения.
@iheanyi: Я думаю, вы имели в виду коммутативную собственность; сложение уже не ассоциативно, поскольку в большинстве реализаций (1LL + 1U) -2! = 1LL + (1U-2). Действительно, изменение сделало бы ассоциативными некоторые ситуации, которых в настоящее время нет, например 3U + (UINT_MAX-2L) будет равно (3U + UINT_MAX) -2. Однако было бы лучше, если бы в языке были добавлены новые отдельные типы для продвигаемых целых чисел и «обертывающих» алгебраических колец, чтобы добавление 2 к ring16_t, содержащему 65535, привело бы к ring16_t со значением 1, независимо от размера int.
@supercat - спасибо за такой ответ. Это проясняет проблемы на хорошем примере :)
Что касается C++, следует отметить, что перегрузки определяемых пользователем операторов не подчиняются тому же правилу: vec[5] в порядке, тогда как 5[vec] - это ошибка.
@LF: Я считать можно предоставить перегрузку, чтобы 5[vec] был действительным (и мог иметь другое значение, чем vec[5]. (Я должен это проверить). Но вопрос помечен как «c», поэтому я не войдите в это.
@KeithThompson Ну, теоретически вы можете обеспечить неявное преобразование в T*, но это противоречит идее векторов.
У этого есть очень хорошее объяснение в Учебное пособие по указателям и массивам на языке C пользователя Ted Jensen.
Тед Дженсен объяснил это так:
In fact, this is true, i.e wherever one writes
a[i]it can be replaced with*(a + i)without any problems. In fact, the compiler will create the same code in either case. Thus we see that pointer arithmetic is the same thing as array indexing. Either syntax produces the same result.This is NOT saying that pointers and arrays are the same thing, they are not. We are only saying that to identify a given element of an array we have the choice of two syntaxes, one using array indexing and the other using pointer arithmetic, which yield identical results.
Now, looking at this last expression, part of it..
(a + i), is a simple addition using the + operator and the rules of C state that such an expression is commutative. That is (a + i) is identical to(i + a). Thus we could write*(i + a)just as easily as*(a + i). But*(i + a)could have come fromi[a]! From all of this comes the curious truth that if:char a[20];writing
a[3] = 'x';is the same as writing
3[a] = 'x';
a + i НЕ является простым сложением, потому что это арифметика указателя. если размер элемента a равен 1 (char), тогда да, это как целое число +. Но если это (например) целое число, то оно может быть эквивалентно a + 4 * i.
@AlexBrown Да, это арифметика указателя, именно поэтому ваше последнее предложение неверно, если вы сначала не приведете 'a' к (char *) (при условии, что int составляет 4 символа). Я действительно не понимаю, почему так много людей зацикливаются на фактическом результате арифметики указателей. Вся цель арифметики указателя состоит в том, чтобы абстрагироваться от базовых значений указателя и позволить программисту думать об объектах, которыми управляют, а не о значениях адресов.
В массивах C arr[3] и 3[arr] одинаковы, и их эквивалентные обозначения указателей - от *(arr + 3) к *(3 + arr). Но наоборот, [arr]3 или [3]arr неверны и приведут к синтаксической ошибке, поскольку (arr + 3)* и (3 + arr)* не являются допустимыми выражениями. Причина в том, что оператор разыменования следует размещать перед адресом, полученным в выражении, а не после адреса.
в компиляторе c
a[i]
i[a]
*(a+i)
- это разные способы ссылки на элемент в массиве! (ВООБЩЕ НЕ СТРАННО)
Я знаю, что на вопрос дан ответ, но я не мог удержаться от этого объяснения.
Я помню принципы построения компилятора,
Предположим, что a - это массив int, а размер int составляет 2 байта,
& Базовый адрес для a - 1000.
Как будет работать a[5] ->
Base Address of your Array a + (5*size of(data type for array a))
i.e. 1000 + (5*2) = 1010
Так,
Точно так же, когда код c разбивается на 3-адресный код,
5[a] станет ->
Base Address of your Array a + (size of(data type for array a)*5)
i.e. 1000 + (2*5) = 1010
Таким образом, в основном оба утверждения указывают на одно и то же место в памяти и, следовательно, на a[5] = 5[a].
Это объяснение также является причиной того, почему отрицательные индексы в массивах работают в C.
т.е. если я получу доступ к a[-5], он даст мне
Base Address of your Array a + (-5 * size of(data type for array a))
i.e. 1000 + (-5*2) = 990
Он вернет мне объект по адресу 990.
В C
int a[] = {10,20,30,40,50};
int *p=a;
printf("%d\n",*p++);//output will be 10
printf("%d\n",*a++);//will give an error
Указатель p - это «переменная», имя массива a - «мнемоника» или «синоним»,
поэтому p++ действителен, но a++ недействителен.
a[2] равен 2[a], потому что внутренняя операция в обоих случаях является «арифметикой указателя», внутренне вычисляемой, поскольку *(a+2) равен *(2+a).
Что ж, это функция, которая возможна только из-за языковой поддержки.
Компилятор интерпретирует a[i] как *(a+i), а выражение 5[a] оценивается как *(5+a). Поскольку сложение коммутативно, оказывается, что оба они равны. Следовательно, выражение оценивается как true.
Хотя это избыточно, это ясно, кратко и кратко.
Немного истории. Среди других языков BCPL оказал довольно большое влияние на раннее развитие C. Если вы объявили массив в BCPL примерно так:
let V = vec 10
это фактически выделяло 11 слов памяти, а не 10. Обычно V было первым и содержало адрес следующего за ним слова. Таким образом, в отличие от C, имя V перешло в это место и взяло адрес нулевого элемента массива. Следовательно, косвенное обращение к массиву в BCPL, выраженное как
let J = V!5
действительно нужно было сделать J = !(V + 5) (с использованием синтаксиса BCPL), поскольку необходимо было получить V, чтобы получить базовый адрес массива. Таким образом, V!5 и 5!V были синонимами. Как анекдотическое наблюдение, WAFL (функциональный язык Warwick) был написан на BCPL, и, насколько мне известно, для доступа к узлам, используемым в качестве хранилища данных, использовался последний синтаксис, а не первый. Конечно, это было где-то между 35 и 40 годами назад, так что моя память немного ржавая. :)
Позже появилось нововведение, заключающееся в отказе от лишних слов для хранения и о том, что компилятор вставляет базовый адрес массива при его названии. Согласно исторической статье C, это произошло примерно в то время, когда в C.
Обратите внимание, что ! в BCPL был и унарным префиксным оператором, и двоичным инфиксным оператором, в обоих случаях выполняя косвенное обращение. просто двоичная форма включала добавление двух операндов перед выполнением косвенного обращения. Учитывая словесно-ориентированный характер BCPL (и B), это имело большой смысл. Ограничение «указатель и целое число» стало необходимым в C, когда он получил типы данных, и sizeof стал вещью.
что-то вроде [+] также работает как * (a ++) OR * (++ a)?