← C 강의 목록으로
🎯
고급 (Advanced)
정의 · 호출 · 재귀 · static

2단원 — 함수

함수는 **이름이 붙은 코드 블록**입니다. 같은 작업을 한 곳에 모아 두면 중복이 줄고, 바꿀 때 한 군데만 고치면 됩니다.

functionrecursionstatic
소요 시간
1~2시간
난이도
📊 중급
선수 조건
🎯 고급 1단원
결과물
함수로 코드를 분리하고 재귀를 다룬다

이 강의에서 배우는 것

  • 1함수의 선언(프로토타입)·정의·호출의 차이를 안다.
  • 2매개변수 전달 방식(값 전달, 포인터 전달)을 구분한다.
  • 3재귀 함수의 종료 조건을 설계한다.
  • 4`static` 변수의 수명과 가시성을 이해한다.

왜 함수로 쪼개는가?

긴 `main` 하나에 모든 코드를 넣으면:

  • **중복** 코드가 늘어남
  • 버그 한 줄 고치려고 어디 있는지 찾기 힘듦
  • 코드를 **재사용**하기 어려움

함수로 쪼개면 "이 함수는 평균을 구한다", "이 함수는 정렬한다"처럼 **역할 단위**로 코드를 다룰 수 있습니다.

핵심 개념

1) 함수의 3가지 모습

c
/* 1) 선언 (프로토타입) — main보다 위에 미리 알려줌 */
int add(int a, int b);

/* 2) 정의 — 실제 본문 */
int add(int a, int b) { return a + b; }

/* 3) 호출 */
int s = add(3, 4);

매개변수 자료형, 반환형, 이름이 모두 일치해야 합니다.

2) 호출 스택의 흐름

c
int add(int a, int b) { return a + b; }
int main(void) {
    int s = add(3, 4);   //  ← main이 add를 부르면
    return 0;            //    add가 끝날 때까지 main은 멈춰 있음
}
text
호출 시작                     호출 종료
  ┌────────┐                   ┌────────┐
  │  add   │  ◄── 현재 실행    │  main  │  ◄── 현재 실행
  ├────────┤                   └────────┘
  │  main  │
  └────────┘
스택 (Last-In-First-Out)

함수가 끝나면 **호출했던 곳**으로 정확히 돌아갑니다.

3) C는 항상 값 전달(call by value)

c
void inc(int x) { x++; }     // 복사본을 1 증가시키고 끝
int n = 5;
inc(n);                      // n은 그대로 5

호출자의 변수를 바꾸려면 **포인터를 전달**해야 합니다.

c
void inc_ptr(int *x) { (*x)++; }
inc_ptr(&n);                 // n은 6

4) 재귀 함수

c
long long factorial(int n) {
    if (n <= 1) return 1;             // 종료 조건 (base case)
    return n * factorial(n - 1);      // 자기 자신 호출
}

**종료 조건**이 없거나 잘못되면 스택 오버플로(무한 호출).

5) `static`의 두 얼굴

  • **함수 안의 `static` 변수**: 호출 사이에도 값이 유지 (수명이 프로그램 전체).
  • **함수에 붙은 `static`**: 같은 파일 내에서만 호출 가능 (외부에 안 보임).

예제로 보기

예제 1 — `ex01_basic.c` : 함수의 선언/정의/호출

c
int add(int a, int b);
int max(int a, int b);
void print_line(void);

**실행 결과**

text
--------------------
add(3, 4) = 7
max(7, 2) = 7
--------------------

핵심: 프로토타입이 main 위에 있어 컴파일러가 호출 형식을 미리 압니다.

예제 2 — `ex02_recursion.c` : 팩토리얼과 피보나치

c
long long factorial(int n);
long long fib(int n);

**실행 결과**

text
 0! = 1,  fib( 0) = 0
 1! = 1,  fib( 1) = 1
 2! = 2,  fib( 2) = 1
 3! = 6,  fib( 3) = 2
 4! = 24,  fib( 4) = 3
 5! = 120,  fib( 5) = 5
 6! = 720,  fib( 6) = 8
 7! = 5040,  fib( 7) = 13
 8! = 40320,  fib( 8) = 21
 9! = 362880,  fib( 9) = 34
