Menu

C 언어 2차원 배열: 다차원 배열, 메모리 배치, 행렬

C에서 2차원 배열을 선언하고 초기화하고 반복하는 법, 행 우선 배치가 메모리에서 실제로 무엇을 뜻하는지, 2차원 배열을 함수에 넘길 때 열 개수가 필요한 이유, 그리고 행렬 예제.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

다차원 배열은 원소가 그 자체로 배열인 배열입니다. int grid[3][4];는 특별한 격자 타입이 아니라 원소 셋이고, 각각이 int 넷짜리 배열이며, 등을 맞대고 저장됩니다. 그것이 이해되고 나면 나머지가 따라옵니다. 배치, 인덱싱 산술, 그리고 그렇지 않으면 당황스러운 함수 전달 규칙 말입니다.

2차원이면 실용적인 쓰임 거의 전부 - 격자, 표, 행렬, 게임판, 이미지 - 를 덮으므로, 이 페이지는 그것을 다룹니다.

선언과 초기화

int grid[3][4];        // 3행 4열 - int 12개

첫 번째 숫자가 행의 개수, 두 번째가 열의 개수입니다. 초기화자는 평평하게 쓸 수도, 안쪽 중괄호와 함께 쓸 수도 있습니다. 중괄호는 모양을 보여 주므로 쓸 만합니다.

int e[][3] 형태가 중요합니다. 개수는 비워 두고 초기화자가 결정하게 할 수 있지만, 개수는 결코 선택 사항이 아닙니다. 다음 절이 그 이유를 설명합니다.

행 우선 배치

C는 2차원 배열을 행 우선 순서로 저장합니다. 0행 전체, 그다음 1행 전체, 이런 식으로 끊기지 않은 메모리 덩어리 하나에 놓습니다. 뒤에 숨은 행 포인터 배열 같은 것은 없습니다.

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

머릿속의 그림                 메모리에 실제로 놓인 모습
+----+----+----+----+
|  1 |  2 |  3 |  4 |   0행    +--+--+--+--+--+--+--+--+--+--+--+--+
+----+----+----+----+           | 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12|
|  5 |  6 |  7 |  8 |   1행    +--+--+--+--+--+--+--+--+--+--+--+--+
+----+----+----+----+            \___ 0행 ___/\___ 1행 ___/\__ 2행 __/
|  9 | 10 | 11 | 12 |   2행
+----+----+----+----+           grid[i][j]는 원소 인덱스 i*4 + j에 있다

그 공식 i * 열수 + j가 메커니즘 전부이고, 무언가를 인덱싱하려면 컴파일러가 열 개수를 알아야 하는 이유입니다. 행 개수는 계산에 아예 들어오지 않습니다.

주소를 출력하면 배치를 직접 볼 수 있습니다.

주소는 한 행이 끝나고 다음 행이 시작하는 자리를 포함해 빈틈없이 sizeof(int)씩 올라갑니다. 평평한 반복문이 이를 증명합니다. flat[k]가 열두 원소를 하나의 나열로 훑습니다.

이 배치에는 알아둘 만한 성능적 결과도 있습니다. 행을 바깥, 열을 안쪽으로 도는 반복문은 메모리를 순서대로 건드리고 CPU 캐시가 그것을 좋아합니다. 중첩을 뒤바꿔 안쪽 반복문이 열을 따라 내려가게 하면 매번 한 행만큼 건너뛰게 되어, 큰 배열에서 몇 배 느려질 수 있습니다.

중첩 반복문

2차원에는 for 반복문 둘이 필요합니다. 바깥이 행을 고르고, 안쪽이 그 행의 열을 훑습니다.

카운터는 뜻하는 바대로 이름 짓고(행은 i/row, 열은 j/col), 순서를 일관되게 유지하세요. 어디서나 grid[row][col]입니다. 2차원 배열 버그의 절반은 뒤바뀐 인덱스 쌍입니다.

#define으로 잡은 크기도 장식이 아닙니다. 이제 모양을 바꿔도 반복문 경계와 선언이 어긋날 수 없습니다.

2차원 배열을 함수에 넘기기

모두를 걸어 넘어뜨리는 규칙이 여기 있습니다. 함수의 매개변수가 열 개수를 선언해야 합니다.

이유는 붕괴입니다. grid를 넘기면 그 첫 원소를 가리키는 포인터로 변환되는데, 원소가 행이므로 타입은 int (*)[4], 즉 int 4개짜리 배열을 가리키는 포인터입니다. grid[i][j]가 무언가를 뜻하려면 컴파일러가 한 행이 얼마나 먼지 알아야 하고, 그것이 바로 그 4입니다. 행 개수는 정말로 타입에 없고, 그래서 별도 인자로 따라갑니다.

