Как двумерные массивы хранятся в памяти?

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int a[101][101];
    a[2][0]=10;
    cout<<a+2<<endl;
    cout<<*(a+2)<<endl;
    cout<<*(*(a+2));
    return 0;
}

Почему значения a+2 и *(a+2) одинаковы? Заранее спасибо!


person Shubham    schedule 05.07.2016    source источник
comment
Может быть просто совпадение, если вы верите в такие вещи...   -  person Mad Physicist    schedule 05.07.2016
comment
Даже если напечатанное значение указателя совпадает, тип не совпадает.   -  person Jarod42    schedule 05.07.2016
comment
Вам повезло! Сегодня и только сегодня на stackoverflow.com у нас особая акция: ответь на свой вопрос! Создайте небольшой массив, скажем, int a[2][2]. Поместите в него четыре значения, скажем, 1, 2, 3 и 4. Получите указатель на первый элемент, int *p=&a[0][0];. Затем посмотрите с помощью отладчика, на что указывает p, и ответьте на свой вопрос. Какая сделка!!   -  person Sam Varshavchik    schedule 05.07.2016
comment
Части совет.   -  person Quentin    schedule 05.07.2016
comment
Взгляните на этот мой старый ответ, он показывает, как массив массивов размещается в памяти.   -  person Some programmer dude    schedule 05.07.2016
comment
@MadPhysicist: это не случайно, но, как ни странно, это требуется по стандарту ... Мне пришлось дважды прочитать код, чтобы убедиться, что UB не задействован, но это совершенно правильный код.   -  person Serge Ballesta    schedule 05.07.2016
comment
@SergeBallesta. Я не был уверен, пока ты не написал. Определенно голосую за это.   -  person Mad Physicist    schedule 05.07.2016
comment
ср. действующий дубликат с сегодняшнего дня: stackoverflow.com/questions/38202077/ и один это дубликат: stackoverflow.com/questions/11552960/   -  person underscore_d    schedule 05.07.2016


Ответы (5)


a — это двумерный массив, то есть массив массивов. Но он превращается в указатель на массив при использовании в соответствующем контексте. Так:

  • в a+2, a распадается на указатель на массивы int размером 101. Когда вы передаете is в ostream, вы получаете адрес первого элемента этого массива, то есть &(a[2][0])
  • в *(a+2) по определению a[2]: это массив размером 101, начинающийся с a[2][0]. Он распадается на указатель на int, и когда вы передаете его в ostream, вы получаете адрес его первого элемента, то есть по-прежнему &(a[2][0])
  • **(a+2) по определению a[2][0]. Когда вы передаете его ostream, вы получаете его значение int, здесь 10.

Но будьте осторожны: a + 2 и a[2] оба являются указателями на один и тот же адрес (static_cast<void *>(a+2) совпадает с static_cast<void *>(a[2])), но они являются указателями на разные типы: первый указывает на массив int размером 101, последний на int.

person Serge Ballesta    schedule 05.07.2016
comment
Спасибо! Это было очень полезно. - person Shubham; 05.07.2016

Я попытаюсь объяснить вам, как память отображается компилятором:

Давайте рассмотрим более практичный пример многомерного массива:

int a[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};

Вы можете выполнить команду

x/10w a

В GDB и посмотрите на память:

0x7fffffffe750: 1   2   3   4
0x7fffffffe760: 5   6   7   8
0x7fffffffe770: 9   0

Каждый элемент хранится в типе int (32 бита/4 байта). Таким образом, первый элемент матрицы был сохранен в:

1) a[0][0] -> 0x7fffffffe750
2) a[0][1] -> 0x7fffffffe754
3) a[0][2] -> 0x7fffffffe758
4) a[1][0] -> 0x7fffffffe75c
5) a[1][1] -> 0x7fffffffe760
6) a[1][2] -> 0x7fffffffe764
7) a[2][0] -> 0x7fffffffe768
        ...

Команда:

std::cout << a + 2 << '\n'

Он напечатает адрес 0x7fffffffe768 из-за арифметики указателя: тип aint**, поэтому это указатель на указатели. a+2 — это a[0] (первая строка) + 2. Результатом является указатель на третью строку.

*(a+2) уважает третью строку, это {7,8,9}

Третья строка представляет собой массив int, это указатель на int.

Затем оператор‹‹ напечатает значение этого указателя.

person BiagioF    schedule 05.07.2016
comment
В этом примере тип a на самом деле не int[3][3]? - person wally; 05.07.2016
comment
@flatmouse ты прав. Точнее будет сказать, что это тип int[3][3], потому что размер статический, время компиляции. - person BiagioF; 05.07.2016

Двумерный массив — это массив массивов, поэтому он хранится в памяти следующим образом:

char v[2][3] = {{1,3,5},{5,10,2}};

Content: | 1 | 3 | 5 | 5 | 10 | 2
Address:   v  v+1 v+2 v+3 v+4 v+5

Чтобы получить доступ к v[x][y], компилятор переписывает его как: *(v + y * M + x) (где M — указанное второе измерение)

Например, для доступа к v[1][1] компилятор переписывает его как *(v + 1*3 + 1) => *(v + 4)

Имейте в виду, что это не то же самое, что указатель на указатель (char**). Указатель на указатель не является массивом: он содержит и адрес на ячейку памяти, которая содержит другой адрес.

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

char **p;
/* Initialize it */
char c = p[3][5];
  1. Перейти по адресу, указанному в содержимом p;
  2. Добавьте смещение к этому адресу (3 в нашем случае);
  3. Перейдите по этому адресу и получите его содержимое (наш новый адрес).
  4. Добавьте второе смещение к этому новому адресу (5 в нашем случае).
  5. Получите содержимое этого адреса.

Чтобы получить доступ к элементу через традиционный двумерный массив, выполните следующие действия:

char p[10][10];
char c = p[3][5];
  1. Получите адрес pи суммируйте первое смещение (3), умноженное на размер строки (10).
  2. Добавьте второе смещение (5) к результату.
  3. Получите содержимое этого адреса.
person Mattia F.    schedule 05.07.2016

Если у вас есть такой массив

T a[N];

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

Так например в выражении ( a + 2 ) a преобразуется тип T * со значением &a[0].

Относительно вашего примера с массивом

int a[101][101];

в выражении

a + 2

a преобразуется в rvalue типа int ( * )[101] и указывает на первую «строку» массива. a + 2 указывает на третью «строку» массива. Тип строки int[101]

Выражение *(a+2) дает эту третью строку, которая имеет тип int[101], который является массивом. И этот массив по мере того, как он используется в выражении, в свою очередь преобразуется в указатель на его первый элемент типа int *.

Это тот же начальный адрес области памяти, которую занимает третья строка.

Только выражение ( a + 2 ) имеет тип int ( * )[101], а выражение *( a + 2 ) имеет тип int *. Но оба дают одно и то же значение - начальный адрес области памяти, занимаемой третьей строкой массива a.

person Vlad from Moscow    schedule 05.07.2016
comment
Спасибо! Это было очень полезно. - person Shubham; 05.07.2016

Первый элемент массива находится в том же месте, что и сам массив — в массиве нет «пустого места».

В cout << a + 2 a неявно преобразуется в указатель на свой первый элемент, &a[0], а a + 2 является расположением третьего элемента a, &a[2].

В cout << *(a + 2) массив *(a + 2), то есть a[2], преобразуется в указатель на его первый элемент, &a[2][0].

Поскольку расположение третьего элемента a и расположение первого элемента третьего элемента a совпадают, вывод будет таким же.

person molbdnilo    schedule 05.07.2016