На какие операции и типы распадается имя динамического двухмерного массива?

Рейтинг: 1Ответов: 1Опубликовано: 07.03.2023

Для примера, определим, чему эквивалентно выражение 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?

Ответы

▲ 2Принят

Вот этот код

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 порождают одинаковый код, содержащий вычисление смещения относительно начала массива и одно обращение к памяти. Следовательно, ранее заявленное мной высказывание, что обращение к памяти = разыменовывание неверно.