2차원 배열
들어가는 말
컴퓨터의 메모리 주소 공간은 선형적이지만, 2차원 배열은 행과 열로 데이터를 다루기 위한 논리적인 구조다.
문제는 2차원 배열을 구현하는 방식이 언어마다 다르고, 그에 따라 쓰이는 용어도 조금씩 다르다는 점이다.
예를 들어 같은 2차원 배열이라는 말을 쓰더라도 생성하는 방법부터 원소에 접근하는 방식까지 서로 다를 수 있다.
이 글에서는 모든 원소가 하나의 연속된 메모리 영역에 배치되는지를 기준으로 2차원 배열을 두 가지 형태로 구분한다.
연속 메모리
#include <stdio.h>
#define ROWS 3
#define COLS 3
int main(void) {
int arr_1d[ROWS * COLS] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int arr_2d[ROWS][COLS] =
{
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
printf("%d\n", arr_1d[4]); // 5
printf("%d\n", arr_2d[1][1]); // 5
int row = 1, col = 1;
printf("%d\n", arr_1d[row * COLS + col]); // 5
return 0;
}
위 코드의 1차원 배열과 2차원 배열은 모두 각 배열의 원소가 하나의 연속된 메모리 영역에 순서대로 저장된다.
C 언어는 2차원 배열을 행 우선 방식으로 저장하므로, 첫 번째 행의 모든 원소 뒤에 두 번째 행의 원소가 이어서 배치된다.
Memory Layout
+--------+--------+--------+--------+--------+--------+--------+--------+--------+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
+--------+--------+--------+--------+--------+--------+--------+--------+--------+
[0] [1] [2] [3] [4] [5] [6] [7] [8]
[0][0] [0][1] [0][2] [1][0] [1][1] [1][2] [2][0] [2][1] [2][2]
비연속 메모리
앞에서는 2차원 배열이 행과 열로 구성된 것처럼 보이지만, 실제 원소는 하나의 선형 메모리 영역에 연속해서 저장되는 것을 확인했다.
비연속 메모리 방식도 원소가 물리적으로 2차원으로 배치되는 것은 아니며, 각 행이 모여 논리적으로 2차원처럼 보이므로 2차원 배열이라고 부른다.
+-----+
| [0] |----> [ 1 | 2 | 3 ]
| [1] |----> [ 4 | 5 ]
| [2] |----> [ 6 | 7 | 8 | 9 ]
+-----+
왼쪽 배열의 각 원소는 오른쪽의 1차원 배열 하나를 가리킨다.
왼쪽 배열에 집중하면 배열 안에 다시 배열이 들어 있는 구조이므로 Array of Arrays라고 부른다.
오른쪽의 1차원 배열이 모여 2차원 구조를 이루지만, 각 배열의 원소 수는 서로 같을 수도 다를 수도 있다.
각 배열의 원소 수가 달라 행의 길이가 들쭉날쭉한 형태를 Jagged Array 또는 Ragged Array라고 부른다.
#include <stdio.h>
int main(void)
{
int row_0[] = { 1, 2, 3 };
int row_1[] = { 4, 5 };
int row_2[] = { 6, 7, 8, 9 };
int* arr[] = { row_0, row_1, row_2 };
int row_count = sizeof(arr) / sizeof(arr[0]);
int col_counts[] =
{
sizeof(row_0) / sizeof(row_0[0]),
sizeof(row_1) / sizeof(row_1[0]),
sizeof(row_2) / sizeof(row_2[0])
};
for (int i = 0; i < row_count; i++)
{
for (int j = 0; j < col_counts[i]; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}