1학년 / 2학기 / C++ / 2강

함수, 재귀, 스코프

함수, 재귀, 스코프

학습 흐름

함수의 구성과 원형 → 값에 의한 전달 → 오버로딩과 기본 인수
        ↓
재귀(기저 조건, 호출 추적) → 재귀를 for / while로 바꾸기
        ↓
스코프(블록, 가리기, ::) → 저장 기간(static 지역 변수)
        ↓
const와 #define 매크로 → 표준 라이브러리와 네임스페이스

1. 함수의 구성

반환형 함수이름(매개변수 목록) {
    본문
    return 값;
}
 
int add(int a, int b) {     // a, b: 매개변수 (함수 정의 쪽)
    return a + b;
}
 
int r = add(3, 5);          // 3, 5: 인수 (호출 쪽)
용어뜻
매개변수(parameter)함수 정의에서 값을 받는 변수. 함수의 지역 변수처럼 동작
인수(argument)호출할 때 실제로 넘기는 값. 리터럴, 변수, 식 모두 가능
void반환값이 없음. return;으로 중간에 끝낼 수 있음

함수 원형(선언)

컴파일러는 호출하는 시점에 함수의 이름과 매개변수 자료형을 알고 있어야 합니다. 정의가 main보다 아래 있으면 위에 원형을 둡니다.

int add(int, int);         // 원형: 본문 없음, 세미콜론, 매개변수 이름 생략 가능
 
int main() {
    cout << add(2, 3);     // OK
}
 
int add(int a, int b) { return a + b; }

2. 값에 의한 전달

일반 매개변수에는 인수의 값이 복사됩니다. 함수 안에서 매개변수를 바꿔도 호출한 쪽 변수는 그대로입니다. 결과를 바깥에 반영하려면 반환값을 받아서 대입하거나 참조 매개변수(다음 단원)를 씁니다.

int next(int k) {
    k = k + 1;      // 복사본만 바뀜
    return k;
}
 
int i = 0;
next(i);            // 반환값을 버림 → i는 그대로 0
i = next(i);        // 반환값을 대입 → i는 1

3. 함수 오버로딩과 기본 인수

오버로딩

이름이 같고 매개변수 목록(개수 또는 자료형)이 다른 함수를 여러 개 만들 수 있습니다. 컴파일러가 인수의 자료형을 보고 가장 잘 맞는 함수를 고릅니다.

void f(int x)    { cout << "int "; }
void f(double x) { cout << "double "; }
void f(char x)   { cout << "char "; }
호출선택이유
f(3)f(int)정확히 일치
f(3.0)f(double)정확히 일치
f('a')f(char)정확히 일치
f(3.0f)f(double)float → double 승격
f(true)f(int)bool → int 승격
f(3L)컴파일 오류long → int / double / char가 모두 "변환"이라 우열이 없음(모호)

기본 인수

인수를 생략하면 기본값을 씁니다. 기본값은 오른쪽 매개변수부터 채워야 합니다.

int add(int a, int b = 10, int c = 100) { return a + b + c; }
 
add(1);         // 1 + 10 + 100 = 111
add(1, 2);      // 1 + 2 + 100  = 103
add(1, 2, 3);   // 6
 
int bad(int a = 1, int b);   // 컴파일 오류: 기본값 뒤에 기본값 없는 매개변수

위의 f 세 개와 add가 있을 때 출력을 쓰시오. 컴파일 오류인 호출이 있으면 표시하시오.

f(3); f(3.0); f('a'); f(3.0f); f(true);
cout << '\n' << add(1) << ' ' << add(1, 2) << ' ' << add(1, 2, 3);
f(3L);
정답 확인
int double char double int 
111 103 6

f(3L)은 세 함수 모두 변환이 필요해 모호한 호출이므로 컴파일 오류입니다. 이 줄을 지워야 위 출력이 나옵니다.


4. 재귀 함수

재귀 함수는 자기 자신을 호출합니다. 반드시 두 부분이 있어야 합니다.

구성역할빠지면
기저 조건더 호출하지 않고 바로 값을 반환무한 재귀 → 스택 넘침
재귀 단계기저 조건에 가까워지는 인수로 다시 호출기저에 도달 못 함
long long factorial(int n) {
    if (n <= 1) return 1;              // 기저 조건 (0!도 1)
    return n * factorial(n - 1);       // 재귀 단계
}
내려가며 호출:  factorial(4) → factorial(3) → factorial(2) → factorial(1)
올라오며 계산:  1 → 2×1=2 → 3×2=6 → 4×6=24

