Многомерные многомерные массивы C/С++

У меня вопрос о том, как C/С++ внутренне хранит многомерные массивы, объявленные с использованием обозначения foo[m][n]. Я не ставил под сомнение чистые указатели на указатели и т.д.... Я спрашиваю из-за скоростных причин...

Исправьте меня, если я ошибаюсь, но синтаксически foo - это массив указателей, которые сами указывают на массив

int foo[5][4]
*(foo + i)           // returns a memory address
*( *(foo + i) + j)    // returns an int

Я слышал из многих мест, что компилятор C/С++ преобразует foo[m][n] в одномерный массив за кулисами (вычисляя требуемый индекс измерения с помощью i * width + j). Однако, если это верно, то следующее:

*(foo + 1)          // should return element foo[0][1]

Таким образом, мой вопрос: Верно ли, что foo[m][n] (всегда?) Хранится в памяти как плоский одномерный массив? Если да, то почему приведенный выше код работает, как показано.

Ответ 1

Да, C/С++ хранит многомерный (прямоугольный) массив в качестве смежной области памяти. Но ваш синтаксис неверен. Чтобы изменить элемент foo[0][1], следующий код будет работать:

*((int *)foo+1)=5;

Явный листинг необходим, потому что foo+1 совпадает с &foo[1], который не является тем же самым, что и foo[0][1]. *(foo+1) является указателем на пятый элемент в области плоской памяти. Другими словами, *(foo+1) в основном foo[1], а **(foo+1) - foo[1][0]. Вот как выкладывается память для некоторых ваших двухмерных массивов:

enter image description here

Ответ 2

Двумерный массив:

int foo[5][4];

- это не что иное, как массив массивов:

typedef int row[4];   /* type "row" is an array of 4 ints */
row foo[5];           /* the object "foo" is an array of 5 rows */

Здесь нет объектов-указателей, явных или неявных.

Массивы не являются указателями. Указатели не являются массивами.

Что часто вызывает путаницу, так это то, что выражение массива в большинстве контекстов неявно преобразуется в указатель на его первый элемент. (И в отдельном правиле говорится, что то, что выглядит как объявление параметра массива, действительно является объявлением указателя, но это не применяется в этом примере.) Объект массива - это объект массива; объявление такого объекта не создает никаких объектов-указателей. Ссылаясь на объект массива, можно создать значение указателя (адрес первого элемента массива), но в памяти отсутствует объект-указатель.

Объект массива foo хранится в памяти как 5 смежных элементов, где каждый элемент представляет собой массив из 4 смежных элементов int; поэтому все это хранится как 20 смежных объектов int.

Оператор индексирования определяется в терминах арифметики указателя; x[y] эквивалентно *(x + y). Обычно левый операнд будет либо выражением указателя, либо выражением массива; если это выражение массива, массив неявно преобразуется в указатель.

So foo[x][y] эквивалентно *(foo[x] + y), что в свою очередь эквивалентно *(*(foo + x) + y). (Обратите внимание, что никаких бросков не требуется.) К счастью, вам не нужно писать так, а foo[x][y] намного легче понять.

Обратите внимание, что вы можете создать структуру данных, к которой можно получить доступ с помощью синтаксиса foo[x][y], но где foo действительно является указателем на указатель на int. (В этом случае префикс каждого оператора [] уже является выражением указателя и не нуждается в преобразовании.) Но для этого вам нужно объявить foo как указатель на указатель -в-ИНТ:

int **foo;

а затем выделите и инициализируйте всю необходимую память. Это более гибко, чем int foo[5][4], так как вы можете определить количество строк и размер (или даже существование) каждой строки динамически.

В разделе 6 раздела comp.lang.c FAQ это очень хорошо объясняется.

EDIT:

В ответ на комментарий Арракиса важно помнить о различии между типом и представлением.

Например, эти два типа:

struct pair { int x; int y;};
typedef int arr2[2];

скорее всего, имеют одинаковое представление в памяти (два последовательных int объекта), но синтаксис доступа к элементам совершенно другой.

Аналогично, типы int[5][4] и int[20] имеют одинаковый макет памяти (20 последовательных int объектов), но синтаксис для доступа к элементам отличается.

Вы можете получить доступ к foo[2][2] как ((int*)foo)[10] (обработка двумерного массива, как если бы это был 1-мерный массив). И иногда это полезно для этого, но, строго говоря, поведение undefined. Вы, вероятно, избежите этого, потому что большинство реализаций C не выполняют проверку границ массива. С другой стороны, оптимизация компиляторов может предполагать, что поведение вашего кода определено и генерировать произвольный код, если это не так.

Ответ 3

C массивы - даже многомерные - являются смежными, т.е. массив типа int [4][5] структурно эквивалентен массиву типа int [20].

Однако эти типы по-прежнему несовместимы в соответствии с семантикой языка C. В частности, следующий код нарушает стандарт C:

int foo[4][5] = { { 0 } };
int *p = &foo[0][0];
int x = p[12]; // undefined behaviour - can't treat foo as int [20]

Причиной этого является то, что стандарт C (возможно, намеренно) сформулирован таким образом, который делает возможными реализации проверки границ: поскольку p получен из foo[0], который имеет тип int [5], действительные индексы должны находиться в диапазоне 0..5 (соответственно 0..4, если вы действительно получаете доступ к элементу).

Многие другие языки программирования (Java, Perl, Python, JavaScript,...) используют зубчатые массивы для реализации многомерных массивов. Это также возможно в C с помощью массива указателей:

int *bar[4] = { NULL };
bar[0] = (int [3]){ 0 };
bar[1] = (int [5]){ 1, 2, 3, 4 };
int y = bar[1][2]; // y == 3

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

Из-за неявного преобразования выражений массива в выражения указателя индексирование зубчатых и нехарактерных массивов выглядит одинаково, но фактические вычисления адресов будут совсем другими:

&foo[1]    == (int (*)[5])((char *)&foo + 1 * sizeof (int [5]))

&bar[1]    == (int **)((char *)&bar + 1 * sizeof (int *))

&foo[1][2] == (int *)((char *)&foo[1] + 2 * sizeof (int))
           == (int *)((char *)&foo + 1 * sizeof (int [5]) + 2 * sizeof (int))

&bar[1][2] == (int *)((char *)bar[1] + 2 * sizeof (int)) // no & before bar!
           == (int *)((char *)*(int **)((char *)&bar + 1 * sizeof (int *))
                      + 2 * sizeof (int))

Ответ 4

int foo[5][4];

foo не является массивом указателей; это массив массивов. Ниже изображение поможет.

введите описание изображения здесь