프로그래밍

중첩 제어 구조

새로 외울 문법은 하나도 없습니다.
이미 아는 선택 구조와 반복 구조를 중첩했을 뿐입니다.

학습 목표

  1. 반복 안에 반복을 중첩했을 때 안쪽 반복이 몇 번 실행되는지 계산할 수 있다.
  2. 안쪽 반복 횟수가 바깥 반복의 횟수를 세는 변수의 값에 따라 달라지는 코드를 읽고 쓸 수 있다.
  3. 반복 안에서 조건에 따라 분기하는 코드를 쓸 수 있다.
  4. 중첩된 반복에서 break가 어느 반복까지 빠져나가는지 설명할 수 있다.

중첩이란

시계의 분침과 초침을 떠올리면 그림이 그려집니다.

새로 배울 문법이 없습니다

이 차시에는 새로운 문법 요소가 하나도 나오지 않습니다. 반복문(for·while)과 선택 구조(if)를 서로의 안에 넣기만 합니다. 이렇게 제어 구조 안에 다른 제어 구조를 넣는 것을 중첩(nesting)이라고 합니다. 어려운 것은 문법이 아니라 중첩했을 때 어떤 순서로 실행되는지를 머릿속에 그리는 일입니다.

분침과 초침

시계에서 분침이 한 눈금 갈 동안 초침은 예순 눈금을 돌아 한 바퀴를 돕니다. 초침이 한 바퀴를 다 돌아야 분침이 비로소 한 눈금 움직입니다. 바깥 반복이 분침, 안쪽 반복이 초침입니다.

그래서 안쪽 반복이 훨씬 자주 실행됩니다. 바깥 반복이 3번, 안쪽 반복이 4번이면 안쪽 중괄호 안의 문장은 3 × 4 = 12번 실행됩니다.

for (int min = 1; min <= 3; min = min + 1) {        // 바깥: 분침
    for (int sec = 1; sec <= 4; sec = sec + 1) {    // 안쪽: 초침
        printf("%d분 %d초  ", min, sec);
    }
    printf("\n");                                    // 안쪽 반복이 「다 끝난 뒤」 줄을 바꾼다
}

줄바꿈을 출력하는 위치를 눈여겨보세요. printf("\n")이 안쪽 중괄호 밖에 있습니다. 그래서 안쪽 반복이 네 번 모두 실행된 뒤에야 한 번 실행되어 한 줄이 끝납니다. 이 문장을 안쪽 중괄호 안에 넣으면 초마다 줄이 바뀝니다.

횟수를 세는 변수의 이름을 서로 다르게 짓습니다

바깥 반복과 안쪽 반복이 같은 변수 하나로 횟수를 세면 서로의 값을 바꿔 놓아 반복 횟수가 어긋납니다. 관례대로 i와 j를 쓰기도 하지만, min과 sec처럼 무엇을 세는지 드러나는 이름은 중첩이 깊어질수록 더 도움이 됩니다.

for (int dan = 2; dan <= 4; dan = dan + 1) {
    printf("--- %d단 ---\n", dan);
    for (int i = 1; i <= 3; i = i + 1) {
        printf("%d x %d = %d\n", dan, i, dan * i);
    }
}

구구단은 중첩 반복의 대표적인 예입니다. 바깥 반복이 「몇 단」을, 안쪽 반복이 「몇을 곱하는지」를 정합니다. --- %d단 ---을 출력하는 문장이 바깥 중괄호 안, 안쪽 반복 앞에 있어서 단이 바뀔 때마다 한 번씩 출력됩니다.

스스로 확인

위 구구단에서 --- %d단 ---을 안쪽 반복 「안」으로 옮기면 화면이 어떻게 달라질까요?

안쪽 반복 횟수가 바깥에 따라 달라질 때

여기서부터 중첩 반복이 재미있어집니다.

지금까지는 안쪽 반복이 늘 같은 횟수만큼 실행되었습니다. 그런데 안쪽 조건식에 바깥 반복의 변수를 쓰면, 바깥 반복이 한 번 실행될 때마다 안쪽 반복 횟수가 달라집니다.

for (int line = 1; line <= 4; line = line + 1) {
    for (int star = 1; star <= line; star = star + 1) {   // 안쪽 반복 횟수가 「바깥의 line 값」에 달렸다
        printf("*");
    }
    printf("\n");
}

한 줄씩 따라가 봅시다

바깥 line 안쪽 조건 출력되는 것
1 star <= 1 별 1개
2 star <= 2 별 2개
3 star <= 3 별 3개
4 star <= 4 별 4개

안쪽 조건식에 고정된 수가 아니라 line이 들어가 있어서 바깥 반복이 한 번 실행될 때마다 안쪽 반복이 한 번씩 더 실행됩니다. 그래서 삼각형 모양이 됩니다.

모양을 바꾸고 싶으면 안쪽 조건식을 수정합니다. star <= 5 - line으로 바꾸면 거꾸로 된 삼각형이 되고, 안쪽 반복을 하나 더 추가해 별 앞에 공백을 출력하면 오른쪽으로 정렬된 삼각형이 됩니다. 조건 하나만 바꾸어도 그림이 달라집니다.

#include <stdio.h>

int main(void) {
    int n;

    printf("몇 단까지 볼까요? ");
    scanf("%d", &n);

    for (int dan = 2; dan <= n; dan = dan + 1) {
        for (int i = 1; i <= 9; i = i + 1) {
            printf("%d x %d = %2d   ", dan, i, dan * i);
        }
        printf("\n");
    }
    return 0;
}