호출 추적: 재귀 호출 앞의 코드와 뒤의 코드

  • 재귀 호출 앞의 문장은 내려가면서 실행됩니다.
  • 재귀 호출 뒤의 문장은 호출이 반환된 뒤, 올라오면서(역순으로) 실행됩니다.

다음 출력을 쓰시오.

// (1)
void p(int n) {
    if (n == 0) return;
    cout << n;
    p(n - 1);
    cout << n;
}
p(3);
 
// (2) 호출 순서와 반환값, 총 호출 수
int fib(int n) {
    cout << "f(" << n << ") ";
    if (n <= 1) return 1;
    int a = fib(n - 1);
    int b = fib(n - 2);
    return a + b;
}
int v = fib(4);
cout << "= " << v;
정답 확인

(1) 321123 — 앞의 cout이 3, 2, 1을 내려가며 출력하고, 뒤의 cout이 1, 2, 3을 올라오며 출력합니다.

(2)

f(4) f(3) f(2) f(1) f(0) f(1) f(2) f(1) f(0) = 5

왼쪽 호출 fib(n - 1)을 끝까지 내려간 뒤 오른쪽 fib(n - 2)를 처리합니다. 총 9번 호출되고 fib(2)가 두 번 계산됩니다.

재귀 함수의 값 구하기: 아래에서부터 표로

족보의 재귀 문제는 작은 값부터 표를 채워 올라가면 빠르고 정확합니다.

int f(int x) {
    if (x <= 0) return 5;
    return f(x - 1) + f(x - 1);
}
  1. 기저값: f(0) = 5 (그리고 x ≤ 0이면 모두 5)
  2. 점화식 정리: 같은 인수로 두 번 호출하므로 f(x) = 2 × f(x - 1)
  3. 표: f(0)=5, f(1)=10, f(2)=20, f(3)=40, f(4)=80 → 일반식 f(n) = 5 × 2ⁿ

5. 재귀를 for / while로 바꾸기

변환 순서 (4단계)

  1. 기저값을 찾는다 → 반복문의 시작값.
  2. 재귀 단계를 f(x) = (f(x-1)을 쓰는 식) 꼴로 한 번 호출만 남게 정리한다.
    • 같은 인수로 k번 호출 → k × f(x-1)로 합친다.
  3. 기저에서 시작해 x까지 올라가며 2의 식을 반복한다.
  4. 기저 조건보다 작은 입력(예: x ≤ 0)은 반복문 전에 따로 처리한다.

예 1: 같은 인수로 두 번 호출

// 재귀
int f(int x) {
    if (x <= 0) return 5;
    return f(x - 1) + f(x - 1);    // = 2 * f(x - 1)
}
 
// for
int fFor(int x) {
    int result = 5;                 // 기저값 f(0)
    for (int i = 1; i <= x; i++)    // f(1)부터 f(x)까지
        result = 2 * result;
    return result;                  // x <= 0이면 반복 0번 → 5
}
 
// while
int fWhile(int x) {
    int result = 5;
    int i = 1;
    while (i <= x) {
        result = 2 * result;
        i++;
    }
    return result;
}

예 2: 점화식에 x가 들어가는 경우

int h(int x) {
    if (x <= 0) return 1;
    return 2 * h(x - 1) + x;        // 이번 단계의 x를 더함
}
 
int hFor(int x) {
    int r = 1;                      // h(0)
    for (int i = 1; i <= x; i++)
        r = 2 * r + i;              // 재귀의 x 자리에 반복 변수 i
    return r;
}

h(0)=1, h(1)=2·1+1=3, h(2)=2·3+2=8, h(3)=2·8+3=19, h(4)=2·19+4=42

예 3: 합 구하기 (재귀 호출 뒤에 연산)

int sum(int n) {                    // 1 + 2 + … + n
    if (n <= 0) return 0;
    return n + sum(n - 1);
}
 
int sumWhile(int n) {
    int total = 0;
    while (n > 0) {                 // n부터 1까지 내려가며 더해도 결과는 같음
        total += n;
        n--;
    }
    return total;
}

예 4: 서로 다른 인수로 두 번 호출 (피보나치)

같은 인수가 아니므로 합칠 수 없습니다. 직전 두 값을 변수 두 개로 들고 올라갑니다.

int fibLoop(int n) {
    if (n <= 1) return 1;           // 위의 fib와 같은 기저
    int prev = 1, cur = 1;          // fib(0), fib(1)
    for (int i = 2; i <= n; i++) {
        int next = prev + cur;
        prev = cur;
        cur = next;
    }
    return cur;
}

다음 함수에 대해 답하시오.

