Гарантирует ли C99, что массивы смежны?

После горячей темы комментариев в другом вопросе я пришел к обсуждению того, что есть и что не определено в стандарте C99 о C массивах.

В основном, когда я определяю 2D-массив, такой как int a[5][5], может ли стандартная гарантия C99 или нет, что он будет непрерывным блоком ints, могу ли я применить его к (int *)a и быть уверенным, что у меня будет действующий массив 1D из 25 ints.

Как я понимаю, стандартное свойство выше подразумевается в определении sizeof и в арифметике указателя, но другие, похоже, не согласны и говорят, что кастинг (int *) приведенной выше структуры дает поведение undefined (даже если они согласны с тем, что все существующие реализации фактически распределяют смежные значения).

В частности, если мы думаем о реализации, которая позволяла бы инструментальным массивам проверять границы массива для всех измерений и возвращать некоторую ошибку при доступе к массиву 1D или не предоставлять правильный доступ к элементам выше 1-й строки. Может ли такая реализация быть стандартным компилятором? И в этом случае важны части стандарта C99.

Ответ 1

Мы должны начать с проверки того, что такое int [5] [5]. Используемые типы:

  • ИНТ
  • массив [5] из ints
  • массив [5] массивов

Не существует массива [25] из ints.

Правильно, что из семантики sizeof следует, что массив в целом смежный. Массив [5] из int должен иметь 5 * sizeof (int) и рекурсивно применяться, а [5] [5] должен иметь размер 5 * 5 * sizeof (int). Нет дополнительного места для заполнения.

Кроме того, массив в целом должен работать, когда заданы memset, memmove или memcpy с sizeof. Также должно быть возможно выполнить итерацию по всему массиву с помощью (char *). Итак, действительная итерация:

int  a[5][5], i, *pi;
char *pc;

pc = (char *)(&a[0][0]);
for (i = 0; i < 25; i++)
{
    pi = (int *)pc;
    DoSomething(pi);
    pc += sizeof(int);
}

Выполнение такого же действия с (int *) будет иметь поведение undefined, потому что, как сказано, нет массива [25] из int. Использование союза, как и в случае Кристофа, тоже должно быть верным. Но есть еще один момент, усложняющий это далее: оператор равенства:

6.5.9.6 Два указателя сравнивают одинаковые, если и только если оба являются нулевыми указателями, оба являются указателями на один и тот же объект (включая указатель на объект и подобъект в начале) или функцию, оба являются указателями на один последний элемент одного и того же массива object, или один - указатель на один конец конца одного объекта массива, а другой - указатель на начало другого объекта массива, который происходит сразу же после первого объекта массива в адресном пространстве. 91)

91). Два объекта могут быть смежными в памяти, поскольку они являются смежными элементами более крупного массива или соседних элементов структуры без прокладки между ними или потому, что реализация выбрала их так, даже если они не связаны друг с другом. Если ранее недействительные операции с указателем (например, доступ к границам внешнего массива) вызвали поведение undefined, последующие сравнения также приводят к поведению undefined.

Это означает:

int a[5][5], *i1, *i2;

i1 = &a[0][0] + 5;
i2 = &a[1][0];

i1 сравнивается как равное i2. Но при итерации по массиву с помощью (int *) поведение по-прежнему undefined, потому что оно изначально получено из первого подмассива. Он не волшебным образом преобразует указатель во второй подмассив.

Даже при выполнении этого

char *c = (char *)(&a[0][0]) + 5*sizeof(int);
int  *i3 = (int *)c;

не поможет. Он сравнивается с i1 и i2, но не выводится ни из одного из подмассивов; это указатель на один int или массив [1] из int в лучшем случае.

Я не считаю это ошибкой в ​​стандарте. Другое дело: разрешить это ввести специальный случай, который нарушает либо систему типов для массивов, либо правила для арифметики указателей или и то, и другое. Это может считаться отсутствующим определением, но не ошибкой.

Итак, даже если макет памяти для [5] [5] идентичен макету [25], и тот же цикл, в котором используется (char *), может использоваться для итерации по обоим, реализация может взорваться, если используется как другая. Я не знаю, почему он должен знать или знать какую-либо реализацию, которая могла бы быть, и, возможно, в стандарте не упоминается ни одного факта, который до сих пор не упоминается, что делает его четко определенным поведением. До тех пор я бы считал, что это undefined и оставаться на безопасной стороне.

Ответ 2

Я добавил еще несколько комментариев к нашей оригинальной дискуссии.

Семантика

sizeof подразумевает, что int a[5][5] является смежным, но посещение всех 25 целых чисел с помощью приращения указателя, такого как int *p = *a, - это поведение undefined: арифметика указателя определяется только до тех пор, пока все указываемые указатели лежат внутри (или один элемент, прошедший последний элемент) того же массива, например, &a[2][1] и &a[3][1] нет (см. раздел C99 6.5.6).

В принципе, вы можете обойти это, набрав &a - который имеет тип int (*)[5][5] - to int (*)[25]. Это является законным в соответствии с пунктом 6.3.2.3 § 7, поскольку оно не нарушает каких-либо требований к выравниванию. Проблема в том, что доступ к целым числам через этот новый указатель является незаконным, поскольку он нарушает правила псевдонимов в 6.5 §7. Вы можете обойти это, используя union для записи типа (см. Сноску 82 в TC3):

int *p = ((union { int multi[5][5]; int flat[25]; } *)&a)->flat;

Это, насколько я могу судить, соответствует стандартам C99.

Ответ 3

Если массив статичен, как ваш массив int a[5][5], он должен быть смежным.