На какие операции и типы распадается имя динамического двухмерного массива?
Для примера, определим, чему эквивалентно выражение y[2][3] в нотации указателей и на какие шаги и типы оно распадается:
#include <iostream>
int main() {
int **y = new int *[5];
for (int i = 0; i != 5; ++i) {
y[i] = new int[7];
}
y[2][3] == *(*(y + 2) + 3);
// Шаг 1
static_assert(std::is_same_v<decltype(y), int **>);
// y — это указатель на массив указателей на массивы
// распада массива здесь не происходит, потому что
// y - это не массив, а указатель на указатель
// y - содержит адрес первого указателя: *y == y[0]
int **ptr_row_2 = y + 2; // получаем адрес указателя 2 (который содержит адрес строки 2)
// ptr_row_2 == &y[2]
// Шаг 2
int *&get_row_2 = *(y + 2); // первое обращение к памяти
// извлекаем указатель на первый элемент строки 2
// get_row_2 == y[2]
// Шаг 3
int *ptr_item_3 = *(y + 2) + 3; // получаем адрес на элемент 3 строки 2
// ptr_item_3 == &y[2][3]
// Шаг 4
int &get_item_3 = *(*(y + 2) + 3); // второе обращение к памяти
// извлекаем элемент 3 строки 2
// get_item_3 == y[2][3]
for (int i = 0; i != 5; ++i) {
delete[] y[i];
}
delete[] y;
return 0;
}
Вопрос: действительно ли первое обращение к памяти происходит на шаге 2? Или все таки на шаге 3?
Ответы (1 шт):
Вот этот код
int a[5][7];
size_t i = 3;
size_t j = 2;
int main()
{
a[i][j] = 4;
return 0;
}
на x86-64 GCC скомпилируется в
main:
push rbp
mov rbp, rsp
mov rdx, QWORD PTR i[rip]
mov rcx, QWORD PTR j[rip]
mov rax, rdx
sal rax, 3
sub rax, rdx
add rax, rcx
mov DWORD PTR a[0+rax*4], 4
mov eax, 0
pop rbp
ret
Здесь видно, что смещение адреса для записи в массив вычисляется по формуле: (7i+j)*4, где 7 - второй размер массива, 4 - размер int. (если прописать расчёт полностью, то получится так: ((i<<3) - i + j) * 4). В результате происходит одно обращение к памяти по адресу, который вычислен на основе размера массива и переданных индексах.
Если же использовать разыменовывания:
int a[5][7];
size_t i = 3;
size_t j = 2;
int main()
{
*(*(a+i)+j) = 5;
return 0;
}
то оно скомпилируется так:
main:
push rbp
mov rbp, rsp
mov rdx, QWORD PTR i[rip]
mov rax, rdx
sal rax, 3
sub rax, rdx
mov rcx, rax
mov rax, QWORD PTR j[rip]
add rax, rcx
sal rax, 2
add rax, OFFSET FLAT:a
mov DWORD PTR [rax], 5
mov eax, 0
pop rbp
ret
Здесь формула расчёта смещения выглядит так: ((i << 3) - i + j) << 2, что эквивалентно выражению (7i+j)*4. Получаем, что выражения
a[i][j] = 4;
и
*(*(a+i)+j) = 5;
для статического массива a порождают одинаковый код, содержащий вычисление смещения относительно начала массива и одно обращение к памяти. Следовательно, ранее заявленное мной высказывание, что обращение к памяти = разыменовывание неверно.