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

1456

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

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

  • 39
    что-то вроде [+] также будет работать как * (a ++) ИЛИ * (++ a)?
  • 40
    @Egon: Это очень креативно, но, к сожалению, так работают не компиляторы. Компилятор интерпретирует a[1] как серию токенов, а не строк: * ({целочисленное расположение} a {operator} + {integer} 1) совпадает с * ({целое число} 1 {оператор} + {целочисленное местоположение of} a), но не совпадает с * ({целочисленное расположение} a {operator} + {operator} +)
Показать ещё 13 комментариев
Теги:
arrays
pointers
pointer-arithmetic

18 ответов

1819
Лучший ответ

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

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

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

*(a + 5)

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

*(5 + a)

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

  • 310
    Интересно, не похоже ли это на * ((5 * sizeof (a)) + a). Отличное объяснение.
  • 0
    Я полностью анальный ... поэтому я не мог устоять. ... оператор присваивания в названии также приводит меня к бананам ... но я не собираюсь быть такой большой ручкой. ;-)
Показать ещё 37 комментариев
273

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

  • 37
    Массивы не определяются с точки зрения указателей, но доступ к ним есть.
  • 5
    Я бы добавил «так что оно равно *(i + a) , что можно записать как i[a] ».
Показать ещё 2 комментария
199

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

Да, 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.

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

И, конечно,

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

  • 126
    На самом деле, это оценивается как ложное; первый термин "ABCD" [2] == 2 ["ABCD"] оценивается как true, или 1, и 1! = 'C': D
  • 8
    @Jonathan: та же самая двусмысленность приводит к редактированию оригинального названия этого поста. Являемся ли мы равными знаками математической эквивалентности, синтаксиса кода или псевдокода. Я утверждаю математическую эквивалентность, но поскольку мы говорим о коде, мы не можем избежать того, что мы рассматриваем все с точки зрения синтаксиса кода.
Показать ещё 19 комментариев
51

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

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

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

Чтобы ответить на вопрос буквально. Не всегда верно, что 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
    На самом деле «nan» не равен самому себе: cout << (a[5] == a[5] ? "true" : "false") << endl; false
  • 7
    @TrueY: Он утверждал, что специально для случая NaN (и, в частности, что x == x не всегда верно). Я думаю, что это было его намерение. Так что он технически правильный (и, возможно, как говорится, лучший вид правильного!).
Показать ещё 1 комментарий
24

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

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

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

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

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

  • 2
    Нет, технически они не одинаковы. Если вы определите некоторое b как int * const и сделаете его указателем на массив, это все еще указатель, означающий, что в таблице символов b ссылается на область памяти, в которой хранится адрес, который, в свою очередь, указывает на то, где находится массив ,
  • 3
    Очень хороший момент. Я помню очень неприятную ошибку, когда я определил глобальный символ как char s [100] в одном модуле, объявив его как extern char * s; в другом модуле. После связывания всего этого программа вела себя очень странно. Поскольку модуль, использующий объявление extern, использовал начальные байты массива в качестве указателя на символ.
Показать ещё 2 комментария
22

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

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)

}

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

  • 6
    О Боже!!! как кто-то может сказать, что предпочитает эту запись !!! Мне больно глаза !!!
  • 0
    Когда вы видите i[a][a][a] вы думаете, что я либо указатель на массив, либо массив указателя на массив или массив ... а a - это индекс. Когда вы видите a[a[a[i]]] , вы думаете, что a - указатель на массив или массив, а i - индекс.
Показать ещё 1 комментарий
17

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

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

а также

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

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

14

Не ответ, а просто пища для размышлений. Если класс имеет перегруженный индекс/индексный оператор, выражение 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
    Вы на самом деле пытались его скомпилировать? Существует множество операторов, которые не могут быть реализованы вне класса (то есть как нестатические функции)!
Показать ещё 3 комментария
9

У него очень хорошее объяснение в учебном пособии по баллам и атакам в 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';
  • 3
    + Я НЕ простое дополнение, потому что это арифметика указателя. если размер элемента a равен 1 (символ), то да, это как целое число +. Но если это (например) целое число, то оно может быть эквивалентно + 4 * i.
  • 0
    @AlexBrown Да, это арифметика указателей, и именно поэтому ваше последнее предложение неверно, если только вы сначала не приведете «a» к (char *) (при условии, что int равно 4 символам). Я действительно не понимаю, почему так много людей зацикливаются на фактическом значении результата арифметики с указателями. Вся цель арифметики указателей состоит в том, чтобы абстрагироваться от базовых значений указателя и позволить программисту думать об объектах, которыми манипулируют, а не об адресных значениях.
6

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

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

4

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

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

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

4

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

0

В 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/

0

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

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

0

В 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)

-3

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

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] 
  • 2
    Массивы не указатели.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню