Step 57. C 제어문과 함수 — 같은 생각, 다른 표기

Step 57. C 제어문과 함수 — 같은 생각, 다른 표기

Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★★☆☆ | 예상 소요 시간 4시간

전제: Step 56을 마쳤다. gcc로 컴파일하고 printf/scanf를 쓸 수 있다.

  • 준비물: 리눅스 터미널, gcc.
  • 주의: 오늘 실습도 전부 내 컴퓨터 안의 파일 만들기·번역·실행입니다. 100% 안전합니다.

좋은 소식부터. C의 제어문(if, for, while)은 파이썬과 생각이 같습니다. 조건이 참이면 실행하고, 정한 횟수만큼 돌고, 조건이 유지되는 동안 돕니다. Step 43에서 배운 그 흐름입니다. 달라지는 것은 표기법뿐입니다 — 들여쓰기 대신 중괄호, range 대신 세 칸짜리 for.

함수도 마찬가지입니다. 입력을 받아 결과를 돌려주는 부품이라는 생각은 Step 44 그대로입니다. 다만 C는 "위에서 아래로 읽는" 언어라, 함수를 쓰기 전에 미리 소개해 두어야 한다는 예의가 추가됩니다. 그리고 오늘의 하이라이트 — 파이썬에서는 일어나지 않던 기묘한 현상, 정수 오버플로우를 직접 일으켜 봅니다. 그것이 왜 취약점의 씨앗인지까지 오늘 눈으로 확인합니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • C의 if, for, while을 파이썬과 대응시켜 쓴다
  • 세 칸짜리 for와 i++, n– 같은 증감 약호를 읽고 쓴다
  • 함수를 선언과 정의로 나누어 작성하고, #include의 역할을 설명한다
  • switch와 break의 함정(빠뜨리면 줄줄이 실행)을 안다
  • 정수 오버플로우를 직접 일으키고, 왜 위험한지 설명한다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 C 언어, 리눅스 터미널, gcc -Wall (실측: Ubuntu 13.3.0)
오늘의 문법 if / else if / else, for (시작; 조건; 증가), while, switch / case / break, 함수 선언·정의, #include <limits.h>
필요한 개념 중괄호 블록, 선언과 정의의 분리, void, int의 범위와 정수 오버플로우

2-1. 들여쓰기에서 중괄호로, range에서 세 칸짜리 for로

파이썬은 들여쓰기로 "어디까지가 if의 내용인가"를 표시합니다. C는 중괄호 {}를 씁니다. if (조건) { ... }. 들여쓰기는 사람을 위한 장식일 뿐, 컴파일러는 중괄호만 봅니다. 그래도 읽기 좋게 들여쓰는 것이 세계의 관례입니다.

C의 for는 이렇게 생겼습니다. for (int i = 0; i < 10; i++). 세 칸은 차례로 "시작값; 계속할 조건; 한 바퀴 끝날 때마다 할 일"입니다. i++는 "i를 1 늘려라"의 약속 부호입니다. 파이썬의 for i in range(10)과 정확히 대응됩니다.

2-2. 선언과 정의 — 미리 소개하기

C 컴파일러는 위에서 아래로 읽습니다. 그래서 아래에 정의된 함수를 위에서 부르면 "그런 함수 모르는데요"라고 경고합니다. 해결책은 위쪽에 한 줄짜리 소개(선언, declaration)를 적어 두는 것입니다. int add(int a, int b); — "이런 모양의 함수가 뒤에 나옵니다"라는 예고이고, 실제 내용(정의, definition)은 아래에 둡니다.

#include <stdio.h>가 바로 printf의 선언들이 담긴 파일(헤더)을 가져오는 것이었다는 연결이 여기서 완성됩니다.

2-3. 정수 오버플로우 — 상자가 넘치는 순간

파이썬의 정수는 상자가 자동으로 커집니다. 아무리 큰 수도 담깁니다. C의 int는 크기가 정해진 상자(보통 약 21억까지)입니다. 그 상자의 최댓값에 1을 더하면? 에러도 경고도 없이, 값이 최솟값(약 -21억)으로 뚝 떨어집니다. 시계가 12시 다음에 1시로 도는 것처럼.

이것이 정수 오버플로우(integer overflow)이고, "컴퓨터가 조용히 거짓말을 하게 만드는" 대표 현상입니다. 오늘 직접 일으켜 봅니다.


