Мастерство указателей и многомерных массивов в C
В языке C массив — это коллекция значений похожего типа, хранящихся в непрерывных областях памяти. Каждый элемент массива (одномерного или многомерного) идентифицируется одним или несколькими уникальными целочисленными индексами.
Указатель, с другой стороны, хранит адрес переменной. Адрес 0-го элемента в массиве — это указатель массива. Можно использовать «оператор разыменования» для доступа к значению, на которое ссылается указатель.
Можно объявить одномерный, двумерный или многомерный массив в C. Термин «размерность» относится к количеству индексов, необходимых для идентификации элемента в коллекции.
Указатели и одномерные массивы
В одномерном массиве каждый элемент идентифицируется одним целым числом:
int a[5] = {1, 2, 3, 4, 5};
Здесь число «1» находится на 0-м индексе, «2» на индексе 1, и так далее.
Переменная, хранящая адрес 0-го элемента, является его указателем —
int *x = &a[0];
Просто имя массива также указывает на адрес 0-го элемента. Так что можно также использовать это выражение —
int *x = a;
Пример
Поскольку значение указателя увеличивается на размер типа данных, x++ перемещает указатель к следующему элементу массива.
#include <stdio.h>
int main(){
int arr[] = {1, 2, 3, 4, 5};
int length = sizeof(arr) / sizeof(arr[0]);
int i = 0;
int *ptr = arr;
while (i < length){
printf("arr[%d]: %d \n", i, *(ptr + i));
i++;
}
return 0;
}
Вывод
При запуске этот код выдаст следующий вывод —
arr[0]: 1
arr[1]: 2
arr[2]: 3
arr[3]: 4
arr[4]: 5
Указатели и двумерные массивы
Если одномерный массив похож на список элементов, то двумерный массив похож на таблицу или матрицу.
Элементы в 2D массиве можно считать логически расположенными в строках и столбцах. Следовательно, положение любого элемента определяется двумя индексами — его номером строки и номером столбца. Индексы строк и столбцов начинаются с «0».
int arr[2][2];
Такой массив представляется как —
| Col0 | Col1 | Col2 | |
|---|---|---|---|
| Row0 | arr[0][0] | arr[0][1] | arr[0][2] |
| Row1 | arr[1][0] | arr[1][1] | arr[1][2] |
| Row2 | arr[2][0] | arr[2][1] | arr[2][2] |
Можно отметить, что табличное расположение — лишь логическое представление. Компилятор выделяет блок непрерывных байт. В C выделение памяти для массива происходит в row-major порядке, что означает, что элементы считываются в массив по строкам.
Здесь мы объявляем 2D массив с тремя строками и четырьмя столбцами (число в первых квадратных скобках всегда относится к количеству строк) как —
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
Компилятор выделит память для вышеуказанного 2D массива в row-wise порядке. Предполагая, что первый элемент массива находится по адресу 1000, а размер типа int — 4 байта, элементы массива получат следующие выделенные адреса памяти —
| Row 0 | Row 1 | Row 2 | |
|---|---|---|---|
| Value | 1 2 3 4 | 5 6 7 8 | 9 10 11 12 |
| Address | 1000 1004 1008 1012 | 1016 1020 1024 1028 | 1032 1036 1040 1044 |
Мы присвоим адрес первого элемента массива num указателю ptr с использованием оператора адреса &.
int *ptr = &arr[0][0];
Пример 1
Если указатель увеличивается на 1, он переходит к следующему адресу. Все 12 элементов в массиве «3x4» можно получить в цикле следующим образом —
#include <stdio.h>
int main(){
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
};
// указатель ptr, указывающий на массив num
int *ptr = &arr[0][0];
int i, j, k = 0;
// печать элементов массива num через указатель ptr
for (i = 0; i < 3; i++){
for (j = 0; j < 4; j++){
printf("%d ", *(ptr + k));
k++;
}
printf("\n");
}
return 0;
}
Вывод
При запуске этот код выдаст следующий вывод —
1 2 3 4
5 6 7 8
9 10 11 12
В общем, адрес любого элемента массива можно получить с помощью следующей формулы —
адрес элемента в i-й строке и j-м столбце = baseAddress + [(i * no_of_cols + j) * sizeof(array_type)]
В нашем массиве 3x4,
адрес arr[2][2] = 1000 + (2*4 + 2)*4 = 1044
Вы можете обратиться к вышеуказанной таблице, и она подтвердит, что адрес arr[2][2] равен 1044.
Пример 2
Используйте разыменование указателя для получения значения по адресу. Давайте используем эту формулу для обхода массива с помощью его указателя —
#include <stdio.h>
int main(){
// 2d array
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int ROWS = 3, COLS = 4;
int i, j;
// pointer
int *ptr = &arr[0][0];
// печать элемента массива через указатель ptr
for (i = 0; i < ROWS; i++){
for (j = 0; j < COLS; j++) {
printf("%4d ", *(ptr + (i * COLS + j)));
}
printf("\n");
}
return 0;
}
Вывод
При запуске этот код выдаст следующий вывод —
1 2 3 4
5 6 7 8
9 10 11 12
Указатели и трёхмерные массивы
Трёхмерный массив — это массив двумерных массивов. Такой массив объявляется с тремя индексами —
int arr[x][y][j];
Этот массив можно рассматривать как «x» количество слоёв таблиц, каждая таблица имеющая «x» строк и «y» столбцов.
Пример 3D массива —
int arr[3][3][3] = {
{ {11, 12, 13}, {14, 15, 16}, {17, 18, 19} },
{ {21, 22, 23}, {24, 25, 26}, {27, 28, 29} },
{ {31, 32, 33}, {34, 35, 36}, {37, 38, 39} },
};
Указатель на 3D массив можно объявить как —
int *ptr = &arr[0][0][0];
Зная, что имя массива само является адресом 0-го элемента, мы можем записать указатель 3D массива как —
int *ptr = arr;
Каждый слой из «x» строк и «y» столбцов занимает —
x * y * sizeof(data_type)
Количество байт. Предполагая, что память, выделенная для 3D массива arr как объявлено выше, начинается с адреса 1000, второй слой (с i = 1) начинается на позиции 1000 + (3 * 3) * 4 = 1036 байт.
ptr = Base address of 3D array arr
Если JMAX — количество строк, а KMAX — количество столбцов, то адрес элемента в 0-й строке и 0-м столбце 1-го среза —
arr[1][0][0] = ptr + (1 * JMAX * KMAX)
Формула для получения значения элемента в j-й строке и k-м столбце i-го среза может быть задана как —
arr[i][j][k] = *(ptr + (i * JMAX * KMAX) + (j * KMAX + k))
Пример: Печать 3D массива с помощью разыменования указателя
Давайте используем эту формулу для печати 3D массива с помощью разыменования указателя —
#include <stdio.h>
int main(){
int i, j, k;
int arr[3][3][3] = {
{ {11, 12, 13}, {14, 15, 16}, {17, 18, 19} },
{ {21, 22, 23}, {24, 25, 26}, {27, 28, 29} },
{ {31, 32, 33}, {34, 35, 36}, {37, 38, 39} },
};
int JMAX = 3, KMAX = 3;
int *ptr = arr; // &arr[0][0][0];
for(i = 0; i < 3; i++){
for(j = 0; j < 3; j++){
for(k = 0; k < 3; k++){
printf("%d ", *(ptr + (i * JMAX * KMAX) + (j * KMAX + k)));
}
printf("\n");
}
printf("\n");
}
return 0;
}
Вывод
При запуске этот код выдаст следующий вывод —
11 12 13 14 15 16 17 18 19
21 22 23 24 25 26 27 28 29
31 32 33 34 35 36 37 38 39
В общем, доступ к массиву через указатель довольно похож на доступ через подписанное представление. Главное различие в том, что подписанное объявление массива выделяет память статически, тогда как мы можем использовать указатели для динамического выделения памяти.
Для передачи многомерного массива в функцию нужно использовать указатели вместо подписок. Однако использование подписанного массива удобнее, чем указателей, которые могут быть сложны для новичков.
Язык C
- Пространства имён в C++: избегайте конфликтов имен
- Освоение ввода и вывода в C++: практическое руководство
- Строки в C: null-терминированные массивы символов, функции библиотеки string.h и лучшие практики
- Инкапсуляция в C#: модификаторы доступа и защита данных
- Понимание .NET Framework: архитектура, компоненты и принципы проектирования
- Небезопасный код в C#: Указатели и управление памятью
- Объединения в C (union): эффективное использование памяти для разных типов данных
- Цикл for в C++: синтаксис, принцип работы и практические примеры
- Публичное, защищённое и приватное наследование в C++: подробный разбор
- Сериализация и десериализация в C#: практический пример