10! = 3628800,  fib(10) = 55

핵심: 종료 조건(`n <= 1`)이 호출 트리의 **잎(leaf)** 입니다.

예제 3 — `ex03_array_param.c` : 배열을 함수로

c
int sum(const int arr[], int n) { ... }
double average(const int arr[], int n) { return (double)sum(arr,n) / n; }

**실행 결과**

text
합계: 150
평균: 30.00

핵심: 함수의 배열 매개변수는 사실상 **포인터**. 그래서 길이를 따로 받습니다.

예제 4 — `ex04_static.c` : 호출 사이에 값 유지

c
int counter(void) {
    static int count = 0;
    count++;
    return count;
}

**실행 결과**

text
호출 1 -> 1
호출 2 -> 2
호출 3 -> 3
호출 4 -> 4
호출 5 -> 5

핵심: `static`이 없으면 매번 0으로 초기화되어 항상 1만 출력됩니다.

다른 시각으로 보기 — "함수는 자판기"

text
   매개변수 (input)              반환값 (output)
       ▼                              ▲
   ┌───────────────────────────┐
   │     함수 본문(블랙박스)    │
   └───────────────────────────┘

같은 입력에는 같은 출력이 나오는 **결정론적 자판기**일수록 테스트와 재사용이 쉽습니다. 전역 변수 의존, 화면 출력 같은 부수 효과가 적을수록 좋습니다.

자주 하는 실수

  1. **반환형 누락**: `add(int a, int b)` 처럼 반환형을 빼면 옛날 C는 `int`로 가정 (지금은 경고).
  2. **선언 없이 호출**: 컴파일러가 추측해 잘못된 호출 규약을 쓸 수 있음.
  3. **재귀 종료 조건 누락**: 무한 재귀 → 스택 오버플로(런타임 충돌).
  4. **로컬 변수 주소 반환**: 함수 종료 후엔 무효 (`int *bad(void) { int x; return &x; }`).
  5. **배열 매개변수의 `sizeof`**: `void f(int a[10]) { sizeof(a); }` 결과는 배열 크기가 아닌 포인터 크기.

정리

  • 함수는 역할 단위로 코드를 분리·재사용하는 도구.
  • 호출은 호출 스택을 사용하며, 끝나면 호출자로 돌아온다.
  • C는 값 전달. 호출자의 변수를 바꾸려면 포인터를 전달.
  • 재귀는 반드시 종료 조건이 있어야 한다.
  • `static` 지역 변수는 호출 사이에 값이 유지된다.

직접 해 보기

bash
cd src
gcc -std=c11 -Wall -o ex01 ex01_basic.c       && ./ex01
gcc -std=c11 -Wall -o ex02 ex02_recursion.c   && ./ex02
gcc -std=c11 -Wall -o ex03 ex03_array_param.c && ./ex03
gcc -std=c11 -Wall -o ex04 ex04_static.c      && ./ex04

응용:

  • `ex02`의 `fib`을 반복문 버전으로 다시 작성해 성능을 비교해 보세요.
  • `ex04`에서 `static`을 빼고 호출 결과가 어떻게 달라지는지 확인.

💻 예제 (examples)

실제로 컴파일·실행해 결과를 확인할 수 있는 예제입니다.

ex01_basic.c함수의 선언/정의/호출
CODE
#include <stdio.h>

int add(int a, int b);
int max(int a, int b);
void print_line(void);

int main(void) {
    print_line();
    printf("add(3, 4) = %d\n", add(3, 4));
    printf("max(7, 2) = %d\n", max(7, 2));
    print_line();
    return 0;
}

int add(int a, int b) { return a + b; }
int max(int a, int b) { return a > b ? a : b; }
void print_line(void) { printf("--------------------\n"); }
▶ 실행 결과
--------------------
add(3, 4) = 7
max(7, 2) = 7
--------------------
ex02_recursion.c팩토리얼과 피보나치
CODE
#include <stdio.h>

long long factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

long long fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