3. 따라 하기

3-1. if — 조건 분기

입력 (grade.c)

#include <stdio.h>

int main(void) {
    int score;
    printf("점수를 입력하세요: ");
    scanf("%d", &score);

    if (score >= 90) {
        printf("A입니다.\n");
    } else if (score >= 80) {
        printf("B입니다.\n");
    } else {
        printf("C입니다.\n");
    }
    return 0;
}

컴파일과 실행

gcc -Wall grade.c -o grade
./grade
점수를 입력하세요: 85
B입니다.

(2026-09-09 실측. 85는 키보드 입력입니다.)

읽는 법: 구조는 파이썬의 if/elif/else와 같고, 조건에 소괄호, 내용에 중괄호가 붙은 것만 다릅니다. else if가 파이썬의 elif입니다.

예측: 90을 입력하면? 79를 입력하면? 각각 예측하고 실행해 확인하세요. 경계값(경계에 있는 숫자)을 시험하는 습관, Step 54에서 배운 그것입니다.

3-2. for — 1부터 100까지의 합

입력 (sum100.c)

#include <stdio.h>

int main(void) {
    int total = 0;
    for (int i = 1; i <= 100; i++) {
        total = total + i;
    }
    printf("합계: %d\n", total);
    return 0;
}

컴파일과 실행

gcc -Wall sum100.c -o sum100
./sum100
합계: 5050

(2026-09-09 실측.)

읽는 법: for (int i = 1; i <= 100; i++)는 "i를 1로 시작해, 100 이하인 동안, 한 바퀴마다 1씩 늘려라"입니다. 세 칸이 각각 시작, 조건, 증가를 담당합니다. 파이썬의 range(1, 101)과 완전히 같은 생각을 다른 표기로 쓴 것입니다 — 새 문법이 아니라 번역 연습입니다.

3-3. 함수 만들기 — 선언과 정의

입력 (funcs.c)

#include <stdio.h>

int add(int a, int b);
int square(int n);
void cheer(char name[]);

int main(void) {
    printf("3 + 5 = %d\n", add(3, 5));
    printf("7의 제곱 = %d\n", square(7));
    cheer("Lee");
    return 0;
}

int add(int a, int b) {
    return a + b;
}

int square(int n) {
    return n * n;
}

void cheer(char name[]) {
    printf("%s님, 잘하고 있습니다!\n", name);
}

컴파일과 실행

gcc -Wall funcs.c -o funcs
./funcs
3 + 5 = 8
7의 제곱 = 49
Lee님, 잘하고 있습니다!

(2026-09-09 실측.)

읽는 법: 위쪽의 세 줄이 선언(소개), 아래쪽이 정의(내용), main이 그 사이에서 부릅니다. 함수 이름 앞의 int는 "정수를 돌려주는 함수"라는 뜻이고, void는 "돌려줄 것이 없는" 함수 표시입니다. 선언을 지우고 컴파일해 보면 경고가 뜹니다 — 실측 메시지는 벽 2에 있습니다.

3-4. 오버플로우 — 상자를 넘겨 보기

입력 (overflow.c)

#include <stdio.h>
#include <limits.h>

int main(void) {
    int big = INT_MAX;
    printf("int의 최댓값: %d\n", big);
    printf("거기에 1을 더하면: %d\n", big + 1);
    return 0;
}

컴파일과 실행

gcc -Wall overflow.c -o overflow
./overflow
int의 최댓값: 2147483647
거기에 1을 더하면: -2147483648

(2026-09-09 실측.)

읽는 법: limits.h에 적힌 INT_MAX가 int 상자의 최댓값, 2,147,483,647(약 21억)입니다. 거기에 1을 더했더니 에러도 경고도 없이 -2,147,483,648이 되었습니다. 시계 바늘이 한 바퀴 돈 것입니다. 컴퓨터는 아무 항의도 하지 않습니다. 조용히 거짓말을 한 것입니다.

예측: 최솟값(INT_MIN)에서 1을 빼면 어떻게 될까요? 시계 바늘 비유로 예측하고 코드를 고쳐 확인해 보세요. (실측: int의 최솟값: -2147483648 다음 줄에 거기서 1을 빼면: 2147483647이 나옵니다 — 반대쪽으로 한 바퀴 돕니다.)

