함수, 재귀, 스코프
함수, 재귀, 스코프
학습 흐름
함수의 구성과 원형 → 값에 의한 전달 → 오버로딩과 기본 인수
↓
재귀(기저 조건, 호출 추적) → 재귀를 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는 13. 함수 오버로딩과 기본 인수
오버로딩
이름이 같고 매개변수 목록(개수 또는 자료형)이 다른 함수를 여러 개 만들 수 있습니다. 컴파일러가 인수의 자료형을 보고 가장 잘 맞는 함수를 고릅니다.
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 6f(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);
}- 기저값:
f(0) = 5(그리고 x ≤ 0이면 모두 5) - 점화식 정리: 같은 인수로 두 번 호출하므로
f(x) = 2 × f(x - 1) - 표: f(0)=5, f(1)=10, f(2)=20, f(3)=40, f(4)=80 → 일반식
f(n) = 5 × 2ⁿ
5. 재귀를 for / while로 바꾸기
변환 순서 (4단계)
- 기저값을 찾는다 → 반복문의 시작값.
- 재귀 단계를
f(x) = (f(x-1)을 쓰는 식)꼴로 한 번 호출만 남게 정리한다.- 같은 인수로 k번 호출 →
k × f(x-1)로 합친다.
- 같은 인수로 k번 호출 →
- 기저에서 시작해 x까지 올라가며 2의 식을 반복한다.
- 기저 조건보다 작은 입력(예: 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);
}g(0),g(1),g(2),g(3),g(-2)를 구하시오.- 같은 값을 반환하는
for반복문 함수와while반복문 함수를 작성하시오. - 기저 조건을
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 |
|---|---|---|---|---|
| 1 | 0 | 1 | a[0] = 1·2+1 = 3 | 1 |
| 2 | 1 | 0 | a[1] = 2·2+0 = 4 | 1 |
| 3 | 1 | 1 | a[1] = 4·2+1 = 9 | 2 |
| 4 | 2 | 0 | a[2] = 3·2+0 = 6 | 2 |
| 5 | 2 | 1 | a[2] = 6·2+1 = 13 | 3 |
| 6 | 3 | 0 | a[3] = 4·2+0 = 8 | 3 |
| 7 | 3 | 1 | a[3] = 8·2+1 = 17 | 4 |
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 + 1 | 7 |
SQ2(a + 1) | ((a + 1) * (a + 1)) | 16 |
100 / SQ2(5) | 100 / ((5) * (5)) | 4 |
100 / SQ(5) | 100 / 5 * 5 = 20 × 5 | 100 |
인자가 두 번 들어가므로 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;.
핵심 정리
- 함수는 호출 전에 선언(원형)되어야 한다. 일반 매개변수는 값이 복사된다.
- 오버로딩은 매개변수 목록으로 구분한다. 반환형만 다르면 안 되고, 모호한 호출은 컴파일 오류다.
- 기본 인수는 오른쪽부터 채운다.
- 재귀 호출 앞의 코드는 내려가며, 뒤의 코드는 올라오며 실행된다.
- 재귀 값은 기저부터 표로 채운다. 기저 조건 위치(
x <= 0,x - 1 <= 0,x < 0)를 확인한다. - 재귀 → 반복: 기저값으로 시작, 같은 인수 k번 호출은
k ×로 합치고, 1부터 x까지 올라간다. - 안쪽 이름은 바깥 이름을 가린다. 가려진 전역 변수는
::x. static지역 변수는 한 번만 초기화되고 호출 사이(재귀 호출 포함)에 값을 유지한다.#define은 텍스트 치환이다. 인자와 전체를 괄호로 감싼다.
내용을 이해했다면 학습 완료를 체크해 주세요.