Почему с массивами, почему [5] == 5 [a]?


1622

Как указывает Джоэл в подкасте № 34 «Переполнение стека» на языке программирования C (он же K & R), в C упоминается это свойство массивов:a[5] == 5[a]

Джоэл говорит, что это из-за арифметики указателей, но я все еще не понимаю. Почемуa[5] == 5[a] ?


48
будет ли что-то вроде [+] работать как * (a ++) ИЛИ * (++ a)?
Egon

45
@Egon: Это очень креативно, но, к сожалению, так работают не компиляторы. Компилятор интерпретирует a[1]как серию токенов, а не строк: * ({целочисленное расположение} a {operator} + {integer} 1) совпадает с * ({целое число} 1 {operator} + {целочисленное расположение} a) но это не то же самое, что * ({целочисленное расположение} a {operator} + {operator} +)
Дина

11
Интересный составной вариант этого проиллюстрирован в разделе «Нелогичный доступ к массиву» , где вы используете его char bar[]; int foo[];и foo[i][bar]используете как выражение.
Джонатан Леффлер

5
@EldritchConundrum, почему вы думаете, что «компилятор не может проверить, является ли левая часть указателем»? Да, оно может. Это правда, что a[b]= *(a + b)для любого данного aи b, но это был свободный выбор проектировщиков языка для +определения коммутативности для всех типов. Ничто не может помешать им запретить i + pпри разрешении p + i.
ACH

13
@Andrey Один обычно ожидает, +что он будет коммутативным, поэтому, возможно, реальная проблема заключается в том, чтобы сделать операции указателя похожими на арифметику, вместо того, чтобы разрабатывать отдельный оператор смещения.
Eldritch Conundrum

Ответы:


1926

Стандарт C определяет []оператор следующим образом:

a[b] == *(a + b)

Поэтому a[5]оценим:

*(a + 5)

и 5[a]будет оценивать:

*(5 + a)

aуказатель на первый элемент массива a[5]это значение, от которого на 5 элементов дальше a, то же самое *(a + 5), и из математики начальной школы мы знаем, что они равны (сложение коммутативно ).


325
Интересно, не похоже ли это на * ((5 * sizeof (a)) + a). Отличное объяснение.
Джон Макинтайр

92
@Dinah: С точки зрения C-компилятора, вы правы. Размер не требуется, и те выражения, которые я упомянул, те же. Тем не менее, компилятор будет учитывать sizeof при создании машинного кода. Если a является массивом int, он a[5]будет скомпилирован в нечто подобное mov eax, [ebx+20]вместо[ebx+5]
Mehrdad Afshari

12
@Dinah: А это адрес, скажем, 0x1230. Если a был в 32-битном массиве int, то a [0] в 0x1230, a [1] в 0x1234, a [2] в 0x1238 ... a [5] в x1244 и т. Д. Если мы просто добавим 5 к 0x1230, мы получаем 0x1235, что неправильно.
Джеймс Керран

36
@ sr105: Это особый случай для оператора +, где один из операндов является указателем, а другой - целым числом. Стандарт говорит, что результатом будет тип указателя. Компилятор / должен быть / достаточно умным.
AIB

48
«Из математики начальной школы мы знаем, что они равны» - я понимаю, что вы упрощаете, но я с теми, кто считает, что это слишком упрощает. Это не элементарно, что *(10 + (int *)13) != *((int *)10 + 13). Другими словами, здесь происходит больше, чем арифметика в начальной школе. Коммутативность критически зависит от компилятора, который распознает, какой операнд является указателем (и какой размер объекта). Другими словами (1 apple + 2 oranges) = (2 oranges + 1 apple), но (1 apple + 2 oranges) != (1 orange + 2 apples).
LarsH

288

Потому что доступ к массиву определяется с помощью указателей. a[i]определяется, чтобы означать *(a + i), что является коммутативным.


42
Массивы не определяются с точки зрения указателей, но доступ к ним есть.
Гонки легкости на орбите

5
Я бы добавил, что «так оно и есть *(i + a), что можно записать как i[a]».
Джим Балтер

