У меня вопрос о том, как 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
не является массивом указателей; это массив массивов. Ниже изображение поможет.
![введите описание изображения здесь]()