프로그래밍

배열

값이 서른 개라고 이름을 서른 개 지을 수는 없습니다.
이름은 하나로 두고 번호를 붙입니다.

학습 목표

  1. 자료형이 같은 값이 여럿일 때 배열을 선언해 저장할 수 있다.
  2. 인덱스가 0부터 시작한다는 것을 알고 원하는 요소에 접근할 수 있다.
  3. 반복문으로 모든 요소를 차례로 처리하며 합과 최댓값을 구할 수 있다.
  4. 배열의 범위를 벗어난 인덱스로 접근하면 무슨 일이 생기는지 설명하고, 그런 접근을 피하는 코드를 쓸 수 있다.

같은 것이 여럿일 때

사물함이 서른 개라고 이름을 서른 개 짓지는 않습니다.

지금까지는 값 하나마다 변수 하나를 선언했습니다. 값이 다섯 개면 변수도 다섯 개입니다.

#include <stdio.h>

int main(void) {
    int score1 = 88;
    int score2 = 95;
    int score3 = 74;
    int score4 = 61;
    int score5 = 90;

    int sum = score1 + score2 + score3 + score4 + score5;

    printf("합계는 %d입니다.\n", sum);
    return 0;
}
이름이 다섯 개 — 「다음 것」으로 넘어갈 방법이 없다 score1 88 score2 95 score3 74 score4 61 score5 90 이름은 하나 — 번호를 올리면 다음 칸 score 88 [0] 95 [1] 74 [2] 61 [3] 90 [4] 번호

다섯은 되지만 서른은 안 됩니다

  • 변수를 score30까지 하나하나 선언해야 합니다.
  • 합을 구하는 식에 변수 서른 개를 모두 적어야 합니다.
  • 무엇보다 반복문을 쓸 수 없습니다. 변수 이름이 제각각이라 「다음 변수」로 넘어갈 방법이 없습니다.

반복문은 같은 코드를 여러 번 실행할 뿐, 반복할 때마다 이름이 다른 변수를 가리키게 할 수는 없기 때문입니다.

사물함으로 바꿔 봅시다

학교 사물함에는 칸마다 다른 이름을 붙이지 않습니다. 「3학년 1반 사물함」이라는 이름 하나를 두고 칸에 번호를 매깁니다. 그러면 「12번 칸」 하나로 가리킬 수 있고, 번호를 하나씩 올려 가며 전부 열어 볼 수도 있습니다.

C에서 그 사물함에 해당하는 것이 배열(array)입니다. 배열은 자료형이 같은 값 여러 개를 메모리에 연속으로 저장하고, 이름 하나와 번호로 가리키는 것입니다. 배열에 저장된 값 하나하나를 요소(element)라고 부릅니다.

#include <stdio.h>

int main(void) {
    int score[5] = {88, 95, 74, 61, 90};   // 요소가 다섯 개인 배열을 선언하고 초기화한다

    printf("첫 번째 요소: %d\n", score[0]);
    printf("네 번째 요소: %d\n", score[3]);

    score[2] = 80;                          // 요소 하나만 골라 바꾼다
    printf("바꾼 뒤 세 번째 요소: %d\n", score[2]);

    return 0;
}
코드 의미
int 요소의 자료형. 모든 요소의 자료형이 같습니다.
score 배열의 이름. 요소마다 붙이지 않고 배열 전체에 하나만 붙입니다.
[5] 요소의 개수, 곧 배열의 크기. 선언할 때 정합니다.
{88, 95, 74, 61, 90} 초기화할 값. 첫 요소부터 차례로 저장됩니다.

배열의 크기는 선언할 때 정해집니다

int score[5];로 선언한 배열은 요소가 언제나 다섯 개입니다. 쓰다가 모자라도 여섯 번째 요소가 새로 생기지 않습니다. 그래서 필요한 개수를 먼저 정하고 선언하는 것이 순서입니다.

또 한 배열에는 한 자료형의 값만 저장됩니다. int로 선언한 배열은 요소가 모두 int라서, 실수를 넣으면 소수점 아래가 잘린 정수로 저장됩니다. 사물함 칸의 크기가 모두 같은 것과 같습니다.

스스로 확인