4
Я бы предложил вам включить цитату из стандарта, которая выглядит следующим образом: 6.5.2.1: 2 Постфиксное выражение, за которым следует выражение в квадратных скобках [], это подписанное обозначение элемента объекта массива. Определение оператора индекса [] заключается в том, что E1 [E2] идентична (* ((E1) + (E2))). Из-за правил преобразования, которые применяются к бинарному оператору +, если E1 является объектом массива (эквивалентно указателю на начальный элемент объекта массива), а E2 является целым числом, E1 [E2] обозначает E2-й элемент Е1 (считая с нуля).
Vality

Чтобы быть более правильным: массивы распадаются на указатели, когда вы получаете к ним доступ.
12431234123412341234123

Nitpick: Нет смысла говорить, что это « *(a + i)коммутативно». Однако, *(a + i) = *(i + a) = i[a]потому что сложение коммутативно.
Андреас Рейбранд

231

Я думаю, что что-то упускается другими ответами.

Да, p[i]по определению эквивалентно тому *(p+i), что (потому что сложение коммутативно) эквивалентно тому *(i+p), что (опять же, по определению []оператора) эквивалентно i[p].

(И в array[i]этом случае имя массива неявно преобразуется в указатель на первый элемент массива.)

Но коммутативность сложения не так уж очевидна в этом случае.

Когда оба операнда имеют значение того же типа, или даже различных числовых типов, которые способствовали к общему типу, коммутативности имеет смысл: x + y == y + x.

Но в данном случае речь идет именно об арифметике указателей, где один операнд является указателем, а другой - целым числом. (Целое + целое - это другая операция, а указатель + указатель - это нонсенс.)

Описание +оператора в стандарте C ( N1570 6.5.6) гласит:

Кроме того, либо оба операнда должны иметь арифметический тип, либо один операнд должен быть указателем на полный тип объекта, а другой должен иметь целочисленный тип.

С таким же успехом можно было бы сказать:

Кроме того, либо оба операнда должны иметь арифметический тип, либо левый операнд должен быть указателем на полный тип объекта, а правый операнд должен иметь целочисленный тип.

в этом случае оба 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? Algol?).

Таким образом, идея о том, что индексация массива определяется с точки зрения сложения и что сложение, даже указателя и целого числа, является коммутативной, восходит на многие десятилетия к языкам-предкам Си.

Эти языки были гораздо менее типизированы, чем современные языки. В частности, различие между указателями и целыми числами часто игнорировалось. (Ранние программисты на Си иногда использовали указатели как целые числа без знака до того, как unsignedключевое слово было добавлено к языку.) Поэтому идея сделать добавление некоммутативной, поскольку операнды имеют разные типы, вероятно, не возникла бы у разработчиков этих языков. Если пользователь хотел добавить две «вещи», будь то эти «вещи», являются целыми числами, указателями или чем-то еще, язык не мог предотвратить это.

И на протяжении многих лет любое изменение этого правила нарушало бы существующий код (хотя стандарт ANSI C 1989 года мог бы стать хорошей возможностью).

Изменение C и / или C ++, требующее размещения указателя слева и целого числа справа, может нарушить некоторый существующий код, но при этом не будет потеря реальной выразительной силы.

Так что теперь мы имеем в виду arr[3]и имеем в 3[arr]виду одно и то же, хотя последняя форма никогда не должна появляться за пределами IOCCC .


12
Фантастическое описание этой недвижимости. С точки зрения высокого уровня, я думаю, что 3[arr]это интересный артефакт, но его следует использовать редко, если когда-либо. Принятый ответ на этот вопрос (< stackoverflow.com/q/1390365/356> ), который я задал некоторое время назад, изменил мой взгляд на синтаксис. Хотя технически зачастую нет правильного и неправильного способа сделать эти вещи, такие функции заставляют вас думать не так, как о деталях реализации. Этот способ мышления имеет преимущество, которое частично теряется, когда вы зацикливаетесь на деталях реализации.
Дина

