Гарантирует ли C99, что массивы смежны? - PullRequest
17 голосов
/ 14 мая 2010

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

Обычно, когда я определяю двумерный массив, например int a[5][5], стандартная гарантия C99 или нет, будет ли это непрерывный блок целых чисел, могу ли я привести его к (int *)a и быть уверенным, что у меня будет действительный одномерный массив 25 дюймов.

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

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

Ответы [ 3 ]

18 голосов
/ 14 мая 2010

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

  • INT
  • массив [5] целых
  • массив [5] массивов

Не задействован массив [25] целых.

Правильно, что размер семантики подразумевает, что массив в целом является смежным. Массив [5] из ints должен иметь 5 * sizeof (int) и рекурсивно применяться, a [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 *) было бы неопределенным поведением, потому что, как уже говорилось, нет задействованного массива [25] для int. Использование союза, как в ответе Кристофа, также должно быть правильным. Но есть еще один момент, усложняющий это далее, оператор равенства:

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

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

Это значит для этого:

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

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

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

Даже при этом

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

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

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

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

11 голосов
/ 14 мая 2010

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

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

В принципе, вы можете обойти это, приведя &a - который имеет тип int (*)[5][5] - к 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.

2 голосов
/ 14 мая 2010

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

...