Почему с массивами, почему [5] == 5[a]?
Как указывает Джоэл в подкасте № 34 "Переполнение стека" на языке программирования C (он же K & R), в C упоминается это свойство массивов: a[5] == 5[a]
Джоэл говорит, что это из-за арифметики указателей, но я все еще не понимаю. Почемуa[5] == 5[a]
?
22 ответа
Стандарт C определяет []
Оператор следующим образом:
a[b] == *(a + b)
Следовательно a[5]
будет оценивать:
*(a + 5)
а также 5[a]
будет оценивать:
*(5 + a)
a
указатель на первый элемент массива a[5]
это значение, которое на 5 элементов дальше a
, который так же, как *(a + 5)
и из математики начальной школы мы знаем, что они равны (сложение коммутативно).
Потому что доступ к массиву определяется в терминах указателей. a[i]
определяется как означающее *(a + i)
, который является коммутативным.
Я думаю, что что-то упускается другими ответами.
Да, p[i]
по определению эквивалентно *(p+i)
, который (потому что сложение коммутативно) эквивалентно *(i+p)
что (опять же по определению []
оператор) эквивалентно i[p]
,
(И в array[i]
имя массива неявно преобразуется в указатель на первый элемент массива.)
Но коммутативность сложения не так уж очевидна в этом случае.
Когда оба операнда имеют один и тот же тип или даже разные числовые типы, которые переводятся в общий тип, коммутативность имеет смысл: x + y == y + x
,
Но в этом случае мы говорим конкретно об арифметике указателей, где один операнд является указателем, а другой - целым числом. (Целое число + целое число - это другая операция, а указатель + указатель - нонсенс.)
Описание стандарта С +
оператор ( 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?).
Таким образом, идея о том, что индексация массива определяется с точки зрения сложения и что сложение, даже указателя и целого числа, является коммутативной, восходит на многие десятилетия к языкам-предкам Си.
Эти языки были гораздо менее строго типизированы, чем современные языки. В частности, различие между указателями и целыми числами часто игнорировалось. (Ранние программисты на C иногда использовали указатели как целые числа без знака до unsigned
К языку было добавлено ключевое слово.) Таким образом, идея сделать сложение некоммутативным, поскольку операнды бывают разных типов, вероятно, не пришла бы в голову разработчикам этих языков. Если пользователь хотел добавить две "вещи", будь то эти "вещи", являются целыми числами, указателями или чем-то еще, язык не мог предотвратить это.
И на протяжении многих лет любое изменение этого правила нарушало бы существующий код (хотя стандарт ANSI C 1989 года мог бы стать хорошей возможностью).
Изменение C и / или C++, требующее размещения указателя слева и целого числа справа, может нарушить некоторый существующий код, но при этом не будет потеря реальной выразительной силы.
Итак, теперь у нас есть arr[3]
а также 3[arr]
это означает то же самое, хотя последняя форма никогда не должна появляться за пределами IOCCC.
И, конечно же,
("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
была доступна другая оптимизация на уровне сборки, и разработчику снова пришлось сделать это явным, потому что компилятор ее не распознал. (В последнее время это делают компиляторы, поэтому эти синтаксисы в настоящее время в основном не нужны)
Кажется, никто не упомянул о проблеме Дины с sizeof
:
Вы можете добавить только целое число к указателю, вы не можете добавить два указателя вместе. Таким образом, добавляя указатель на целое число или целое число на указатель, компилятор всегда знает, какой бит имеет размер, который необходимо учитывать.
Ответить на вопрос буквально. Это не всегда правда, что 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
Я просто обнаружил, что этот уродливый синтаксис может быть "полезен" или, по крайней мере, очень забавно играть, когда вы хотите иметь дело с массивом индексов, которые ссылаются на позиции в одном и том же массиве. Он может заменить вложенные квадратные скобки и сделать код более читабельным!
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)
}
Конечно, я вполне уверен, что в реальном коде для этого нет смысла, но я все равно нашел это интересным:)
Хороший вопрос / ответы.
Сразу хочу отметить, что указатели и массивы C не совпадают, хотя в этом случае разница несущественна.
Рассмотрим следующие объявления:
int a[10];
int* p = a;
В a.out символ a находится по адресу, который является началом массива, а символ p - по адресу, где хранится указатель, а значение указателя в этой ячейке памяти является началом массива.
Для указателей в C мы имеем
a[5] == *(a + 5)
а также
5[a] == *(5 + a)
Следовательно, это правда, что a[5] == 5[a].
Не ответ, а просто пища для размышлений. Если класс перегружен оператором индекса / индекса, выражение 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&);
};
У него очень хорошее объяснение в "Учебнике по указателям и массивам в Си" Теда Дженсена.
Тед Дженсен объяснил это так:
На самом деле это так, то есть везде, где пишут
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';
Я знаю, что на вопрос дан ответ, но я не удержался, чтобы поделиться этим объяснением.
Я помню принципы компиляции, давайте предположим 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.
А теперь немного истории. Среди других языков 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
стал вещью.
В массивах Carr[3]
а также 3[arr]
являются одинаковыми, и их эквивалентные обозначения указателя *(arr + 3)
в *(3 + arr)
, А наоборот [arr]3
или же [3]arr
не правильно и приведет к синтаксической ошибке, так как (arr + 3)*
а также (3 + arr)*
не являются допустимыми выражениями. Причина в том, что оператор разыменования должен быть помещен перед адресом, полученным выражением, а не после адреса.
В компиляторе c
a[i]
i[a]
*(a+i)
разные способы ссылки на элемент в массиве! (Не совсем странно)
C был основан на BCPL. BCPL напрямую раскрывает память как последовательность адресуемых слов. Унарный оператор!X
(также известный как LV) дал вам содержимое адресной ячейки X. Для удобства был также бинарный оператор, эквивалентный которому, выдавал вам содержимое Y-го слова массива в ячейке X или, что то же самое, X' слово массива в ячейке Y.
В С,X!Y
сталX[Y]
, но исходная семантика BCPL!(X+Y)
показать насквозь, что объясняет, почему оператор коммутативен.
Ну, это особенность, которая возможна только из-за языковой поддержки.
Компилятор интерпретирует a[i]
как *(a+i)
и выражение 5[a]
оценивает *(5+a)
, Поскольку сложение коммутативно, получается, что оба равны. Следовательно, выражение оценивает true
,
Поскольку компилятор C всегда преобразует нотацию массива в нотацию указателя.
a[5] = *(a + 5)
также
5[a] = *(5 + a) = *(a + 5)
Итак, оба равны.
Потому что это полезно, чтобы избежать путаницы с вложением.
Вы бы предпочли это прочитать:
array[array[head].next].prev
или это:
head[array].next[array].prev
?
В С
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)
В языке C указатель и массив очень близки друг к другу, массив можно разбить в виде указателя. Имя массива является указателем на его первый элемент. Таким образом, если acData является массивом символов, то "acData" будет адресом его первого элемента. Вы также можете сказать, что "acData" похожа на &acData [0].
Согласно стандарту C, мы можем представить одномерный массив в виде указателя.
Смотрите выражение ниже,
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;
}
Типы указателей
1) указатель на данные
int *ptr;
2) постоянный указатель на данные
int const *ptr;
3) константный указатель на константные данные
int const *const ptr;
и массивы типа (2) из нашего списка
Когда вы определяете массив одновременно, один адрес инициализируется в этом указателе
Поскольку мы знаем, что мы не можем изменить или изменить значение const в нашей программе, потому что она выдает ОШИБКУ во время компиляции
Основное различие, которое я нашел...
Мы можем повторно инициализировать указатель по адресу, но не в том же случае с массивом.
======
и вернемся к вашему вопросу...
[5] - это не что иное, как *(a + 5)
Вы можете легко понять,
содержащий адрес (люди называют его базовым адресом), как указатель типа (2) в нашем списке
[] - этот оператор может быть заменен указателем * .
так наконец...
a[5] == *(a +5) == *(5 + a) == 5[a]