3
Дополнение коммутативно. Для стандарта C определить его иначе было бы странно. Вот почему не так просто сказать: «Кроме того, либо оба операнда должны иметь арифметический тип, либо левый операнд должен быть указателем на полный тип объекта, а правый операнд должен иметь целочисленный тип». - Это не имеет смысла для большинства людей, которые добавляют вещи.
iheanyi

9
@iheanyi: сложение, как правило, коммутативное, и оно обычно занимает два операнда одного типа. Добавление указателя позволяет добавить указатель и целое число, но не два указателя. ИМХО, это уже достаточно странный особый случай, когда требование указателя быть левым операндом не будет значительным бременем. (Некоторые языки используют «+» для конкатенации строк; это определенно не коммутативно.)
Кит Томпсон,

3
@supercat, это еще хуже. Это будет означать, что иногда х + 1! = 1 + х. Это полностью нарушило бы ассоциативное свойство сложения.
iheanyi

3
@iheanyi: я думаю, что вы имели в виду коммутативную собственность; сложение уже не ассоциативно, так как в большинстве реализаций (1LL + 1U) -2! = 1LL + (1U-2). Действительно, изменение сделало бы некоторые ситуации ассоциативными, которых в настоящее время нет, например, 3U + (UINT_MAX-2L) будет равно (3U + UINT_MAX) -2. Однако лучше всего было бы, чтобы в языке были добавлены новые различные типы для целых чисел и «обертывающих» алгебраических колец, так что добавление 2 к a, ring16_tкоторое содержит 65535, привело бы к ring16_tзначению 1, независимому от размераint .
суперкат

196

И, конечно же,

 ("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, была доступна другая оптимизация на уровне сборки, и разработчику снова пришлось сделать это явным, потому что компилятор ее не распознал. (В последнее время это делают компиляторы, поэтому эти синтаксисы сегодня в основном не нужны)


127
На самом деле, это оценивается как ложное; первый термин "ABCD" [2] == 2 ["ABCD"] оценивается как true, или 1, и 1! = 'C': D
Джонатан Леффлер

8
@Jonathan: та же самая двусмысленность приводит к редактированию оригинального названия этого поста. Являемся ли мы равными знаками математической эквивалентности, синтаксиса кода или псевдокода. Я утверждаю математическую эквивалентность, но поскольку мы говорим о коде, мы не можем избежать того, что мы рассматриваем все с точки зрения синтаксиса кода.
Дина

19
Разве это не миф? Я имею в виду, что операторы + = и ++ были созданы для упрощения компилятора? Некоторый код становится понятнее с ними, и это полезный синтаксис, независимо от того, что с ним делает компилятор.
Томас Падрон-Маккарти

6
+ = и ++ имеет еще одно существенное преимущество. если левая часть изменяет некоторую переменную во время оценки, изменение будет сделано только один раз. а = а + ...; сделаю это дважды.
Йоханнес Шауб -

8
Нет - "ABCD" [2] == * ("ABCD" + 2) = * ("CD") = 'C'. Разыменование строки дает вам символ, а не подстроку
MSalters

55

Похоже, никто не упомянул о проблеме Дины с sizeof:

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


1
В комментариях к принятому ответу об этом довольно исчерпывающий разговор. Я сослался на упомянутый разговор в редакторе на исходный вопрос, но не обратился напрямую к вашей очень важной проблеме sizeof. Не уверен, как лучше сделать это в SO. Должен ли я сделать еще одно редактирование в ориг. вопрос?
Дина

49

Ответить на вопрос буквально. Это не всегда правда, что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

27
На самом деле «нан» не равен себе: cout << (a[5] == a[5] ? "true" : "false") << endl;есть false.
Правда

9
@TrueY: Он утверждал, что специально для случая NaN (и, в частности, x == xэто не всегда так). Я думаю, что это было его намерение. Так что он технически правильный (и, возможно, как говорится, лучший вид правильного!).
Тим Час

4
Вопрос о C, ваш код не является C-кодом. Существует также NANв <math.h>, который лучше 0.0/0.0, потому что 0.0/0.0это UB , когда __STDC_IEC_559__не определена (Большинство реализаций не определяют __STDC_IEC_559__, но в большинстве реализаций 0.0/0.0все равно будет работать)
+12431234123412341234123

26

Я просто обнаружил, что этот уродливый синтаксис может быть «полезен» или, по крайней мере, очень забавно играть, когда вы хотите иметь дело с массивом индексов, которые ссылаются на позиции в том же массиве. Он может заменить вложенные квадратные скобки и сделать код более читабельным!

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]вы думаете, что я либо указатель на массив, либо массив указателя на массив или массив ... и aэто индекс. Когда вы видите a[a[a[i]]], вы думаете, что это указатель на массив или массив и iиндекс.
12431234123412341234123

