Введение
Ранее рассматривали передачу одиночных значений в функции — массивы добавляют новое измерение, представляя последовательность однотипных элементов, тесно связанную с указателями на низком уровне реализации в 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, требующим именно такого представления.