Зависит от типа массива.
int **a;
// или vector<vector<int>> a;
a[10][7];
Тут происходит 2 разименовывания указателя. Массив в памяти хранится строчками. Каждая строка может быть где угодно. При этом дополнительно хранится массив указателей на строки (длиной с длину столбца). Поэтому такой массив занимает в памяти
M*(sizeof(int*))+M*N*sizeof(int)
. Чуть сложнее для vector, но идея такая же.
int a[10][3];
a[4][5];
Тут массив, хоть и многомерный, но фиксированного размера. Поэтому он хранится одним блоком. Компилятор знает длины всех строк и сразу вычисляет адрес конкретного элемента - сдвигаясь на (длину строки)*(номер строки)+(номер столбца). Он занимает
N*M*sizeof(int)
.
Сравните ассемблерный
код.
Кстати, именно поэтому вы не можете преобразовать
int[4][5]
к
int**
. И такой массив при передаче в функцию надо передавать по типу
int[][5]
(можно опустить количество строк. Ибо для адресации нужна лишь длина строк, но нестолбцов, но размер строки указать предется обязательно).
arr[1][2] => *(*(arr + 1) + 2)
Это действительно работает, потому что arr имеет тип
int[][3]
или
int*[3]
. Коспилятор видя
arr+1
, знает, что над сместится на 1 размер
int[3]
. * разыменовывает это, но при этом указывает на то же место. И получает просто указатель на int начало строки. Фактически тут просто меняется тип указателя с int*[3] на int*. +2 сдвигается в строке на 2 размера int.