it-roy-ru.com

С массивами, почему это так, что a [5] == 5 [a]?

Как указывает Джоэл в подкасте Stack Overflow # 34 , в язык программирования C (иначе: K & R), есть упоминание об этом свойстве массивов в C: a[5] == 5[a]

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

1491
Dinah

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

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

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

*(a + 5)

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

*(5 + a)

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

1786
Mehrdad Afshari

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

277
David Thornley

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

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

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

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

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

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

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

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

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

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

в этом случае и i + p, и i[p] будут недопустимы.

В терминах C++ у нас действительно есть два набора перегруженных операторов +, которые можно условно описать так:

pointer operator+(pointer p, integer i);

а также

pointer operator+(integer i, pointer p);

из которых только первое действительно необходимо.

Так почему же так?

C++ унаследовал это определение от C, который получил его от B (коммутативность индексации массива явно упоминается в 1972 Справочник пользователя по B ), который получил от BCPL (руководство от 1967 г. ), что вполне могло быть получено из более ранних языков (CPL? Algol?).

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

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

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

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

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

201
Keith Thompson

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

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

190
James Curran

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

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

52
user30364

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

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

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

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

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

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

23
PolyThinker

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

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)

}

Конечно, я вполне уверен, что в реальном коде для этого нет смысла, но я все равно нашел это интересным :)

21
Frédéric Terrazzoni

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

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

а также

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

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

17
user1287577

Не ответ, а просто пища для размышлений . Если в классе перегружен оператор индекса/индекса, выражение 0[x] не будет работать:

class Sub
{
public:
    int operator [](size_t nIndex)
    {
        return 0;
    }   
};

int main()
{
    Sub s;
    s[0];
    0[s]; // ERROR 
}

Так как у нас нет доступа к int class, это невозможно сделать:

class int
{
   int operator[](const Sub&);
};
14
Ajay

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

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

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

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

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

char a[20];

пишу

a[3] = 'x';

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

3[a] = 'x';
9
A.s. Bhullar

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

Я помню Принципы разработки компилятора, Предположим, что 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.

6
Ajinkya Patil

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

5
Krishan

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

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

разные способы ссылаться на элемент в массиве! (НЕ AT ВСЕ странно)

4
AVIK DUTTA

Немного истории сейчас. Среди других языков BCPL оказал довольно значительное влияние на раннее развитие C. Если вы объявили массив в BCPL с чем-то вроде:

let V = vec 10

это фактически выделило 11 слов памяти, а не 10. Обычно V был первым и содержал адрес непосредственно следующего за Word. Таким образом, в отличие от C, имя V пошло в это место и взяло адрес нулевого элемента массива. Поэтому косвенность массива в BCPL, выраженная как

let J = V!5

действительно нужно было делать J = !(V + 5) (используя синтаксис BCPL), так как было необходимо выбрать V, чтобы получить базовый адрес массива. Таким образом, V!5 и 5!V были синонимами. В качестве эпизодического наблюдения WAFL (функциональный язык Warwick) был написан на BCPL, и, насколько мне было известно, он использовал последний синтаксис, а не первый для доступа к узлам, используемым в качестве хранилища данных. Конечно, это где-то между 35 и 40 годами назад, поэтому моя память немного ржавая. :)

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

Обратите внимание, что ! в BCPL был одновременно унарным префиксным оператором и двоичным инфиксным оператором, причем в обоих случаях выполнялось косвенное обращение. просто двоичная форма включала добавление двух операндов перед выполнением косвенного обращения. Учитывая Word-ориентированную природу BCPL (и B), это действительно имело большой смысл. Ограничение «указатель и целое число» было сделано необходимым в C, когда он получил типы данных, и sizeof стал чем-то особенным.

1
dgnuff

В С 

 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], потому что внутренняя операция в обоих случаях 

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

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

0
Jayghosh Wankar

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

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

0
Harsha J K