1
Вот Это Да! Это очень крутое использование этой "глупой" функции. Может быть полезен в алгоритмическом конкурсе в некоторых задачах))
Сергей Бреусов

26

Хороший вопрос / ответы.

Сразу хочу отметить, что указатели и массивы C не совпадают , хотя в этом случае разница несущественна.

Рассмотрим следующие объявления:

int a[10];
int* p = a;

В a.out, символ aнаходится по адресу, который является началом массива, и символ pнаходится по адресу, где хранится указатель, а значение указателя в этой ячейке памяти является началом массива.


2
Нет, технически они не одинаковы. Если вы определите некоторое b как int * const и сделаете его указателем на массив, это все еще указатель, означающий, что в таблице символов b ссылается на область памяти, в которой хранится адрес, который, в свою очередь, указывает на то, где находится массив ,
PolyThinker

4
Очень хороший момент. Я помню очень неприятную ошибку, когда я определил глобальный символ как char s [100] в одном модуле, объявив его как extern char * s; в другом модуле. После связывания всего этого программа вела себя очень странно. Поскольку модуль, использующий объявление extern, использовал начальные байты массива в качестве указателя на символ.
Джорджио

1
Первоначально, в BCPL дедушки C, массив был указателем. То есть то, что вы получили, когда писали (я транслитерировал на C), int a[10]был указатель с именем «a», который указывал на достаточно места для хранения 10 целых чисел в другом месте. Таким образом, a + i и j + i имели одинаковую форму: добавьте содержимое пары ячеек памяти. На самом деле, я думаю, что BCPL был без типов, поэтому они были идентичны. И масштабирование по типу не применимо, поскольку BCPL был ориентирован исключительно на слова (также на машинах с адресной адресацией).
Дэйв

Я думаю , что лучший способ понять разницу заключается в сравнении int*p = a;с int b = 5; В последнем, «б» и «5» являются целыми числами, а «Ь» является переменной, а «5» представляет собой фиксированное значение. Аналогично, «p» и «a» оба являются адресами символа, но «a» является фиксированным значением.
Джеймс Керран

20

Для указателей в C мы имеем

a[5] == *(a + 5)

а также

5[a] == *(5 + a)

Следовательно, это правда, что a[5] == 5[a].


15

Не ответ, а просто пища для размышлений. Если в классе перегружен оператор index / subscript, выражение 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&);
};

2
class Sub { public: int operator[](size_t nIndex) const { return 0; } friend int operator[](size_t nIndex, const Sub& This) { return 0; } };
Бен Фойгт

1
Вы на самом деле пытались его скомпилировать? Существует множество операторов, которые не могут быть реализованы вне класса (т.е. как нестатические функции)!
Ajay

3
ой, ты прав. operator[]msgstr " должна быть нестатической функцией-членом с ровно одним параметром." Я был знаком с этим ограничением operator=, не думал, что он применяется к [].
Бен Фойгт

1
Конечно, если вы измените определение []оператора, оно никогда не будет эквивалентным снова ... если a[b]оно равно *(a + b)и вы измените это, вам придется перегружать также, int::operator[](const Sub&);и intэто не класс ...
Луис Колорадо,

7
Это ... не ... C.
MD XF

11

У него очень хорошее объяснение в «Учебнике по указателям и массивам в Си » Теда Дженсена.

Тед Дженсен объяснил это так:

