Синтаксис си/с++

Массивы: одномерные, многомерные, связь с указателями

Введение

Ранее рассматривали передачу одиночных значений в функции — массивы добавляют новое измерение, представляя последовательность однотипных элементов, тесно связанную с указателями на низком уровне реализации в C/C++.

Концепция

Массив в C/C++ — непрерывный блок памяти, содержащий заданное количество элементов одного типа, размер которого (для статических массивов, объявленных без динамического выделения памяти) должен быть известен на этапе компиляции. Имя массива в большинстве контекстов неявно преобразуется в указатель на его первый элемент — это тесная связь между массивами и указателями, лежащая в основе того, что элементы массива могут эквивалентно адресоваться как через индексацию (array[i]), так и через арифметику указателей (*(array + i)). Многомерные массивы (int matrix[3][4]) в C/C++ физически представляют собой непрерывный блок памяти, логически организованный как массив массивов, с элементами, расположенными подряд по строкам (row-major order) — важная деталь для понимания реального порядка элементов в памяти при низкоуровневой работе с многомерными массивами.

Пример кода

#include <iostream>

int main()
{
    int numbers[5] = {10, 20, 30, 40, 50}; // статический массив фиксированного, известного размера

    for (int i = 0; i < 5; ++i) {
        std::cout << numbers[i] << " "; // доступ через индексацию
    }
    std::cout << std::endl;

    // Имя массива неявно преобразуется в указатель на первый элемент
    int *pointerToFirst = numbers;
    std::cout << *pointerToFirst << std::endl;     // 10 — тот же первый элемент
    std::cout << *(pointerToFirst + 2) << std::endl; // 30 — арифметика указателя эквивалентна numbers[2]

    // sizeof массива (НЕ указателя!) даёт ПОЛНЫЙ размер в байтах
    std::cout << sizeof(numbers) << std::endl;        // 5 * sizeof(int), типично 20
    std::cout << sizeof(numbers) / sizeof(numbers[0]) << std::endl; // 5 — классический способ узнать количество элементов
    return 0;
}
// Многомерный массив — физически непрерывный блок памяти, организованный по строкам
#include <iostream>

int main()
{
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    for (int row = 0; row < 3; ++row) {
        for (int col = 0; col < 4; ++col) {
            std::cout << matrix[row][col] << " ";
        }
    }
    // Элементы РЕАЛЬНО расположены в памяти подряд: 1,2,3,4,5,6,7,8,9,10,11,12 (row-major order)

    return 0;
}
// Распад массива в указатель при передаче в функцию — теряется информация о размере!
void processArray(int *arr, int size) // НЕОБХОДИМО передавать size ОТДЕЛЬНО — он не сохраняется автоматически
{
    for (int i = 0; i < size; ++i) {
        std::cout << arr[i] << " ";
    }
}

int main()
{
    int numbers[5] = {1, 2, 3, 4, 5};
    processArray(numbers, 5); // явная передача размера — функция НЕ может определить его самостоятельно
    return 0;
}

Пояснения к коду

sizeof(numbers) / sizeof(numbers[0]) показывает классический, идиоматичный способ определения количества элементов статического массива — sizeof для самого массива (а не указателя на него) возвращает полный размер всего блока памяти в байтах, и деление на размер одного элемента даёт точное количество элементов, что работает только для статических массивов с известным на этапе компиляции размером, а не для массивов, переданных в функцию (как показывает третий пример). Пример с многомерным массивом и его реальным порядком в памяти (1,2,3,4,5,6,7,8,...) показывает важную деталь — несмотря на удобный, «двумерный» синтаксис matrix[row][col], физически это всего лишь единый, непрерывный блок памяти, и понимание этого row-major порядка важно для эффективной, кеш-дружественной работы с многомерными массивами (обход по строкам, а не по столбцам, обычно эффективнее именно из-за локальности в памяти). processArray(int *arr, int size) явно демонстрирует ключевое практическое следствие «распада» массива в указатель при передаче в функцию — информация об исходном размере массива теряется, и функция вынуждена принимать размер как отдельный, явный параметр, не имея возможности самостоятельно определить его через sizeof внутри собственного тела (поскольку внутри функции arr — это уже просто указатель, а не сам массив).

Подводные камни

  • Попытка использовать sizeof(arr) / sizeof(arr[0]) внутри функции, принимающей массив как параметр, ожидая такого же результата, как для статического массива на уровне вызывающего кода — поскольку массив «распадается» в указатель при передаче в функцию (как показано в processArray), sizeof(arr) внутри функции вернёт размер самого указателя (обычно 4 или 8 байт), а не размер исходного массива, что является частой, неочевидной для новичков ошибкой при первом знакомстве с этой особенностью передачи массивов в функции.
  • Выход за границы массива при индексации (array[size] или больше, для массива размером size) — C/C++ не выполняет автоматическую проверку границ массива при обычной индексации (в отличие от многих других языков программирования, либо от std::vector::at(),), и обращение за пределы выделенной для массива памяти является неопределённым поведением, способным как тихо «сработать» без видимой ошибки (с непредсказуемым, потенциально некорректным значением), так и привести к явному краху программы, в зависимости от конкретных обстоятельств.
  • Путаница в порядке индексов многомерного массива относительно реального порядка хранения в памяти, особенно при попытке оптимизации (обход по строкам обычно эффективнее обхода по столбцам именно из-за row-major порядка хранения C/C++ массивов, в отличие, например, от Fortran с column-major порядком) — код, обходящий многомерный массив «не в том» порядке (col во внешнем цикле, row во внутреннем), формально корректен, но менее эффективен по использованию кеша процессора по сравнению с обходом, соответствующим реальному физическому порядку элементов в памяти.
  • Использование «сырых» статических C-массивов в новом, современном C++ коде вместо std::array/std::vector (алгоритмический цикл) без явной, веской причины — современный C++ предоставляет типобезопасные, более удобные альтернативы (std::array для статического размера, std::vector для динамического), избегающие большинства подводных камней «сырых» массивов (отсутствие проверки границ, потеря размера при передаче в функцию, о ручном управлении памятью для динамических случаев), и использование «сырых» массивов в современном коде обычно ограничено действительно низкоуровневыми случаями или взаимодействием с C-API, требующим именно такого представления.