점수 다섯 개를 저장한다고 해 봅시다. 변수를 score1, score2, score3, score4, score5로 다섯 개 만들면, 값 하나를 다룰 때마다 이름을 새로 불러야 하고 개수가 백 개로 늘면 손을 댈 수 없습니다. 같은 종류의 값이 여러 개일 때 그것을 한 이름으로 묶고 번호로 꺼내 쓰는 도구가 배열입니다. 이번 편은 배열이 무엇이고 왜 쓰는지부터, 선언과 0번부터 시작하는 번호, 반복문으로 훑는 방법을 예제로 익힙니다.
14.1 변수 다섯 개의 불편함
배열을 모르면 점수 다섯 개의 합을 이렇게 구해야 합니다.
int s1 = 90, s2 = 85, s3 = 70, s4 = 100, s5 = 60;
int sum = s1 + s2 + s3 + s4 + s5;
다섯 개는 참을 만합니다. 그러나 학생이 서른 명이 되면 변수 이름을 서른 개 짓고 덧셈에 서른 개를 나열해야 합니다. 이름이 저마다 따로라 반복문으로 돌릴 수도 없습니다. 값의 개수만큼 코드가 늘어나는 이 구조가 근본 문제입니다.
14.2 배열 선언, 칸이 여러 개인 상자
배열은 같은 자료형의 칸을 여러 개 이어 붙인 하나의 상자입니다. 선언은 자료형, 이름, 대괄호 안 칸 개수 순서로 씁니다.
int scores[5]; // int 값을 담는 칸 5개짜리 배열
이러면 int 값 다섯 개를 담을 공간이 이름 하나 아래 나란히 잡힙니다. 선언과 동시에 값을 채울 때는 중괄호로 나열합니다.
int scores[5] = {90, 85, 70, 100, 60};
중괄호 안 개수가 칸보다 적으면 나머지 칸은 0으로 채워집니다. int a[5] = {0};은 다섯 칸이 모두 0이 되고, int a[5] = {1, 2};는 뒤 세 칸이 0입니다. 반대로 초기화하지 않은 지역 배열에는 쓰레기 값이 들어 있으니, 값을 넣기 전에 읽지 않습니다.
14.3 번호는 0부터 셉니다
![[C 14] 배열, 같은 값 여러 개를 한 이름에](https://img.thenullpage.com/posts/6549/6549_1_fb5ca8.webp)
각 칸은 대괄호 안 번호(인덱스)로 꺼냅니다. 그런데 이 번호는 1이 아니라 0부터 시작합니다. 칸이 다섯 개면 번호는 0, 1, 2, 3, 4입니다.
int scores[5] = {90, 85, 70, 100, 60};
printf("첫째 값: %d\n", scores[0]); // 90
printf("다섯째 값: %d\n", scores[4]); // 60
그래서 크기가 n인 배열의 마지막 번호는 항상 n에서 1을 뺀 값입니다. 함정. scores[5]는 여섯째 칸을 달라는 뜻이지만 그런 칸은 없습니다. 이렇게 경계 밖을 건드리면 컴파일은 되지만 실행할 때 엉뚱한 메모리를 읽거나 덮어써서, 정의되지 않은 동작(undefined behavior, 결과를 예측할 수 없는 상태)이 됩니다. 번호의 끝은 크기가 아니라 크기에서 1을 뺀 값이라고 기억해 둡니다.
14.4 반복문으로 전체 훑기
배열의 진짜 힘은 반복문과 만날 때 나옵니다. 번호를 0부터 4까지 올리며 같은 코드 한 줄로 모든 칸을 처리합니다.
#include <stdio.h>
int main(void) {
int scores[5] = {90, 85, 70, 100, 60};
for (int i = 0; i < 5; i++) {
printf("%d번 학생: %d점\n", i + 1, scores[i]);
}
return 0;
}
실행하면 이렇게 나옵니다.
1번 학생: 90점
2번 학생: 85점
3번 학생: 70점
4번 학생: 100점
5번 학생: 60점
반복 변수 i가 곧 번호입니다. 조건을 i < 5로 둔 것에 주의합니다. i는 0에서 시작해 4까지만 돌고 5에서 멈추므로, 있는 칸만 정확히 훑습니다. i <= 5로 잘못 쓰면 없는 scores[5]를 건드려 경계를 넘습니다.
14.5 합계와 평균 내기
훑는 김에 값을 더해 나가면 합계가 됩니다. 합을 담을 변수를 0으로 두고, 칸마다 더합니다.
int scores[5] = {90, 85, 70, 100, 60};
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += scores[i];
}
printf("합계: %d\n", sum);
printf("평균: %.1f\n", (double)sum / 5);
실행 결과는 이렇습니다.
합계: 405
평균: 81.0
평균에서 (double)를 붙인 이유가 있습니다. sum과 5가 모두 정수라 그냥 나누면 몫만 남고 소수점이 버려집니다. 한쪽을 실수로 바꿔야 81.0처럼 소수까지 제대로 나옵니다.
14.6 가장 큰 값 찾기
최고점을 찾는 방법은 사람이 하는 것과 같습니다. 첫 칸을 임시 우승자로 두고, 나머지를 훑다가 더 큰 값을 만나면 우승자를 바꿉니다.
int scores[5] = {90, 85, 70, 100, 60};
int max = scores[0]; // 일단 첫 값을 후보로
for (int i = 1; i < 5; i++) {
if (scores[i] > max) max = scores[i];
}
printf("최고점: %d\n", max);
최고점: 100
반복을 0이 아니라 1부터 시작한 점을 보세요. 0번은 이미 후보로 넣었으니 1번부터 비교하면 충분합니다. 반대로 최솟값을 찾고 싶으면 부등호만 <로 뒤집으면 됩니다.
14.7 거꾸로 출력하기
번호를 반대로 돌리면 저장된 순서를 뒤집어 볼 수 있습니다. 마지막 번호 4에서 시작해 0까지 하나씩 내립니다.
int arr[5] = {1, 2, 3, 4, 5};
for (int i = 4; i >= 0; i--) {
printf("%d ", arr[i]);
}
printf("\n");
5 4 3 2 1
시작을 크게, 조건을 i >= 0으로, 증감을 i--로 두면 역순 훑기가 됩니다. 여기서도 시작을 5가 아니라 4로 둔 것이 핵심입니다. 5로 시작하면 첫걸음부터 경계를 넘습니다.
14.8 조건에 맞는 값 세기
배열을 훑으며 특정 조건을 만족하는 칸만 세는 것은 통계의 기본 동작입니다. 주사위를 여덟 번 굴린 기록에서 6이 몇 번 나왔는지 세어 봅니다.
int dice[8] = {1, 6, 3, 6, 6, 2, 5, 6};
int count = 0;
for (int i = 0; i < 8; i++) {
if (dice[i] == 6) count++;
}
printf("6이 나온 횟수: %d\n", count);
6이 나온 횟수: 4
세는 변수 count를 0으로 시작해, 조건 dice[i] == 6이 참일 때만 1씩 올립니다. 여기서 같음 비교는 등호 하나 =가 아니라 둘 ==이라는 점을 잊지 마세요. 하나만 쓰면 비교가 아니라 대입이 되어 뜻이 완전히 달라집니다.
14.9 배열은 자기 크기를 모릅니다
지금까지 반복 조건에 5, 8 같은 숫자를 손으로 적었습니다. 배열 자신은 칸이 몇 개인지 들고 다니지 않기 때문입니다. 다만 선언된 배열이라면 전체 크기를 칸 하나 크기로 나눠 개수를 구할 수 있습니다.
int arr[5] = {1, 2, 3, 4, 5};
int n = sizeof(arr) / sizeof(arr[0]);
printf("칸 개수: %d\n", n); // 5
주의. 이 계산은 배열을 선언한 그 자리에서만 통합니다. 배열을 함수 인자로 넘기면 사실은 첫 칸의 주소만 전달되어, 받은 쪽에서 sizeof를 쓰면 배열 전체가 아니라 주소의 크기가 나옵니다. 그래서 배열을 함수에 넘길 때는 개수를 항상 별도 인자로 함께 넘깁니다. 배열은 자기 경계를 스스로 지키지 못하므로, 그 경계는 코드를 짜는 사람이 지켜야 합니다.
배열은 같은 종류의 값을 한 이름으로 묶고 0부터 시작하는 번호로 꺼내는 도구입니다. 선언에서 칸 개수를 정하고, 반복문의 번호를 0부터 크기 직전까지 돌리면 합계, 평균, 최댓값, 뒤집기, 세기가 모두 같은 틀에서 풀립니다. 반복 조건을 크기보다 하나 작은 번호에서 멈추는 습관, 그 하나가 경계 밖 접근이라는 가장 흔한 사고를 막아 줍니다.