: 이 조용한 거짓말이 왜 위험한지는 7번 섹션에서 정리합니다. 오늘 이 출력이 이 챕터에서 가장 비싼 화면입니다.

3-5. while — 조건이 유지되는 동안

입력 (countdown.c)

#include <stdio.h>

int main(void) {
    int n = 5;
    while (n > 0) {
        printf("%d...\n", n);
        n--;
    }
    printf("발사!\n");
    return 0;
}

컴파일과 실행

gcc -Wall countdown.c -o countdown
./countdown
5...
4...
3...
2...
1...
발사!

(2026-09-09 실측.)

읽는 법: n--는 "n을 1 줄여라"입니다. 조건(n > 0)이 참인 동안 돌고, 거짓이 되면 빠져나옵니다. n--를 지우면 조건이 영원히 참이라 무한 반복합니다. 멈추려면 Ctrl+C. 파이썬과 같은 원리입니다.

3-6. switch — 갈림길이 많을 때

if가 둘씩만 나누는 문이라면, 갈림길이 많을 때는 switch라는 전용 문도 있습니다.

입력 (menu.c)

#include <stdio.h>

int main(void) {
    int menu;
    printf("메뉴를 고르세요 (1~3): ");
    scanf("%d", &menu);

    switch (menu) {
        case 1:
            printf("파일 목록을 봅니다.\n");
            break;
        case 2:
            printf("설정을 엽니다.\n");
            break;
        case 3:
            printf("종료합니다.\n");
            break;
        default:
            printf("없는 메뉴입니다.\n");
    }
    return 0;
}

컴파일과 실행

gcc -Wall menu.c -o menu
./menu
메뉴를 고르세요 (1~3): 2
설정을 엽니다.

(2026-09-09 실측.)

읽는 법: case는 "이 값이면 여기로", default는 "어느 case도 아니면"입니다. 여기서 중요한 것이 break입니다. break를 빼면 해당 case를 실행한 뒤 다음 case까지 줄줄이 실행해 버립니다. 실측: case 2의 break를 지우고 2를 입력하면 설정을 엽니다. 다음에 종료합니다.까지 이어서 나왔습니다. C의 고전 함정 중 하나입니다.


4. 미션과 연습문제

미션 — 숫자 맞히기 게임

오늘의 작품: 숫자 맞히기 게임을 C로 만듭니다.

  1. 1부터 50까지의 정답을 코드에 정해 둡니다(예: 37).
  2. 사용자가 숫자를 입력하면 "더 크다/더 작다"를 알려 주는 함수 compare를 만듭니다.
  3. while로 맞힐 때까지 반복하고, 시도 횟수를 셉니다.
  4. 맞히면 "7번 만에 정답!"처럼 축하 메시지를 출력하는 함수 celebrate를 만듭니다.
  5. 함수가 두 개 이상이고, 선언과 정의를 분리한 형태로 완성합니다.
  6. 보너스: 0이나 100 같은 범위 밖 입력에는 "1~50 사이로 입력해 주세요"라고 안내하게 합니다.

연습문제

문제 1. if (score = 90)if (score == 90)의 차이를 말하고, 전자를 썼을 때 프로그램이 어떻게 동작하는지 설명해 보세요.

문제 2. C에서 함수를 main보다 아래에 정의했다면, 위쪽에 무엇을 적어 두어야 하며 왜 필요할까요? 그리고 #include <stdio.h>가 이것과 어떻게 연결되는지 말해 보세요.

문제 3. for (int i = 1; i <= 100; i++)의 세 칸이 각각 무엇을 뜻하는지 말하고, 이 한 줄과 정확히 같은 일을 하는 파이썬 코드를 적어 보세요.

문제 4. 정수 오버플로우는 왜 에러나 경고 없이 일어나며, 그것이 "검사를 통과하는 거짓말"로 악용될 수 있는지 한 가지 예를 들어 설명해 보세요.


5. 모범 답안과 완료 기준

미션 모범 답안

#include <stdio.h>

int compare(int answer, int guess);
void celebrate(int tries);