int main(void) {
    for (int i = 0; i <= 10; i++) {
        printf("%2d! = %lld,  fib(%2d) = %lld\n",
               i, factorial(i), i, fib(i));
    }
    return 0;
}
▶ 실행 결과
 0! = 1,  fib( 0) = 0
 1! = 1,  fib( 1) = 1
 2! = 2,  fib( 2) = 1
 3! = 6,  fib( 3) = 2
 4! = 24,  fib( 4) = 3
 5! = 120,  fib( 5) = 5
 6! = 720,  fib( 6) = 8
 7! = 5040,  fib( 7) = 13
 8! = 40320,  fib( 8) = 21
 9! = 362880,  fib( 9) = 34
10! = 3628800,  fib(10) = 55
ex03_array_param.c배열을 함수로
CODE
#include <stdio.h>

/* 배열 매개변수는 사실상 포인터로 전달된다 */
int sum(const int arr[], int n) {
    int s = 0;
    for (int i = 0; i < n; i++) s += arr[i];
    return s;
}

double average(const int arr[], int n) {
    return (double)sum(arr, n) / n;
}

int main(void) {
    int data[] = {10, 20, 30, 40, 50};
    int n = (int)(sizeof(data) / sizeof(data[0]));

    printf("합계: %d\n", sum(data, n));
    printf("평균: %.2f\n", average(data, n));
    return 0;
}
▶ 실행 결과
합계: 150
평균: 30.00
ex04_static.c호출 사이에 값 유지
CODE
#include <stdio.h>

int counter(void) {
    static int count = 0;   /* 호출 사이에 값이 유지됨 */
    count++;
    return count;
}

int main(void) {
    for (int i = 0; i < 5; i++) {
        printf("호출 %d -> %d\n", i + 1, counter());
    }
    return 0;
}
▶ 실행 결과
호출 1 -> 1
호출 2 -> 2
호출 3 -> 3
호출 4 -> 4
호출 5 -> 5

📝 과제 (exercises)

직접 풀어보고, 막힐 때 정답을 펼쳐 비교해보세요.

과제 1

문제 1 (hw01.c)

목표: 두 정수 a, b에 대해 **최대공약수(GCD)** 를 재귀로 구하는 함수 `int gcd(int a, int b)`를 작성하세요. (유클리드 호제법)

요구사항
  • 파일명: hw01.c
정답 코드 펼치기 / 접기
SOLUTION
#include <stdio.h>

int gcd(int a, int b) {
    if (b == 0) return a;
    return gcd(b, a % b);
}

int main(void) {
    int a, b;
    printf("두 정수: ");
    scanf("%d %d", &a, &b);

    printf("GCD(%d, %d) = %d\n", a, b, gcd(a, b));
    return 0;
}
과제 2

문제 2 (hw02.c)

목표: 정수 배열에서 **최댓값 인덱스**를 반환하는 함수 `int find_max_index(const int *arr, int n)`을 작성하세요.

요구사항
  • 파일명: hw02.c
정답 코드 펼치기 / 접기
SOLUTION
#include <stdio.h>

int find_max_index(const int *arr, int n) {
    int idx = 0;
    for (int i = 1; i < n; i++)
        if (arr[i] > arr[idx]) idx = i;
    return idx;
}

int main(void) {
    int arr[] = {3, 7, 2, 8, 5, 8, 1};
    int n = (int)(sizeof(arr) / sizeof(arr[0]));

    int idx = find_max_index(arr, n);
    printf("최댓값 %d는 인덱스 %d에 있습니다.\n", arr[idx], idx);
    return 0;
}
과제 3

문제 3 (hw03.c)

목표: 양의 정수 n을 받아 **2진수 문자열**을 출력하는 재귀 함수를 작성하세요. (`printBinary(13)` -> `1101`)

요구사항
  • 파일명: hw03.c
정답 코드 펼치기 / 접기
SOLUTION
#include <stdio.h>

void printBinary(unsigned int n) {
    if (n > 1) printBinary(n / 2);
    putchar('0' + (n % 2));
}

int main(void) {
    unsigned int n;
    printf("양의 정수 입력: ");
    scanf("%u", &n);

    printBinary(n);
    putchar('\n');
    return 0;
}
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