int g(int x) {
    if (x <= 0) return x + 1;
    return g(x - 1) + g(x - 1) + g(x - 1);
}
  1. g(0), g(1), g(2), g(3), g(-2)를 구하시오.
  2. 같은 값을 반환하는 for 반복문 함수와 while 반복문 함수를 작성하시오.
  3. 기저 조건을 x < 0으로 바꾸면 g(4)는 얼마인가?
정답 확인

1. g(0) = 1, g(1) = 3, g(2) = 9, g(3) = 27, g(-2) = -1 (기저에서 x + 1).

2. 같은 인수로 세 번 호출하므로 g(x) = 3 × g(x - 1), g(0) = 1입니다.

int gFor(int x) {
    if (x <= 0) return x + 1;       // 기저 범위는 따로 처리
    int result = 1;                 // g(0)
    for (int i = 1; i <= x; i++)
        result *= 3;
    return result;
}
 
int gWhile(int x) {
    if (x <= 0) return x + 1;
    int result = 1, i = 1;
    while (i <= x) {
        result *= 3;
        i++;
    }
    return result;
}

3. 0. g(-1) = 0이 새 기저값이 되고 g(0) = 3 × 0 = 0, 그 위도 모두 0입니다.

f(1), f(2), f(3), f(4)를 구하고, 재귀가 끝나는 x의 범위를 쓰시오.

int f(int x) {
    int i = x - 1;
    if (i <= 0) return 5;
    return f(x - 1) + f(x - 1);
}
정답 확인

f(1) = 5, f(2) = 10, f(3) = 20, f(4) = 40. 기저 조건은 x - 1 <= 0, 즉 x ≤ 1입니다. x ≤ 0인 예제보다 한 칸 밀린 것에 주의하세요.


6. 상호 재귀

두 함수가 서로를 호출합니다. 먼저 정의된 함수가 아래 함수를 호출하므로 원형 선언이 필요합니다.

string egg(int g);                          // 원형
string chicken(int g) { return g == 0 ? "Chicken!" : egg(g - 1); }
string egg(int g)     { return g == 0 ? "Egg!" : chicken(g - 1); }

chicken(3) → egg(2) → chicken(1) → egg(0) → Egg!, chicken(4) → Chicken! (짝수 단계면 출발한 쪽).


7. 스코프 (이름을 쓸 수 있는 범위)

종류범위
블록 변수선언한 곳부터 그 { }의 끝까지
for (int i …)의 i그 for문 안에서만
매개변수함수 본문 전체
전역 변수선언한 곳부터 파일 끝까지

이름 가리기와 ::

안쪽 블록에 같은 이름을 선언하면 바깥 이름이 가려집니다. 가려진 전역 변수는 ::이름으로 쓸 수 있습니다. (가려진 바깥 지역 변수는 꺼낼 방법이 없습니다.)

다음 출력을 쓰시오.

int x = 1;                              // 전역
void show() { cout << x << ' '; }
 
int main() {
    int x = 2;
    {
        int x = 3;
        cout << x << ' ' << ::x << ' ';
    }
    cout << x << ' ';
    show();
    for (int x = 9; x < 10; x++) cout << x << ' ';
    cout << x;
}
정답 확인
3 1 2 1 9 2
  • 안쪽 블록의 x는 3, ::x는 전역 1.
  • 블록이 끝나면 main의 x(2)가 다시 보입니다.
  • show()는 자신이 정의된 위치에서 보이는 전역 x(1)를 씁니다. 호출한 쪽의 지역 변수와 상관없습니다.
  • for의 x(9)는 반복문 안에서만 존재합니다.

8. 저장 기간과 static 지역 변수

구분스코프(어디서 이름을 쓰나)저장 기간(언제 존재하나)초기화
일반 지역 변수블록 안블록에 들어올 때 생성, 나갈 때 소멸매 호출마다
static 지역 변수블록 안프로그램 끝까지처음 한 번만 (값이 없으면 0)
전역 변수파일 전체프로그램 끝까지처음 한 번 (값이 없으면 0)

다음 출력을 쓰시오.

// (1)
int counter() {
    static int s = 0;
    int t = 0;
    s++;
    t++;
    return s * 10 + t;
}
cout << counter() << ' ' << counter() << ' ' << counter() << '\n';
 
// (2)
int r(int n) {
    static int c = 0;
    c++;
    if (n == 0) return c;
    return r(n - 1);
}
cout << r(3) << ' ';
cout << r(2) << '\n';
정답 확인

(1) 11 21 31 — s는 1, 2, 3으로 이어지고 t는 매번 0에서 1이 됩니다.