int main(void) {
    int answer = 37;
    int guess;
    int tries = 0;

    printf("숫자 맞히기 (1~50)\n");
    while (1) {
        printf("숫자를 입력하세요: ");
        scanf("%d", &guess);
        tries++;
        if (guess < 1 || guess > 50) {
            printf("1~50 사이로 입력해 주세요\n");
            continue;
        }
        if (compare(answer, guess) == 0) {
            break;
        }
    }
    celebrate(tries);
    return 0;
}

int compare(int answer, int guess) {
    if (guess > answer) {
        printf("더 작습니다\n");
        return 1;
    } else if (guess < answer) {
        printf("더 큽니다\n");
        return 1;
    }
    return 0;
}

void celebrate(int tries) {
    printf("%d번 만에 정답!\n", tries);
}

실행 (2026-09-09 실측, 25 → 42 → 37 순으로 입력)

숫자 맞히기 (1~50)
숫자를 입력하세요: 25
더 큽니다
숫자를 입력하세요: 42
더 작습니다
숫자를 입력하세요: 37
3번 만에 정답!

검증하는 법: ① while (1)은 영원히 도는 반복이고, 정답일 때 break로 빠져나오는 구조입니다. ② 범위 밖 입력(0, 51)에서 안내 문구가 뜨는지 확인합니다. ③ -Wall로 컴파일해 경고가 없어야 합니다. 함수를 main 아래에만 쓰고 선언을 빠뜨리면 경고가 뜨는 것(벽 2)으로 선언의 필요성을 스스로 확인할 수 있습니다.

연습문제 해답

문제 1 해답. =는 "넣어라"(대입), ==는 "같으냐"(비교)입니다. if (score = 90)은 score에 90을 넣은 뒤 그 값(90)을 조건으로 씁니다. C에서 0이 아닌 값은 참으로 취급되므로 조건이 항상 참이 됩니다. 실측에서 이 코드는 score가 70이어도 "A입니다."를 출력했고, -Wallwarning: suggest parentheses around assignment used as truth value라고 경고했습니다.

문제 2 해답. 위쪽에 선언 한 줄(예: int add(int a, int b);)을 적어 둡니다. C 컴파일러가 위에서 아래로 읽기 때문에, 아래에 정의된 함수를 미리 소개하지 않으면 "모르는 함수"라고 경고합니다. #include <stdio.h>는 printf 같은 표준 함수들의 선언이 담긴 헤더 파일을 가져오는 것 — 우리가 손으로 적은 선언과 같은 역할을 대신해 주는 것입니다.

문제 3 해답. 세 칸은 차례로 시작값(i를 1로), 계속할 조건(i가 100 이하인 동안), 한 바퀴 끝날 때마다 할 일(i를 1 늘림)입니다. 같은 일을 하는 파이썬은 for i in range(1, 101):입니다.

문제 4 해답. C는 계산 결과가 상자를 넘는지 검사하지 않기 때문입니다 — "프로그래머를 믿는다"는 설계의 대가입니다. 넘친 값은 최솟값 쪽으로 한 바퀴 돌아 조용히 이어집니다. 악용의 예: "잔액 + 입금액이 한도 이하인가"를 검사하는 코드에서 공격자가 거액을 넣어 합계를 한 바퀴 돌리면, 검사가 엉뚱하게 통과할 수 있습니다. 게임 아이템 복제, 금액 우회 같은 사고의 뿌리에 이 현상이 있습니다.

완료 기준 체크리스트

  • [ ] C의 if/for/while을 파이썬과 대응해 설명할 수 있다
  • [ ] 세 칸짜리 for와 i++, n–의 뜻을 안다
  • [ ] 함수의 선언과 정의를 분리해 작성할 수 있다
  • [ ] switch에서 break를 빠뜨리면 어떻게 되는지 안다
  • [ ] 정수 오버플로우를 직접 일으키고 설명할 수 있다
  • [ ] 숫자 맞히기 게임을 함수 2개 이상으로 완성했다

6. 흔한 실수와 해결

벽 1. = 와 == 혼동

증상: if (score = 90)이라고 썼더니 조건이 항상 참처럼 동작합니다. -Wall로 컴파일하면 이렇게 알려 줍니다 (2026-09-09 실측):