На самом деле это действительно так, то есть везде, где пишут, a[i]можно *(a + i) без проблем заменить . Фактически, компилятор создаст один и тот же код в любом случае. Таким образом, мы видим, что арифметика указателей - это то же самое, что индексирование массива. Любой синтаксис дает одинаковый результат.

Это НЕ говорит, что указатели и массивы - это одно и то же, но это не так. Мы только говорим, что для идентификации данного элемента массива у нас есть выбор из двух синтаксисов, один из которых использует индексирование массива, а другой - арифметику указателей, которые дают идентичные результаты.

Теперь, глядя на это последнее выражение, его часть .. (a + i), это простое дополнение, использующее оператор + и правила C утверждают, что такое выражение коммутативно. То есть (+ я) идентичен (i + a). Таким образом, мы могли бы написать *(i + a)так же легко, как *(a + i). Но *(i + a)мог прийти i[a]! Из всего этого вытекает любопытная правда, что если:

char a[20];

письмо

a[3] = 'x';

так же, как написание

3[a] = 'x';

4
+ Я НЕ простое дополнение, потому что это арифметика указателя. если размер элемента a равен 1 (символ), то да, это как целое число +. Но если это (например) целое число, то оно может быть эквивалентно + 4 * i.
Алекс Браун

@AlexBrown Да, это арифметика указателей, и именно поэтому ваше последнее предложение неверно, если только вы сначала не произвели «a», чтобы быть (char *) (при условии, что int равно 4 символам). Я действительно не понимаю, почему так много людей зацикливаются на фактическом значении результата арифметики с указателями. Вся цель арифметики указателей состоит в том, чтобы абстрагироваться от базовых значений указателя и позволить программисту думать об объектах, которыми манипулируют, а не об адресных значениях.
jschultz410

8

Я знаю, что на вопрос дан ответ, но я не удержался, чтобы поделиться этим объяснением.

Я помню Принципы разработки компилятора. Предположим, aэто intмассив и размер int2 байта, а базовый адрес для a1000.

Как 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.


6

В массивах С , arr[3]и 3[arr]одни и те же, и их эквивалентные обозначения указателей являются *(arr + 3)для *(3 + arr). Но наоборот [arr]3или [3]arrне правильно и приведет к синтаксической ошибке, так как (arr + 3)*и (3 + arr)*не являются допустимыми выражениями. Причина в том, что оператор разыменования должен быть помещен перед адресом, полученным выражением, а не после адреса.


6

в компиляторе c

a[i]
i[a]
*(a+i)

разные способы ссылки на элемент в массиве! (Не совсем странно)


5

Немного истории сейчас. Среди других языков 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стал чем-то особенным.


1

Ну, это особенность, которая возможна только из-за языковой поддержки.

Компилятор интерпретирует a[i]как *(a+i)и выражение5[a] вычисляет до *(5+a). Поскольку сложение коммутативно, оказывается, что оба равны. Следовательно, выражение оценивается как true.


Хотя это и избыточно, это ясно, кратко и кратко.
Билл К

0

В С

 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++ недействителен

a[2] равен 2 [а], потому что внутренняя операция на обоих это

«Арифметика указателя» внутренне рассчитывается как

*(a+3) равно *(3+a)


-4

типы указателей

1) указатель на данные

int *ptr;

2) постоянный указатель на данные

int const *ptr;

3) постоянный указатель на постоянные данные

int const *const ptr;

и массивы имеют тип (2) из ​​нашего списка.
Когда вы определяете массив одновременно, один адрес инициализируется в этом указателе.
Мы знаем, что мы не можем изменить или изменить значение const в нашей программе, потому что он выдает ОШИБКУ. при компиляции время

Основное различие я нашел ...

Мы можем повторно инициализировать указатель по адресу, но не в том же случае с массивом.

======
и вернемся к вашему вопросу ...
a[5]это ничего, но *(a + 5)
вы можете легко понять это,
a указав адрес (люди называют его базовым адресом), как указатель типа (2) в нашем списке
[]- этот оператор может быть заменяется указателем* .

так наконец ...

a[5] == *(a +5) == *(5 + a) == 5[a] 
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.