Индексация 2D-массива - неопределенное поведение?

Недавно я столкнулся с некоторыми фрагментами кода, выполняющими некоторые сомнительные операции индексации 2D-массивов. Рассмотрим в качестве примера следующий пример кода:

int a[5][5];
a[0][20] = 3;
a[-2][15] = 4;
a[5][-3] = 5;

Подвержены ли описанные выше операции индексации неопределенному поведению?


person dragosht    schedule 05.08.2014    source источник
comment
Есть хороший дубликат этого, но я не могу его найти, функция поиска SO намного хуже, чем воспоминания людей.   -  person M.M    schedule 05.08.2014
comment
Возможный дубликат здесь, но не уверен, что мы должны закрыть его, хотя , так как другой вопрос не в хорошем смысле, кроме того, принятый ответ здесь лучше...   -  person Aconcagua    schedule 07.02.2020


Ответы (3)


Это неопределенное поведение, и вот почему.

Доступ к многомерному массиву можно разбить на серию обращений к одномерному массиву. Другими словами, выражение a[i][j] можно рассматривать как (a[i])[j]. Цитируя C11 §6.5.2.1/2:

Определение оператора нижнего индекса [] состоит в том, что E1[E2] идентично (*((E1)+(E2))).

Это означает, что приведенное выше идентично *(*(a + i) + j). После C11 §6.5.6/8 относительно добавления целого числа и указателя (выделено мной):

Если и операнд-указатель, и результат указывают на элементы одного и того же объекта-массива или на элементы, следующие за последним элементом объекта-массива, оценка не должна вызывать переполнения; в противном случае поведение не определено.

Другими словами, если a[i] не является допустимым индексом, поведение сразу же становится неопределенным, даже если "интуитивно" a[i][j] кажется ограниченным.

Таким образом, в первом случае a[0] допустимо, а в следующем [20] нет, потому что тип a[0] равен int[5]. Следовательно, индекс 20 выходит за пределы.

Во втором случае a[-1] уже находится за пределами поля, то есть уже UB.

Однако в последнем случае выражение a[5] указывает на один после последнего элемента массива, что допустимо в соответствии с §6.5.6/8:

... если выражение P указывает на последний элемент объекта массива, выражение (P)+1 указывает на единицу после последнего элемента объекта массива...

Однако далее в том же абзаце:

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

Таким образом, несмотря на то, что a[5] является допустимым указателем, его разыменование приведет к неопределенному поведению, вызванному окончательной индексацией [-3] (которая также находится за пределами границ, поэтому UB).

person Drew McGowen    schedule 05.08.2014
comment
[…] потому что тип a[0] — это int[5] […] — это та часть, где я застрял. a[0] здесь подвергается преобразованию lvalue, поэтому оно затухает до int *. Не уверен в этом… - person mafso; 05.08.2014
comment
Несмотря на то, что он распадается на int *, он по-прежнему является указателем на массив (который, как я склонен полагать, считается состоящим только из 5 элементов). - person Drew McGowen; 05.08.2014
comment
@mafso a[0] имеет тип int[5] ; испорченный указатель представляет собой значение rvalue (и указывает на объект, представляющий собой массив из 5 целых чисел) - person M.M; 05.08.2014
comment
И a[5] не разыменовывается, a[5]-3 да (что может быть допустимо)… В стандарте явно сказано, что массивы хранятся непрерывно (без заполнения и увеличения адресов) и явно указано, что доступ за пределы является УБ. Я не совсем понимаю, как они могут быть вместе… - person mafso; 05.08.2014
comment
@mafso на самом деле, a[5]-3 означает, что a + 5 разыменован. Как я уже говорил, a[5][-3] эквивалентно *(*(a + 5) - 3); выражение *(a + 5) есть UB. - person Drew McGowen; 05.08.2014
comment
@MattMcNabb: я понимаю вашу точку зрения. Но это будет означать, что memcpyпревращение int[5][5] в unsigned char[25*sizeof(int)] также будет UB. Или, скажем, у нас будет функция wordcpy, принимающая int указателей (но в остальном похожая на memcpy), будет ли wordcpy(tgt, a[0], 25) UB? - person mafso; 05.08.2014
comment
@mafso memcpy(&a[0][0], x, 25*sizeof**a); является UB, но выполнение &a[0] или &a нормально, потому что в этом случае указатель указывает на массив такого размера, а не на массив меньшего размера - person M.M; 05.08.2014
comment
имейте в виду, что указателю разрешено также хранить границы того, на что он указывает, так что реализация проверки границ является законной. Границы определяются тем, членом какого объекта является объект, на который указывает указатель. - person M.M; 05.08.2014
comment
@MattMcNabb: я согласен. Спасибо! - person mafso; 05.08.2014