assign.c:5:9: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
    5 |     if (score = 90) {
      |         ^~~~~

원인: =는 대입, ==는 비교입니다. 대입문의 결과값 90이 참으로 취급됩니다.
해결: 비교 자리에는 ==. 그리고 -Wall을 항상 켜 두세요 — 컴파일러가 "이거 의도한 게 맞니?"라고 물어봐 줍니다.

벽 2. 함수 선언을 잊는다

증상: 함수를 main 아래에만 쓰고 컴파일했더니 (2026-09-09 실측):

nodecl.c:4:20: warning: implicit declaration of function 'add' [-Wimplicit-function-declaration]
    4 |     printf("%d\n", add(3, 5));
      |                    ^~~

원인: C는 위에서 아래로 읽는데, 아래에 있는 함수를 미리 소개하지 않았습니다.
해결: 파일 위쪽에 선언 한 줄을 적습니다. 또는 함수 전체를 main 위에 둡니다. 관례는 선언을 위에 두는 쪽입니다.

벽 3. 무한 반복에 갇힌다

증상: 프로그램이 끝나지 않고 같은 것만 뿜습니다.
원인: 반복 조건이 영원히 참입니다. 증가시키는 것(n++, n--)을 잊은 경우가 대부분입니다.
해결: Ctrl+C로 멈추고, "조건이 언제 거짓이 되는가"를 손으로 추적하세요. 반복문 설계의 첫 질문은 "무엇을 반복할까"가 아니라 "어떻게 끝날까"입니다.

벽 4. 오버플로우를 오류로 기대한다

증상: 큰 수 계산이 이상한데 에러가 없어 원인을 못 찾습니다.
원인: 오버플로우는 에러가 아니라 조용한 한 바퀴 돌기입니다. 3-4절 실측에서 -Wall조차 아무 말도 하지 않았습니다.
해결: int의 범위(약 ±21억)를 기억하고, 큰 수를 다룰 때는 long long(더 큰 상자)을 고려하세요. "에러가 없다고 맞는 것은 아니다"가 C 세계의 상식입니다.

벽 5. switch에서 break를 빼먹는다

증상: 메뉴 하나를 골랐는데 다음 항목의 동작까지 줄줄이 실행됩니다. 실측: case 2의 break를 지우고 2를 입력하니 설정을 엽니다.에 이어 종료합니다.가 출력됐습니다.
원인: break가 없으면 실행이 다음 case로 "흘러내립니다"(fall-through).
해결: 각 case 끝에 break를 씁니다. 일부러 흘러내리게 하는 고급 기법도 있지만, 입문 단계에서는 "break는 기본"으로 새기세요.


7. 정리

오늘의 개념

개념 한 줄 설명
중괄호 블록 파이썬의 들여쓰기 자리 — { } 가 범위
선언 vs 정의 위쪽의 한 줄 소개 vs 아래쪽의 실제 내용
void 돌려줄 것이 없는 함수 표시
break switch의 case를 여기서 끝내는 마개
정수 오버플로우 상자를 넘친 값이 조용히 한 바퀴 도는 현상

오늘의 문법

문법 하는 일
if / else if / else 조건 분기 (파이썬 if/elif/else에 대응)
for (int i = 0; i < 10; i++) 시작; 조건; 증가의 세 칸 반복
while (조건) 조건이 참인 동안 반복
switch / case / default 갈림길이 많을 때의 분기 (break 필수)
int add(int a, int b); 함수 선언 — "뒤에 이런 함수가 나옵니다"
#include <limits.h> INT_MAX 같은 한계값이 담긴 헤더 가져오기

문법보다 중요한 감각

이틀 만에 C로 제어문과 함수를 쓰게 됐습니다. 비결은 여러분이 이미 프로그래밍을 알고 있었다는 것 — 조건, 반복, 함수는 언어가 아니라 사고방식이고, 그 사고방식은 파이썬에서 이미 길렀습니다. 언어가 바뀌어도 사고방식은 그대로라는 것이 오늘의 가장 큰 수확입니다.

그리고 오버플로우의 화면을 기억해 두세요. 최댓값에 1을 더했더니 아무 항의 없이 음수가 되는 그 순간 — 컴퓨터가 조용히 거짓말을 하게 만드는 기술의 교과서입니다. 유명한 취약점들의 고향이 대부분 C인 이유가 여기 있습니다. C는 프로그래머를 믿는 언어라, 믿음이 어긋난 자리가 곧 사고의 자리입니다. 메모리를 보는 눈이 조금씩 열리고 있습니다.


전부 체크되면 Step 57 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.