표 형태로 저장하기
한 줄로 나열하기 어려운 자료가 많습니다.
배열은 자료형이 같은 값 여러 개를 메모리에 연속으로 저장한 것입니다. 그런데 좌석 배치, 달력, 바둑판, 모둠별 기록처럼 표 형태인 자료도 흔합니다. 지금까지 쓴 배열처럼 인덱스가 하나인 배열(1차원 배열)에도 저장할 수는 있지만, 「몇 번째 행의 몇 번째 열」인지를 프로그래머가 직접 계산해야 합니다.
대괄호를 하나 더 붙입니다
int score[3][4];는 「요소 4개로 이루어진 행이 3개」인 배열을 선언합니다.
이처럼 인덱스 두 개로 요소를 가리키는 배열을 2차원 배열(two-dimensional array)이라고 합니다.
앞의 [3]이 행(row)의 개수, 뒤의 [4]가
한 행에 있는 열(column)의 개수입니다.
먼저 행으로 나누고, 각 행을 다시 열로 나눈다고 읽으면 순서를 헷갈리지 않습니다.
int score[3][4] = {
{80, 92, 75, 88}, // 0번 모둠의 네 차례
{60, 71, 95, 84}, // 1번 모둠
{90, 85, 70, 77} // 2번 모둠
};| 0번 차례 | 1번 | 2번 | 3번 | |
|---|---|---|---|---|
| 0번 모둠 | 80 | 92 | 75 | 88 |
| 1번 모둠 | 60 | 71 | 95 | 84 |
| 2번 모둠 | 90 | 85 | 70 | 77 |
세 모둠이 네 차례씩 얻은 점수 — 수업에서 원리를 확인하려고 지어낸 값
위 코드처럼 초기화할 때는 중괄호를 중첩해 씁니다. 바깥 중괄호가 배열 전체, 안쪽 중괄호 하나가 한 행입니다. 한 행의 초기값을 코드 한 줄에 쓰면 코드도 표 형태가 됩니다.
printf("0번 모둠의 0번 차례: %d\n", score[0][0]); // 80
printf("1번 모둠의 2번 차례: %d\n", score[1][2]); // 95
score[2][3] = 100; // 요소 하나만 바꾼다
printf("2번 모둠의 3번 차례: %d\n", score[2][3]); // 100인덱스가 둘이 되었을 뿐입니다
score[1][2]는 「1번 행의 2번 열」에 있는 요소입니다.
행과 열의 인덱스 모두 0부터 셉니다 — 1차원 배열의 규칙 그대로입니다.
그래서 행이 3개이면 마지막 행의 인덱스는 [2], 열이 4개이면 마지막 열의 인덱스는 [3]입니다.
요소 하나를 가리키면 일반 변수와 똑같이 쓸 수 있습니다.
값을 읽을 수도 있고 score[2][3] = 100;처럼 값을 대입할 수도 있습니다.
스스로 확인
가로 9칸, 세로 9칸인 바둑판을 2차원 배열에 저장하려면 어떻게 선언해야 할까요? 한가운데 칸의 행 인덱스와 열 인덱스는 각각 얼마입니까?
중첩 반복으로 순회하기
중첩 반복문이 자연스럽게 쓰이는 대표적인 곳입니다.
1차원 배열은 반복문 하나로 순회합니다. 인덱스가 둘이면 반복문도 둘입니다. 반복문 안에 반복문을 넣는 중첩 반복문을 씁니다.
for (int row = 0; row < 3; row = row + 1) { // 바깥 반복은 행을 고르고
for (int col = 0; col < 4; col = col + 1) { // 안쪽 반복은 그 행을 순회한다
printf("%4d", score[row][col]);
}
printf("\n"); // 한 행이 끝나면 줄을 바꾼다
}바깥이 행을 고르고 안쪽이 그 행을 순회합니다
바깥 반복이 row를 1 늘릴 때마다 안쪽 반복은 그 행의 요소 네 개를 모두 지나갑니다.
시계에서 분침이 한 눈금 움직이는 동안 초침이 한 바퀴를 도는 것과 같은 관계입니다.
그래서 안쪽 반복문의 중괄호 안은 3 × 4 = 12번 실행됩니다 — 배열의 요소 개수와 같습니다.
printf("\n")이 안쪽 중괄호 밖에 있는 것을 눈여겨보세요.
한 행을 모두 출력한 뒤에 줄을 바꾸어야 화면에도 표 형태로 출력됩니다.
%4d는 출력 폭을 4로 잡고 오른쪽에 맞춰 출력하라는 뜻이라 열이 맞습니다.
바깥과 안쪽의 반복 범위가 다릅니다
바깥은 < 3, 안쪽은 < 4입니다.
둘 다 < 3으로 쓰면 마지막 열을 모두 빠뜨리고,
둘 다 < 4로 쓰면 없는 행을 가리킵니다.
C는 범위를 벗어난 인덱스를 써도 막아 주지 않습니다.
인덱스로 쓰는 변수의 이름을 i·j 대신 row·col로 지은 이유입니다.
변수 이름에 행인지 열인지가 드러나면 두 반복의 범위를 바꿔 쓰는 실수가 눈에 잘 띕니다.
스스로 확인
바깥과 안쪽 반복문을 서로 바꾸면 출력이 어떻게 달라질까요? 표가 뒤집혀 나올까요, 순서만 달라질까요?
사실은 한 줄이다
표는 사람이 보는 형태일 뿐, 메모리의 구조는 다릅니다.
메모리는 바이트마다 주소가 매겨져 한 줄로 이어진 것이고, 배열의 요소도 그 위에 연속으로 저장됩니다. 한 줄뿐인 메모리에 표를 어떻게 저장할까요? 요소의 주소를 출력해 보면 알 수 있습니다.
printf("score[0][0] %p\n", (void *) &score[0][0]);
printf("score[0][3] %p\n", (void *) &score[0][3]);
printf("score[1][0] %p\n", (void *) &score[1][0]); // score[0][3] 바로 뒤에 이어져 있다0번 행이 끝나는 곳에 바로 1번 행이 이어집니다
score[0][3]과 score[1][0]의 주소는
int 하나의 크기만큼만 차이 납니다.
행이 바뀌었는데도 사이에 빈 공간이 전혀 없습니다.
2차원 배열은 0번 행부터 차례로 이어 붙여 메모리에 연속으로 저장됩니다.
이런 배치를 행 우선(row-major) 순서라고 합니다.
C에서 int score[3][4]는 원래 「요소 4개짜리 배열 3개를 요소로 갖는 배열」입니다. 행 우선으로 놓이는 것도 이 때문입니다.
바깥 배열의 요소 하나가 안쪽 배열 하나 전체인 셈입니다.
score[0]은 0번 행 전체, 곧 요소 4개짜리 배열 하나입니다.
요소가 연속으로 저장되어 있으니 포인터 하나로 처음부터 끝까지 차례로 읽을 수도 있습니다.
int *p = &score[0][0];
for (int i = 0; i < 12; i = i + 1) {
printf("%d ", *(p + i)); // 행을 나누지 않고 요소 12개를 차례로 읽는다
}
printf("\n"); // 80 92 75 88 60 71 95 84 90 85 70 77
엄밀히 말하면 C 표준은 &score[0][0]에서 출발한 포인터로 0번 행의 요소 네 개를 넘어서까지 읽는 것을
보장하지 않습니다. 이런 것을 정의되지 않은 동작(undefined behavior)이라고 합니다. 흔히 쓰는 컴파일러에서는 위와 같이 출력되지만,
이 코드는 메모리 배치를 확인하는 실험으로만 쓰고 실제 프로그램에서는 score[i][j]로 읽습니다.
행의 경계는 선언에 적은 약속입니다
포인터로 요소 12개를 차례로 읽으면 표가 아니라 한 줄로 출력됩니다.
메모리에는 「줄바꿈」이 없습니다.
「요소 4개마다 행이 바뀐다」는 것은 선언의 [3][4]에 적어 둔 약속일 뿐입니다.
배열 전체를 요소 12개짜리 1차원 배열로 보았을 때 row번 행의 col번 열에 있는 요소의 인덱스도 계산할 수 있습니다 —
row × 열의 개수 + col입니다.
score[1][2]는 1 × 4 + 2 = 6이므로 인덱스 6인 요소이고,
위 결과에서 왼쪽부터 일곱 번째 값이 95인 것과 맞습니다.
스스로 확인
score를 1차원 배열로 보면 score[2][3]의 인덱스는 얼마일까요? 요소 12개 가운데 어디에 놓이는지, 위 실행 결과에서 어떤 값인지도 확인해 보세요.
함수에 전달할 때
열의 크기를 적어야 하는 이유는 메모리 배치에 있습니다.
#include <stdio.h>
void show(int data[][4], int rows) { // 열의 크기 4는 반드시 적어야 한다
for (int row = 0; row < rows; row = row + 1) {
for (int col = 0; col < 4; col = col + 1) {
printf("%4d", data[row][col]);
}
printf("\n");
}
}
int main(void) {
int score[3][4] = {
{80, 92, 75, 88},
{60, 71, 95, 84},
{90, 85, 70, 77}
};
show(score, 3);
return 0;
}열의 크기는 비워 둘 수 없습니다
1차원 배열 매개변수는 int data[]처럼 배열의 크기를 비워도 됩니다.
그런데 2차원 배열 매개변수는 int data[][4]처럼 열의 크기를 반드시 적어야 합니다.
int data[][]라고 쓰면 컴파일 오류가 납니다.
이유는 「사실은 한 줄이다」 절에서 본 배치에 있습니다. 인자로 쓴 배열 이름 score는
첫 요소, 곧 0번 행의 주소로 바뀌어 전달되고, 메모리에는 줄바꿈이 없습니다.
data[1][0]을 찾으려면 「요소를 몇 개 건너뛰어야 다음 행인지」를 알아야 하는데,
그것을 알려 주는 것이 열의 크기입니다.
행의 개수는 몰라도 됩니다. 몇 번 행까지 처리할지는 rows로 따로 전달받으면 됩니다.
요소의 위치를 계산하는 데에는 열의 개수만 쓰입니다.
전달되는 것은 여전히 주소입니다
2차원 배열이라고 규칙이 바뀌지는 않습니다. 배열 전체가 복사되지 않으므로,
함수 안에서 요소를 바꾸면 호출한 쪽의 배열이 바뀝니다.
함수 안에서 sizeof로 배열 전체의 크기를 알 수 없는 것도 같습니다.
행마다 합계를 구하고 표 전체에서 가장 큰 값을 찾는 코드까지 모아 봅니다. 가장 큰 값이 「어디에」 있는지까지 알아내려면 행과 열의 인덱스를 함께 저장해 두어야 합니다.
#include <stdio.h>
void row_sum(int data[][4], int rows) {
for (int row = 0; row < rows; row = row + 1) {
int sum = 0;
for (int col = 0; col < 4; col = col + 1) {
sum = sum + data[row][col];
}
printf("%d번 모둠 합계 %d, 평균 %.1f\n", row, sum, (double) sum / 4);
}
}
int main(void) {
int score[3][4] = {
{80, 92, 75, 88},
{60, 71, 95, 84},
{90, 85, 70, 77}
};
row_sum(score, 3);
int best = score[0][0];
int best_row = 0;
int best_col = 0;
for (int row = 0; row < 3; row = row + 1) {
for (int col = 0; col < 4; col = col + 1) {
if (score[row][col] > best) {
best = score[row][col];
best_row = row;
best_col = col;
}
}
}
printf("가장 높은 점수 %d — %d번 모둠의 %d번 차례\n", best, best_row, best_col);
return 0;
}
sum을 바깥 반복 「안」에서 선언한 것을 보세요.
행이 바뀔 때마다 0부터 다시 더해야 하기 때문입니다.
반복문 밖에서 선언하면 앞 행들의 합까지 계속 누적됩니다.
누적할 변수를 어느 중괄호 안에서 선언하느냐에 따라 무엇의 합을 구하는지가 정해집니다.
스스로 확인
행별 합계가 아니라 차례별 합계(열마다 세로로 더한 값)를 구하려면 두 반복문 가운데 어느 쪽이 바깥에 와야 할까요?
핵심 정리
대괄호가 하나 더
int a[3][4]는 열이 4개인 행이 3개인 2차원 배열이다.
앞이 행, 뒤가 열이고 두 인덱스 모두 0부터 센다.
요소 하나는 일반 변수와 똑같이 쓴다.
인덱스가 둘이면 반복도 둘
바깥 반복이 행을 고르고 안쪽 반복이 그 행을 순회한다. 두 반복의 범위가 서로 다르므로 인덱스로 쓰는 변수의 이름을 뜻이 드러나게 짓는다.
메모리에는 한 줄로
0번 행부터 차례로 이어 붙여 연속으로 저장한다(행 우선 순서). 메모리에 줄바꿈은 없다.
배열을 1차원으로 보았을 때 요소의 인덱스는 행 인덱스 × 열의 개수 + 열 인덱스다.
열의 크기는 반드시 적는다
함수가 전달받는 것은 주소 하나뿐이라 요소 몇 개마다 행이 바뀌는지를 알려 주어야 요소의 위치를 계산한다. 행의 개수는 따로 전달하면 된다.
확인 퀴즈
배운 내용을 확인해 봅시다.
1. int a[5][3];에서 마지막 요소를 가리키는 것은?
2. int a[3][4]를 1차원 배열로 보았을 때 a[2][1]의 인덱스는?
3. 2차원 배열을 함수에 전달할 때 열의 크기를 적어야 하는 이유는?