한 반 스물다섯 명의 키를 저장하려면 배열을 어떻게 선언해야 할까요? 요소의 자료형과 배열의 크기를 각각 정해 보세요.

인덱스는 0부터

이 차시에서 자주 틀리는 대목입니다.

배열에서 요소의 위치를 가리키는 번호를 인덱스(index)라고 합니다. score[2]의 2가 인덱스입니다. 그런데 이 번호는 1이 아니라 0에서 시작합니다.

인덱스 0 1 2 3 4
저장된 값 88 95 74 61 90
사람이 부르는 말 첫째 둘째 셋째 넷째 다섯째
int score[5] = {88, 95, 74, 61, 90}; 88 95 74 61 90 ? [0] [1] [2] [3] [4] [5] 첫 칸 마지막 칸 없는 칸 인덱스는 「몇째」가 아니라 「첫 칸에서 몇 칸 떨어졌나」다.
#include <stdio.h>

int main(void) {
    int score[5] = {88, 95, 74, 61, 90};

    printf("인덱스  : 0   1   2   3   4\n");
    printf("요소 값 : ");

    for (int i = 0; i < 5; i = i + 1) {     // 0에서 시작해 5 「미만」까지
        printf("%d  ", score[i]);
    }
    printf("\n");

    return 0;
}

인덱스는 「몇째」가 아니라 「얼마나 떨어졌나」입니다

첫 요소의 인덱스가 0인 것이 이상해 보이지만, 첫 요소를 기준으로 삼으면 자연스럽습니다. 첫 요소는 기준 자신이므로 0만큼 떨어져 있고, 그다음 요소는 1만큼 떨어져 있습니다. 인덱스는 순서가 아니라 첫 요소로부터의 거리입니다.

이렇게 보면 마지막 요소의 인덱스도 바로 나옵니다. 요소가 n개면 마지막 요소의 인덱스는 n − 1입니다. 크기가 5인 배열의 마지막 요소는 score[5]가 아니라 score[4]입니다.

같은 [5]가 두 뜻으로 쓰입니다

어디에 쓰였느냐에 따라 의미가 다릅니다. 선언문에 쓴 [5]는 「요소를 다섯 개 만들라」는 뜻이고, 선언한 뒤에 쓴 [5]는 「인덱스가 5인 요소」를 가리킵니다. 앞의 것은 개수(배열의 크기), 뒤의 것은 위치입니다. 모양이 같아 헷갈리기 쉬우니 선언문인지 아닌지를 먼저 봅니다.

스스로 확인

요소가 100개인 배열에서 「쉰 번째 요소」를 가리키려면 인덱스를 얼마로 써야 할까요?

반복과 만날 때

배열이 쓸모 있어지는 것은 여기서부터입니다.

대괄호 안에는 숫자뿐 아니라 변수도 쓸 수 있습니다. 반복문이 그 변수를 1씩 증가시키면 코드 한 줄로 모든 요소를 차례로 처리할 수 있습니다. 『같은 것이 여럿일 때』 절에서 「반복문을 쓸 수 없다」던 문제가 여기서 풀립니다.

int sum = 0;

for (int i = 0; i < 5; i = i + 1) {
    sum = sum + score[i];               // 요소를 하나씩 차례로 더한다
}

printf("합계는 %d입니다.\n", sum);      // 408
i가 0에서 4까지 오르면 한 줄이 다섯 칸을 다 지나간다 i = 0 88 88 i = 1 95 183 i = 2 74 257 i = 3 61 318 i = 4 90 408 sum 반복문 밖에서 선언한 sum이 앞 반복의 결과를 이어받는다.

i < 5이지 i <= 5가 아닙니다

인덱스가 0부터 시작하므로 크기가 5인 배열의 인덱스는 0, 1, 2, 3, 4입니다. i < 5가 정확히 이 다섯 값을 만들어 냅니다. i <= 5로 쓰면 여섯 번 반복하면서 마지막에 없는 요소에 접근합니다.

그래서 배열을 처리하는 반복문은 0에서 시작해 < 크기로 끝내는 형식을 그대로 익혀 두는 편이 좋습니다.

최댓값을 찾는 코드도 같은 형식입니다. 다만 비교할 기준값을 먼저 하나 정해 두는 것이 다릅니다.

int best = score[0];                    // 첫 요소를 임시 최댓값으로 정한다