(2) 4 7 — r(3)은 r(3), r(2), r(1), r(0) 네 번 호출되어 c = 4. r(2)는 c가 4에서 이어져 세 번 더 호출되므로 7. 재귀 호출끼리도 static 변수 하나를 공유합니다.

다음 프로그램의 최종 배열과 step의 총 호출 횟수를 쓰시오. 호출마다 시작 k, 시작 flag, 바뀐 원소, 반환 k를 추적하시오.

int step(int a[], int k) {
    static int flag = 1;
    a[k] = a[k] * 2 + flag;
    if (flag) k++;
    flag = 1 - flag;
    return k;
}
 
int main() {
    int a[4] = {1, 2, 3, 4};
    int i = 0;
    while (i < 4)
        i = step(a, i);
    for (int j = 0; j < 4; j++) cout << a[j] << ' ';
}
정답 확인

출력: 3 9 13 17 , 총 7번 호출.

호출시작 k시작 flag바뀐 원소반환 k
101a[0] = 1·2+1 = 31
210a[1] = 2·2+0 = 41
311a[1] = 4·2+1 = 92
420a[2] = 3·2+0 = 62
521a[2] = 6·2+1 = 133
630a[3] = 4·2+0 = 83
731a[3] = 8·2+1 = 174

flag가 static이 아니면 매 호출 1에서 시작하므로 4번만 호출되고 결과는 3 5 7 9입니다.

extern

extern int g;는 "다른 파일 어딘가에 정의된 전역 변수 g를 쓴다"는 선언입니다. 실제 정의(int g = 0;)는 한 곳에만 있어야 합니다.


9. const와 #define 매크로

const

const int MAX = 30;      // 선언과 동시에 초기화해야 함
// MAX = 40;             // 컴파일 오류
// const int N;          // 컴파일 오류: 초기값 없음

#define 매크로는 텍스트 치환

전처리기가 컴파일 전에 글자를 그대로 바꿔 넣습니다. 계산 순서를 지켜 주지 않습니다.

#define SQ(x)  x * x          // 괄호 없음 (위험)
#define SQ2(x) ((x) * (x))    // 인자와 전체를 괄호로

int a = 3;일 때 출력을 쓰시오.

cout << SQ(a + 1) << ' ' << SQ2(a + 1) << ' '
     << 100 / SQ2(5) << ' ' << 100 / SQ(5) << '\n';
정답 확인
7 16 4 100
식치환 결과값
SQ(a + 1)a + 1 * a + 1 = 3 + 3 + 17
SQ2(a + 1)((a + 1) * (a + 1))16
100 / SQ2(5)100 / ((5) * (5))4
100 / SQ(5)100 / 5 * 5 = 20 × 5100

인자가 두 번 들어가므로 SQ2(i++)처럼 값을 바꾸는 식을 넣으면 안 됩니다.


10. 표준 라이브러리와 네임스페이스

헤더주요 기능
<iostream>cin, cout
<string>string
<vector>vector
<cmath>sqrt, pow, fabs, ceil, floor, log, exp

ceil(3.2) = 4, floor(3.8) = 3, floor(-3.2) = −4 (정수 변환의 절단과 다름!), sqrt(16.0) = 4.

네임스페이스는 같은 이름을 구역으로 나눕니다. game::score처럼 ::로 소속을 지정하고, using namespace std;는 std::를 생략하게 해 줍니다. 필요한 이름만 가져오려면 using std::cout;.


핵심 정리

  1. 함수는 호출 전에 선언(원형)되어야 한다. 일반 매개변수는 값이 복사된다.
  2. 오버로딩은 매개변수 목록으로 구분한다. 반환형만 다르면 안 되고, 모호한 호출은 컴파일 오류다.
  3. 기본 인수는 오른쪽부터 채운다.
  4. 재귀 호출 앞의 코드는 내려가며, 뒤의 코드는 올라오며 실행된다.
  5. 재귀 값은 기저부터 표로 채운다. 기저 조건 위치(x <= 0, x - 1 <= 0, x < 0)를 확인한다.
  6. 재귀 → 반복: 기저값으로 시작, 같은 인수 k번 호출은 k ×로 합치고, 1부터 x까지 올라간다.
  7. 안쪽 이름은 바깥 이름을 가린다. 가려진 전역 변수는 ::x.
  8. static 지역 변수는 한 번만 초기화되고 호출 사이(재귀 호출 포함)에 값을 유지한다.
  9. #define은 텍스트 치환이다. 인자와 전체를 괄호로 감싼다.
오늘도 한 걸음 앞으로.

내용을 이해했다면 학습 완료를 체크해 주세요.

학습 기록은 현재 브라우저에 저장됩니다.
HJS / STUDY한지석