Зависит от типа массива.
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.