Одномерный доступ к многомерному массиву: хорошо ли определено поведение? - PullRequest
16 голосов
/ 09 июня 2011

Я думаю, что мы все согласны с тем, что считается идиоматическим C для доступа к истинному многомерному массиву путем разыменования (возможно смещенного) указателя на его первый элемент в одномерном виде, например:

void clearBottomRightElement(int *array, int M, int N)
{
    array[M*N-1] = 0;  // Pretend the array is one-dimensional
}


int mtx[5][3];
...
clearBottomRightElement(&mtx[0][0], 5, 3);

Тем не менее, языковой адвокат во мне нуждается в убеждении, что это на самом деле четко определенный C! В частности:

  1. Гарантирует ли стандарт, что компилятор не поместит отступ между ними, например mtx[0][2] и mtx[1][0]?

  2. Обычно индексирование конца массива (кроме одного после конца) не определено (C99, 6.5.6 / 8). Таким образом, следующее явно не определено:

    struct {
        int row[3];           // The object in question is an int[3]
        int other[10];
    } foo;
    int *p = &foo.row[7];     // ERROR: A crude attempt to get &foo.other[4];
    

    Таким образом, по тому же правилу можно ожидать, что следующее будет неопределенным:

    int mtx[5][3];
    int (*row)[3] = &mtx[0];  // The object in question is still an int[3]
    int *p = &(*row)[7];      // Why is this any better?
    

    Так почему это должно быть определено?

    int mtx[5][3];
    int *p = &(&mtx[0][0])[7];
    

Так, какая часть стандарта C явно разрешает это? (Предположим, ради обсуждения.)

EDIT

Обратите внимание, что я не сомневаюсь, что это прекрасно работает во всех компиляторах. Я спрашиваю, разрешено ли это стандартом.

Ответы [ 4 ]

13 голосов
/ 09 июня 2011

Все массивы (включая многомерные) не содержат отступов. Даже если это никогда явно не упоминается, оно может быть выведено из sizeof правил.

Теперь подписка на массив является частным случаем арифметики указателей, и в разделе 6.5.6, §8 раздела C99 четко указано, что поведение определяется только в том случае, если операнд указателя и результирующий указатель лежат в одном и том же массиве (или один элемент после) , что делает возможной реализацию проверок границ языка Си.

Это означает, что ваш пример - фактически неопределенное поведение. Однако, поскольку большинство реализаций C не проверяют границы, оно будет работать как положено - большинство компиляторов обрабатывают неопределенные выражения указателя, такие как

mtx[0] + 5 

идентично четко определенным аналогам, таким как

(int *)((char *)mtx + 5 * sizeof (int))

, который четко определен, потому что любой объект (включая весь двумерный массив) всегда может рассматриваться как одномерный массив типа char.


О дальнейшей медитации над формулировкой раздела 6.5.6, разделив доступ за пределы границ на, казалось бы, четко выраженное подвыражение, подобное

(mtx[0] + 3) + 2

обоснование того, что mtx[0] + 3 является указателем на один элемент после конца mtx[0] (что делает первое добавление четким), а также указателем на первый элемент mtx[1] (что делает второе добавление правильным -определено) неверно:

Даже если mtx[0] + 3 и mtx[1] + 0 гарантированно сравниваются равными (см. Раздел 6.5.9, §6), они семантически различны. Например, первое не может быть разыменовано и поэтому не указывает на элемент mtx[1].

9 голосов
/ 09 июня 2011

Единственное препятствие для вида доступа, который вы хотите сделать, - это то, что объектам типа int [5][3] и int [15] не разрешено накладывать псевдонимы друг на друга. Таким образом, если компилятор знает, что указатель типа int * указывает на один из int [3] массивов первого, это может наложить ограничения на границы массива, которые будут препятствовать доступу к чему-либо вне этого int [3] массива.

Возможно, вам удастся обойти эту проблему, поместив все в объединение, содержащее как массив int [5][3], так и массив int [15], но мне действительно неясно, использует ли объединение хаки, которые люди используют для наложения типов на самом деле четко определены. Этот случай может быть немного менее проблематичным, поскольку вы не будете вводить отдельные ячейки, а только логику массива, но я все еще не уверен.

Один особый случай, который следует отметить: если бы ваш тип был unsigned char (или любой тип char), доступ к многомерному массиву как одномерному массиву был бы совершенно четко определен. Это связано с тем, что одномерный массив unsigned char, который перекрывает его, явно определен стандартом как «представление» объекта, и ему по своей природе разрешено псевдоним.

2 голосов
/ 09 июня 2011
  1. Уверен, что между элементами массива нет заполнения.

  2. Существуют условия для вычисления адресов меньшего размера, чем полное адресное пространство.Это можно использовать, например, в огромном режиме 8086, чтобы часть сегмента не всегда обновлялась, если компилятор знал, что вы не можете пересечь границу сегмента.(Мне уже давно не хотелось напоминать, использовали ли я используемые компиляторы или нет).

С моей внутренней моделью - я не уверен, что она совершенно одинаковакак стандартный, и это слишком больно проверять, информация, распространяемая повсюду -

  • , то, что вы делаете в clearBottomRightElement, действительна.

  • int *p = &foo.row[7]; не определено

  • int i = mtx[0][5]; не определено

  • int *p = &row[7]; не компилируется (gcc согласен со мной)

  • int *p = &(&mtx[0][0])[7]; находится в серой зоне (в последний раз, когда я проверял в деталях что-то вроде этого, я в итоге выбрал недействительный C90 и действительный C99, это может быть случай здесь илиЯ мог что-то пропустить).

0 голосов
/ 09 июня 2011

Мое понимание C99 стандарта заключается в том, что нет требования о том, что многомерные массивы должны быть расположены в непрерывном порядке в памяти. В соответствии с единственной соответствующей информацией, которую я нашел в стандарте (каждое измерение равно гарантированно является смежным).

Если вы хотите использовать доступ x [COLS * r + c], я предлагаю вам использовать одномерные массивы.

Массив подписки

Последовательные операторы нижних индексов обозначают элемент объекта многомерного массива. Если E - это n-мерный массив (n ≥ 2) с размерами i × j ×. , , × k, то E (используется как кроме lvalue) преобразуется в указатель на (n - 1) -мерный массив с размеры j ×. , , × к. Если унарный оператор * применяется к этому указателю явно, или неявно в результате подписки, результатом является (n - 1) -мерный массив, который сам по себе преобразуется в указатель, если используется как lvalue. Из этого следует что массивы хранятся в главном порядке строк (последний индекс изменяется быстрее всего).

Тип массива

- Тип массива описывает непрерывно распределенный непустой набор объектов с определенный тип объекта члена, называемый типом элемента. 36) Типы массивов характеризуется типом элемента и количеством элементов в массиве. Тип массива считается производным от его типа элемента, и если его тип элемента равен T, Тип массива иногда называют «массивом T». Построение типа массива из тип элемента называется «деривация типа массива».

...