int (*grid)[COLS]int grid[][COLS]는 같은 매개변수를 두 가지로 쓴 것입니다. 괄호는 필수인데, int *grid[COLS]라면 포인터 배열이 되기 때문입니다. 그 구분은 포인터와 배열에서 다룹니다.

열 개수를 실행 중에만 알 수 있다면, C99의 가변 수정 매개변수로 그것을 먼저 넘길 수 있습니다.

void print_any(int rows, int cols, int grid[rows][cols]);

rowscols는 그것들을 쓰는 배열 매개변수보다 먼저 선언되어야 합니다. 그것을 쓸 수 없는 곳에서 흔한 대안은 평평한 1차원 배열에 직접 하는 인덱스 산술입니다.

data[i * cols + j]는 고정 크기인 경우에 컴파일러가 대신 써 주는 바로 그것입니다. 손으로 하면 한 줄이 들지만, 실행 중에 결정되는 어떤 모양에도 통합니다.

행렬 예제

행렬 곱셈이 이 페이지 전체를 한데 모읍니다. 행 우선 저장 위를 도는 중첩 반복문 셋입니다.

따라 할 만한 세부 사항이 둘입니다. 안쪽 k 반복문은 a[i][k]b[k][j]를 짝짓습니다. 한 인덱스는 행을, 다른 인덱스는 열을 훑습니다. 그리고 전치는 안쪽 반복문을 j = i + 1에서 시작합니다. 0에서 시작하면 모든 쌍을 두 번 바꿔 행렬이 그대로 남습니다.

3차원과 그 너머

패턴은 확장되고, 함수 매개변수 규칙도 그렇습니다. 첫 번째를 제외한 모든 차원을 선언해야 합니다.

실무에서 3차원쯤 되면 고정 크기 배열이 다루기 버거워지기 시작하고, 대부분의 코드는 인덱스를 계산하는 평평한 덩어리나 각 축이 무엇을 뜻하는지 이름 붙인 구조체 배열로 옮겨 갑니다.

흔한 실수

  • grid[i, j]라고 쓰기. 콤마 연산자가 i를 평가하고 버린 뒤 j로 인덱싱합니다. 컴파일은 됩니다. 틀렸습니다. grid[i][j]를 쓰세요.
  • 인덱스를 뒤바꾸기. grid[col][row]는 엉뚱한 자리에서 진짜 원소를 읽으므로 잡아 줄 오류가 없습니다. 어디서나 [row][col] 순서를 지키세요.
  • 매개변수에서 열 크기를 빠뜨리기. void f(int grid[][])는 컴파일되지 않고, 그것이 컴파일러가 여러분을 구해 주는 것입니다.
  • 범위를 벗어나기. 다른 배열과 마찬가지로 경계 검사가 없습니다. [3][4] 격자에서 grid[0][5]는 조용히 grid[1][1]을 읽습니다. 배치가 연속이고 산술은 신경 쓰지 않으니까요.

자주 묻는 질문

C에서 2차원 배열은 어떻게 선언하나요?

대괄호 안에 크기 둘을 줍니다. int grid[3][4];는 4열짜리 행 3개, 총 12개의 int를 선언합니다. "3개짜리 배열인데 각각이 int 4개짜리 배열"로 읽으세요. C가 실제로 저장하는 방식이 그렇습니다.

C에서 2차원 배열은 메모리에 어떻게 저장되나요?

행 우선 순서입니다. 0행의 모든 원소, 그다음 1행의 모든 원소 순으로 연속된 하나의 덩어리에 놓입니다. grid[i][j]는 시작에서 i * 열수 + j번째 원소 자리에 있고, 그래서 컴파일러에게 필요한 숫자가 열 개수입니다.

C에서 2차원 배열을 함수에 어떻게 넘기나요?

매개변수가 열 개수를 선언해야 합니다. void print(int grid[][4], int rows) 또는 같은 뜻의 void print(int (*grid)[4], int rows)입니다. 배열이 행을 가리키는 포인터로 붕괴하므로 행 개수는 생략할 수 있지만, 열 크기 없이는 컴파일러가 한 행이 어디서 시작하는지 계산할 수 없습니다.

2차원 배열을 전부 0으로 초기화할 수 있나요?

네. int grid[3][4] = {0};은 모든 원소를 0으로 만듭니다. 나열하지 않은 원소는 0으로 초기화되기 때문입니다. int grid[3][4] = {{1, 2}};는 0행의 앞 두 자리를 채우고 나머지 열 개는 0으로 둡니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기