Многомерные массивы можно задавать несколькими способами:
int arr1[2][3];
int (*arr2)[3] = new int[2][3];
int *arr3 = new int[2*3];
int **arr4 = new int*[2];
arr4[0] = new int[3];
arr4[1] = new int[3];
// Следующие утверждения верны:
static_assert(sizeof(arr1) == (sizeof(int)*2*3));
static_assert(sizeof(arr2) == sizeof(int(*)[3]));
static_assert(sizeof(arr3) == sizeof(int*));
static_assert(sizeof(arr4) == sizeof(int**));
static_assert(sizeof(arr1[0]) == (sizeof(int)*3));
static_assert(sizeof(arr2[0]) == (sizeof(int)*3));
static_assert(sizeof(arr3[0]) == sizeof(int));
static_assert(sizeof(arr4[0]) == sizeof(int*));
В 1-3 варианте данные массива хранятся последовательно друг за другом по строкам. 4 вариант - это "массив массивов" (его могут называть и по другому), тут требуется отдельный массив указателей, данные строк могут хранится в разных местах памяти.
Обращаться к элементам arr1, arr2 и arr4 можно с помощью индексации:
arr1[i][j]
и это будет работать, но по разному для arr1/2 и arr4.
arr3 это по сути одномерный массив. То что он двумерный знаете только вы как программист. Поэтому через индексацию можно обращаться только к первой размерности. Чтоб включить и вторую размерность потребуется ее явно посчитать. Например для элемента arr2[1][2]:
*(arr2 + 1 * 3 + 2);
// или то же самое
arr2[1*3 + 2];
С точки зрения производительности 4 вариант самый плохой, т.к. для обращения к элементу требуется два чтения памяти, а так же из-за того что каждая строка хранится отдельно от других в памяти, то кэш процессора будет использоваться менее эффективно, чем для остальных вариантов. Кроме того тут используется дополнительный массив указателей (первая размерность).
Не смотря на то что 3 вариант кажется довольно многословным (особенно если константы заменить на осмысленные имена переменных), но на производительности это никак не сказывается, т.к. по сути те же самые вычисления индекса делаются и для arr1/2, только для них это делает компилятор сам неявно.