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

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

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

Ответы

Ответ 1

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

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

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

*(a + 5)

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

*(5 + a)

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

Ответ 2

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

Ответ 3

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

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

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

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

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

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

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

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

Можно так же легко сказать:

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

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

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

pointer operator+(pointer p, integer i);

и

pointer operator+(integer i, pointer p);

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

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

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

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

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

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

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

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

Ответ 4

И, конечно,

 ("ABCD"[2] == 2["ABCD"]) && (2["ABCD"] == 'C') && ("ABCD"[2] == 'C')

Основная причина этого заключалась в том, что еще в 70-х годах, когда C был разработан, у компьютеров не было много памяти (64KB было много), поэтому компилятор C не проводил много синтаксической проверки. Следовательно, "X[Y]" был скорее слепо переведен в "*(X+Y)"

Это также объясняет синтаксисы "+=" и "++". Все в форме "A = B + C" имело ту же скомпилированную форму. Но если B был тем же объектом, что и A, тогда была доступна оптимизация уровня сборки. Но компилятор не был достаточно ярким, чтобы распознать его, поэтому разработчику пришлось (A += C). Аналогично, если C был 1, была доступна другая оптимизация уровня сборки, и разработчик должен был сделать это явным, потому что компилятор не узнал его. (Совсем недавно компиляторы делают, поэтому в наши дни эти синтаксисы в значительной степени не нужны)

Ответ 5

Одна вещь, кажется, не упоминала о проблеме Дины с sizeof:

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

Ответ 6

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

Ответ 7

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

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

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

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

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

Ответ 8

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

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 easier to increase the level of indirection (without loop)

}

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

Ответ 9

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

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

а также

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

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

Ответ 10

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

Ответ 11

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

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

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

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

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

char a[20];

писать

a[3] = 'x';

совпадает с записью

3[a] = 'x';

Ответ 12

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

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

Ответ 13

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

Ответ 14

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

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

- это разные способы обращения к элементу массива! (НЕ НА ВСЕ ВРЕМЯ)

Ответ 15

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

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

"Арифметика указателей" внутренне рассчитана как

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

Ответ 16

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

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

Ответ 17

В C-языке указатель и массив очень близки друг к другу, массив можно разделить в виде pointer. Имя массива является указателем на его первый элемент. Поэтому, если acData является массивом символов, то "acData" будет адресом его первого элемента. Вы также можете сказать, что "acData" похож на & acData [0].

В соответствии со стандартом C мы можем представить 1D-массив в виде указателя.

См. приведенное ниже выражение

acData [i] = * (acData + i); --------- > 1D массив в виде указателя

Итак, если я = 5;

cData [5] = * (acData +5);

Мы можем также представить выражение в форме ниже,

cData [5] = * (5 + acData);

Итак, теперь мы можем написать

cData [5] = 5 [cData];

См. приведенный ниже код

#include <stdio.h>

int main(int argc, char *argv[]) {

 char cData  [] = {'w', 'o', 'r', 'l' ,'d' }; // character array

 int index = 0;

 for(index = 0; index < sizeof(cData ); ++index)
 {
     printf("Array element access by pointer = %c\n\n",cData[index]);

     printf("Array element access by   array = %c\n\n",index[cData]);
 }


    return 0;
}

Литература, https://aticleworld.com/array-in-c/

Ответ 18

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

Ответ 19

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

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

int *ptr;

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

int const *ptr;

3) указатель const для данных const

int const *const ptr;

и массивы являются типом (2) из нашего списка
Когда вы определяете массив за один раз, этот адрес инициализируется в этом указателе
Как мы знаем, мы не можем изменить или изменить значение const в нашей программе, потому что он генерирует ERROR во время компиляции

Основная разница, которую я нашел, - это...

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

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

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

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