for (int i = 1; i < 5; i = i + 1) {     // 두 번째 요소부터 비교한다
    if (score[i] > best) {
        best = score[i];
    }
}

printf("가장 큰 값은 %d입니다.\n", best);     // 95

왜 0이 아니라 첫 요소의 값으로 시작할까요

best를 0으로 초기화하면 모든 값이 음수인 배열에서 답이 0으로 나옵니다. 배열에 들어 있지도 않은 값이 최댓값이 되어 버리는 것입니다. 첫 요소를 임시 최댓값으로 정하면 어떤 값이 저장되어 있든 답은 배열 안에서 나옵니다.

그래서 반복도 i = 0이 아니라 i = 1에서 시작합니다. score[0]은 이미 best에 저장되어 있으니 자기 자신과 비교할 이유가 없습니다.

요소의 개수를 코드 곳곳에 5로 써 두면, 배열의 크기를 늘릴 때 고쳐야 할 곳이 여러 군데로 흩어집니다. 배열이 메모리에서 차지하는 크기를 이용해 요소의 개수를 구하는 방법이 있습니다.

int count = sizeof(score) / sizeof(score[0]);

printf("요소는 모두 %d개입니다.\n", count);   // 5

sizeof는 메모리에서 차지하는 크기를 알려 줍니다

sizeof는 괄호 안에 쓴 것이 메모리에서 차지하는 크기를 구하는 연산자입니다. sizeof(score)는 배열 전체가 메모리에서 차지하는 크기, sizeof(score[0])은 요소 하나가 차지하는 크기입니다. 전체 크기를 요소 하나의 크기로 나누면 요소의 개수가 나옵니다.

이 계산은 배열을 선언한 곳에서만 맞습니다. 배열을 다른 곳으로 전달하면 「전체 크기」를 더는 알 수 없게 되어 이 나눗셈이 엉뚱한 값을 냅니다. 지금은 배열을 선언한 곳에서 요소의 개수를 셀 때만 씁니다.

스스로 확인

최댓값 대신 최솟값을 찾으려면 최댓값을 찾는 코드에서 무엇을 바꿔야 할까요? 바꿀 곳이 몇 군데인지도 세어 보세요.

범위 밖과 초기화

C가 알려 주지 않는 것이 둘 있습니다.

#include <stdio.h>

int main(void) {
    int score[5] = {88, 95, 74, 61, 90};

    printf("%d\n", score[4]);   // 마지막 요소. 배열은 여기까지다
    printf("%d\n", score[5]);   // 범위 밖이다. C는 막아 주지 않는다

    return 0;
}
score[5]는 배열이 끝난 「다음」 위치다 88 [0] 95 [1] 74 [2] 61 [3] 90 [4] ? ? [5] 배열 score의 공간 다른 공간 C는 경계에서 멈추라고 말해 주지 않는다.

범위 밖에 접근해도 C는 막지 않습니다

score[5]는 없는 요소입니다. 그런데 C는 「그런 요소는 없습니다」라고 알려 주지 않습니다. 대개 코드에 쓴 대로 배열이 끝난 다음 위치의 메모리를 읽어 옵니다. 사물함 줄 끝을 지나 옆 반 사물함을 여는 것과 같습니다.

그래서 결과가 한 가지로 정해지지 않습니다. 엉뚱한 값이 출력되기도 하고, 프로그램이 비정상 종료되기도 하고, 아무 일도 없는 것처럼 정상적으로 실행되기도 합니다. 가장 나쁜 것은 정상적으로 실행되는 경우입니다. 틀린 코드가 맞는 것처럼 보이기 때문입니다.

막아 주는 장치가 없으니 인덱스가 범위를 벗어나지 않게 하는 일은 프로그래머가 맡습니다. 반복 조건을 < 크기로 쓰는 습관만으로도 대부분을 막을 수 있습니다.

나머지 하나는 초기화하지 않은 요소입니다. 초기값을 몇 개만 쓰면 나머지 요소에는 무엇이 저장될까요?

#include <stdio.h>