%2d는 출력 폭을 2로 잡고 수를 오른쪽에 맞춰 출력하라는 뜻입니다. 한 자리 수와 두 자리 수가 섞여도 열이 맞아 표처럼 읽힙니다.

스스로 확인

별을 오른쪽으로 정렬한 삼각형으로 만들려면 무엇을 먼저 출력해야 할까요? 반복이 몇 개 필요할까요?

반복 안의 갈림길

반복끼리만 중첩하는 것이 아닙니다.

반복 안에 갈림길을 두면 「여러 값을 하나씩 검사하면서 조건에 맞는 것만 골라내는」 일을 할 수 있습니다. 실제 프로그램에서 아주 자주 쓰는 구조입니다.

int count = 0;

for (int n = 1; n <= 20; n = n + 1) {
    if (n % 3 == 0) {              // 반복 「안」에서 조건에 따라 분기한다
        printf("%d ", n);          // 3 6 9 12 15 18
        count = count + 1;
    }
}

printf("\n3의 배수는 모두 %d개입니다.\n", count);

세 가지가 한곳에 모였습니다

  • 반복이 1부터 20까지의 수를 차례로 만든다.
  • if문이 3의 배수인지 판단한다 — 나머지 연산자를 쓴다.
  • 누적할 변수가 3의 배수의 개수를 센다 — 반복 밖에서 초기화한다.

반복 · if문 · 누적할 변수가 함께 쓰이는 구조입니다.

count는 누적할 변수이므로 반복 밖에서 한 번만 초기화했습니다. 안에서 초기화하면 반복할 때마다 0으로 되돌아가 1보다 커지지 못합니다.

스스로 확인

1부터 100까지 가운데 3의 배수이면서 5의 배수인 수를 세려면 조건을 어떻게 쓸까요?

어디까지 빠져나가나

반복을 중첩했을 때만 생기는 오해가 하나 있습니다.

for (int a = 1; a <= 3; a = a + 1) {
    for (int b = 1; b <= 3; b = b + 1) {
        if (b == 2) {
            break;                 // 「안쪽」 반복만 빠져나온다. 바깥 반복은 계속된다
        }
        printf("a=%d b=%d\n", a, b);
    }
}

break는 「자신을 감싼 가장 가까운 반복 하나」만 빠져나옵니다

안쪽 반복에서 break를 만나면 안쪽 반복만 끝납니다. 바깥 반복은 아무 일 없었다는 듯 다음 반복을 시작하고, 안쪽 반복도 처음부터 다시 실행됩니다. 그래서 위 코드는 b=1인 줄만 세 번 출력하고 끝납니다.

「break를 썼는데 왜 반복이 계속되지?」라는 의문은 여기서 생깁니다. 두 반복을 모두 끝내고 싶다면 break 하나로는 안 됩니다. 끝낼지를 나타내는 변수를 두어 바깥 반복의 조건에서도 검사하거나, 바깥 반복에서 한 번 더 break를 써야 합니다.

중첩할수록 들여쓰기가 코드를 읽는 지도가 됩니다

C 컴파일러는 들여쓰기를 무시하지만 사람은 들여쓰기로 코드를 읽습니다. 중첩하면 닫는 중괄호가 여러 개 이어지는데, 들여쓰기가 흐트러지면 어느 닫는 중괄호가 어느 여는 중괄호와 쌍을 이루는지 알기 어렵습니다. 한 단계 들어갈 때마다 공백 네 개씩 들여 쓰면 쌍이 눈에 잘 보입니다.

반복은 두 단계 정도까지 중첩할 때 읽기 편합니다. 그보다 깊이 중첩하면 실행 순서를 머릿속에 그리기가 훨씬 어려워집니다. 그럴 때는 중첩을 늘리기보다 일을 나누어 따로 처리하는 편이 낫습니다.

스스로 확인

바깥 반복과 안쪽 반복을 한꺼번에 끝내려면 어떤 방법이 있을까요? 바깥 반복의 조건과 break를 함께 떠올려 보세요.

핵심 정리

분침과 초침

안쪽 반복이 모두 끝나야 바깥 반복이 한 번 끝난다. 바깥 m번, 안쪽 n번이면 안쪽 중괄호 안의 문장은 m × n번 실행된다.

줄바꿈의 위치

안쪽 중괄호 밖에 두면 안쪽 반복이 다 끝난 뒤 한 번, 안에 두면 안쪽 반복마다 한 번 실행된다. 위치가 결과를 정한다.

바깥 값을 안쪽 조건에

안쪽 조건식에 바깥 반복의 변수를 쓰면 바깥 반복마다 안쪽 반복 횟수가 달라진다. 삼각형 모양이 여기서 나온다.

break의 범위

자신을 감싼 가장 가까운 반복 하나만 빠져나온다. 바깥 반복은 계속된다. 둘 다 끝내려면 break 하나로는 부족하다.

확인 퀴즈

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

1. 바깥 반복이 5번, 안쪽 반복이 3번 실행되는 중첩 반복에서 안쪽 중괄호 안의 문장은 몇 번 실행되는가?

2. 안쪽 반복 안에서 break를 만나면?

3. 별 삼각형에서 줄마다 별 개수가 늘어나게 만든 것은?