Да, это неопределенное поведение.

person Morgan Wilde    schedule 05.08.2014
comment
Абсолютно неопределенный. Я даже не думаю, что существует строгое требование к порядку строк по сравнению с порядком столбцов (или наоборот). - person Edwin Buck; 05.08.2014
comment
Вы уверены в последнем? a[5] никогда не используется, оценивается только действительный адрес. Если мои рассуждения верны (в чем я очень не уверен), вопрос становится эквивалентным a[4][-3], где я тоже не уверен, что он определен (но я не могу представить платформу, на которой он не работает). - person mafso; 05.08.2014
comment
@мафсо &a[5][-3] != &a[4][-3] - person Drew McGowen; 05.08.2014
comment
@DrewMcGowen: Может быть, я немного не понял. Я задавался вопросом, действительно ли a[4][-3], и если это так, то я могу представить, что a[5][-3] также был определен (который имел бы дополнительное требование, чтобы a[5] был действительным, если он распадается, подобно тому, как &a[5] является действительным). - person mafso; 05.08.2014
comment
Если это ваша причина понизить голосование @Lundin, то вы совершенно неправильно поняли, для чего нужно понизить голос на SO. - person Morgan Wilde; 05.08.2014
comment
Я почти уверен, что отрицательные голоса используются для плохих ответов. - person Lundin; 05.08.2014
comment
Является ли мой ответ неправильным, вводящим в заблуждение, спамом и т. д.? Пожалуйста, прочитайте вопрос, слово в слово, это то, ЧТО просил ОП, и это достоверно. Но я уверен, что если вы хотите скрыть простой ответ за стеной текста, будьте моим гостем @Lundin. - person Morgan Wilde; 05.08.2014
comment
Но вы не объясняете, почему это поведение undefined. Вы не объясняете, что это за неопределенное поведение. Связано ли это с алиасингом или доступом за пределы, или это просто еще один случай, когда стандарт C формально помечает что-то UB, хотя на практике это всегда будет работать, или что? Это не тривиальный вопрос, это довольно хороший вопрос, который требует определенного опыта от человека, который отвечает... лично я не смог бы ответить почему, не взглянув поближе на C стандарт. Таким образом, хороший ответ будет цитировать соответствующую часть указанного стандарта. - person Lundin; 05.08.2014
comment
Я не утверждаю, что это лучший возможный ответ, но и отсутствие такового не означает, что он плохой. Удачи вам, сэр. - person Morgan Wilde; 05.08.2014

индексация массива с отрицательными индексами является неопределенным поведением. Извините, что a[-3] совпадает с *(&a - 3) в большинстве архитектур/компиляторов и принимается без предупреждения, но язык C позволяет вам добавлять отрицательные целые числа к указателям, но не использовать отрицательные значения в качестве индексов массива. К сожалению, это даже не проверяется во время выполнения.

Кроме того, есть некоторые проблемы, с которыми следует ознакомиться при определении массивов перед указателями. Можно оставить неуказанным только первый субиндекс и не более, как в:

int a[][3][2]; /* array of unspecified size, definition is alias of int (*a)[3][2]; */

(действительно, это определение указателя, а не массива, просто напечатайте sizeof a)

or

интервал [4] [3] [2]; /* массив из 24 целых чисел, размер 24*sizeof(int) */

когда вы делаете это, способ оценки смещения для массивов отличается от для указателей, поэтому будьте осторожны. В случае массивов int a[I][J][K];

&a[i][j][k] 

находится в

&a + i*(sizeof(int)*J*K) + j*(sizeof(int)*K) + k*(sizeof(int))

но когда вы объявляете

int ***a; 

тогда a[i][j][k] совпадает с:

*(*(*(&a+i)+j)+k), что означает, что вы должны разыменовать указатель a, затем добавить (sizeof(int **))*i к его значению, затем снова разыменовать, затем добавить (sizeof (int *))*j к этому значению, затем разыменовать его и добавить (sizeof(int))*k к этому значению, чтобы получить точный адрес данных.

BR

person Luis Colorado    schedule 06.08.2014
comment
int a[][3][2]; является незаконным. Вы должны либо указать первое измерение, либо указать инициализатор, из которого вычисляется первое измерение. Это не псевдоним указателя. Вы можете запутаться со значением декларатора массива в списке параметров функции, но в этом случае int a[4][3][2] также int (*a)[3][2]. - person M.M; 24.02.2015
comment
в &a + i * (sizeof... вы имели в виду (char *)&a ; арифметика указателя выполняется с точки зрения размера объекта, на который указывает - person M.M; 24.02.2015
comment
a[i][j][k] совпадает с *(*(*(a+i)+j)+k) (обратите внимание на отсутствие &) - person M.M; 24.02.2015