int main(void) {
    int few[5] = {7, 8};        // 앞의 두 값만 썼다
    int zero[5] = {0};          // 모든 요소를 0으로 초기화하는 흔한 방법

    for (int i = 0; i < 5; i = i + 1) {
        printf("%d ", few[i]);  // 7 8 0 0 0
    }
    printf("\n");

    for (int i = 0; i < 5; i = i + 1) {
        printf("%d ", zero[i]); // 0 0 0 0 0
    }
    printf("\n");

    return 0;
}

중괄호로 초기화했다면 나머지 요소는 0입니다

{7, 8}처럼 중괄호로 초기값을 일부만 쓴 경우, 값을 쓰지 않은 요소는 0으로 초기화됩니다. 그래서 {0} 하나로 모든 요소를 0으로 초기화하는 방법을 자주 씁니다.

중괄호를 아예 쓰지 않았다면 이야기가 다릅니다. main 안에서 int score[5];라고만 선언한 배열의 요소에는 정해지지 않은 값이 들어 있습니다. 0이 들어 있으리라 믿고 바로 더하기 시작하면 결과를 믿을 수 없습니다. 값을 저장하기 전에는 읽지 않는다는 것만 지키면 됩니다.

여기까지를 한 프로그램으로

배열을 선언하고, 반복문으로 값을 입력받고, 다시 반복문으로 합을 구한 뒤, if문으로 평균보다 큰 값의 개수를 세는 것까지 한 프로그램에 모았습니다.

#include <stdio.h>

int main(void) {
    int score[5];
    int sum = 0;

    for (int i = 0; i < 5; i = i + 1) {
        printf("%d번째 점수: ", i + 1);     // 사람에게는 1번부터 보여 준다
        scanf("%d", &score[i]);             // 요소 하나에 바로 입력받는다
    }

    for (int i = 0; i < 5; i = i + 1) {
        sum = sum + score[i];
    }

    double average = (double) sum / 5;

    int over = 0;
    for (int i = 0; i < 5; i = i + 1) {
        if (score[i] > average) {
            over = over + 1;
        }
    }

    printf("\n합계 %d, 평균 %.1f\n", sum, average);
    printf("평균을 넘은 것은 %d개입니다.\n", over);

    return 0;
}

입력받는 줄의 &score[i]를 눈여겨보세요. score[i]는 요소 하나이므로 일반 변수와 똑같이 다룹니다. 변수에 입력받을 때 &를 붙였던 것처럼 여기서도 붙입니다.

안내 문구에는 i + 1을 출력했습니다. 컴퓨터는 0부터 세지만 사람에게는 1부터 보여 주는 것이 친절합니다. 「1번째 점수」라고 안내하고 입력값은 score[0]에 저장하면 됩니다. 화면에 보이는 번호와 실제 인덱스를 굳이 같게 만들 이유가 없습니다.

스스로 확인

「여기까지를 한 프로그램으로」의 프로그램에서 반복 조건을 i <= 5로 잘못 고치면 어느 줄에서 무슨 일이 생길까요? 입력받는 반복과 더하는 반복을 나누어 생각해 보세요.

핵심 정리

이름 하나에 번호

배열은 자료형이 같은 값 여러 개를 메모리에 연속으로 저장한 것이다. 이름은 배열 전체에 하나뿐이고 요소는 인덱스로 가리킨다. 요소의 자료형도 배열의 크기도 선언할 때 정해진다.

인덱스는 거리다

첫 요소의 인덱스는 0이다. 인덱스는 순서가 아니라 첫 요소에서 얼마나 떨어졌는지이기 때문이다. 크기가 n인 배열의 마지막 인덱스는 n − 1이다.

인덱스에 변수를 넣는다

반복할 때마다 1씩 증가하는 변수를 인덱스로 쓰면 코드 한 줄로 모든 요소를 처리한다. 반복 범위는 0에서 < 크기까지다.

범위는 프로그래머가 지킨다

범위를 벗어난 인덱스로 접근해도 C는 막지 않는다. 결과는 한 가지로 정해지지 않는다 — 정상처럼 보이는 경우가 가장 위험하다.

확인 퀴즈

배운 내용을 확인해 봅시다.

1. int a[8];로 선언한 배열에서 마지막 요소를 가리키는 것은?

2. 크기가 5인 배열의 모든 요소를 한 번씩 처리하는 반복문은?

3. 배열의 범위를 벗어난 인덱스로 접근했을 때 일어나는 일로 알맞은 것은?