제어문, 함수와 재귀
조건·반복 제어와 함수 호출, 매개변수 전달, 스코프·재귀의 실행 과정을 학습한다.
1. 구조적 프로그래밍의 세 가지 제어 구조
프로그램의 흐름은 크게 순차, 선택, 반복으로 구성한다.
순차: A → B → C
선택: 조건 ─참─► A
└거짓► B
반복: 조건 확인 → 본문 실행 → 조건 재확인
- 순차 구조: 작성한 명령을 앞에서부터 수행한다.
- 선택 구조: 조건에 따라 실행 경로를 고른다.
- 반복 구조: 조건을 만족하는 동안 또는 정해진 횟수만큼 수행한다.
복잡한 goto 사용을 줄이고 세 구조를 조합하면 코드 흐름을 분석하기 쉽다.
2. 조건문
if 계열
if (score >= 90) {
grade = 'A';
} else if (score >= 80) {
grade = 'B';
} else {
grade = 'C';
}
여러 조건이 겹칠 때는 먼저 만족한 분기만 실행된다. 따라서 범위가 큰 조건을 앞에 두면 뒤 조건이 도달 불가능해질 수 있다.
switch 계열
C·Java의 switch는 한 값에 따라 분기할 때 유용하다. C에서는 break가 없으면 다음 case로 계속 진행하는 fall-through가 발생한다.
int x = 2;
switch (x) {
case 1: printf("A");
case 2: printf("B");
case 3: printf("C"); break;
default: printf("D");
}
// 출력: BC
3. 반복문
| 반복문 | 조건 검사 시점 | 적합한 상황 |
|---|---|---|
for | 반복 전 | 횟수나 인덱스가 명확함 |
while | 반복 전 | 조건 중심 반복 |
do-while | 반복 후 | 본문을 최소 한 번 실행해야 함 |
반복문을 판독할 때는 다음 표를 직접 그리는 것이 안전하다.
반복 | i 시작값 | 조건 결과 | 본문 효과 | i 변경값 | 누적값
1 | | | | |
2 | | | | |
예시:
int sum = 0;
for (int i = 1; i <= 5; i += 2) {
sum += i;
}
System.out.println(sum); // 1 + 3 + 5 = 9
break는 가장 가까운 반복 또는 switch를 종료하고, continue는 현재 반복의 나머지를 건너뛰고 다음 반복으로 이동한다.
4. 함수의 구성
함수는 입력을 받아 작업을 수행하고 결과를 반환할 수 있는 코드 단위이다.
호출자
│ 인수(argument)
▼
함수 진입 → 매개변수(parameter) 생성 → 지역변수 생성
│
├─ 함수 본문 수행
▼
반환값 전달 → 지역변수 제거 → 호출 다음 위치로 복귀
int add(int a, int b) {
return a + b;
}
int main(void) {
int result = add(3, 4);
return 0;
}
표준 C 프로그램의 대표 진입점은 int main(void) 또는 인수를 받는 int main(int argc, char *argv[]) 형태이다.
5. 매개변수 전달
C
C는 기본적으로 값을 복사해 전달한다. 호출자의 변수를 변경하려면 주소를 전달하고 포인터로 역참조한다.
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
Java
Java도 항상 값을 전달한다. 객체를 넘길 때는 객체를 가리키는 참조값의 복사본이 전달된다. 함수 안에서 객체 상태는 변경할 수 있지만 매개변수 자체를 다른 객체로 바꿔도 호출자의 참조 변수는 바뀌지 않는다.
static void change(Box b) {
b.value = 20;
b = new Box(99);
}
Python
Python은 객체 참조가 함수의 지역 이름에 바인딩된다. 변경 가능한 객체를 수정하면 호출자에서도 보이지만, 매개변수 이름을 다른 객체에 재바인딩하면 호출자 이름은 그대로이다.
def change(items):
items.append(3)
items = [99]
6. 스코프와 수명
- 스코프: 이름을 참조할 수 있는 코드 범위
- 수명: 값이나 객체가 메모리에 존재하는 기간
- 지역변수: 함수·블록 내부에서 사용
- 전역변수: 넓은 영역에서 사용되지만 의존성과 결합도를 높일 수 있음
- 정적 지역변수(C): 함수 호출이 끝나도 값이 유지됨
변수 이름이 겹치면 가까운 범위의 선언이 바깥 선언을 가리는 shadowing이 발생할 수 있다.
7. 재귀 함수
재귀는 함수가 자신을 직접 또는 간접 호출하는 방식이다. 반드시 종료조건과 문제 크기가 줄어드는 과정이 필요하다.
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
factorial(4)의 호출 과정은 다음과 같다.
factorial(4)
4 × factorial(3)
3 × factorial(2)
2 × factorial(1)
1
2 × 1 = 2
3 × 2 = 6
4 × 6 = 24
호출 스택은 다음처럼 쌓였다가 역순으로 제거된다.
TOP ┌─────────────────┐
│ factorial(1) │ ← 먼저 반환
├─────────────────┤
│ factorial(2) │
├─────────────────┤
│ factorial(3) │
├─────────────────┤
│ factorial(4) │
└─────────────────┘
종료조건이 없거나 문제 크기가 줄지 않으면 무한 재귀 또는 스택 오버플로가 발생한다.
8. 반복과 재귀 비교
| 기준 | 반복 | 재귀 |
|---|---|---|
| 상태 저장 | 반복 변수 | 호출 스택 |
| 오버헤드 | 일반적으로 작음 | 함수 호출·스택 비용 발생 |
| 표현 | 단순 반복에 유리 | 트리·분할정복처럼 재귀 구조에 자연스러움 |
| 위험 | 무한 반복 | 종료조건 오류·스택 오버플로 |
9. 코드 판독 절차
① 변수 초기값 기록
② 조건식의 참·거짓 판단
③ 반복별 변경값을 표로 기록
④ 함수 호출 시 매개변수와 지역변수 분리
⑤ 재귀는 호출 단계와 반환 단계를 따로 기록
⑥ break·continue·return의 이동 위치 확인
10. 중첩 제어문의 이동 범위
break와 continue는 기본적으로 가장 가까운 반복문에 적용된다.
int count = 0;
for (int i=0; i<3; i++) {
for (int j=0; j<3; j++) {
if (j == 1) break;
count++;
}
}
// 각 i에서 j=0만 수행하므로 count=3
Java의 라벨을 사용하면 바깥 반복까지 종료할 수 있지만, 남용하면 흐름을 읽기 어려워진다.
outer:
for (int i=0; i<3; i++) {
for (int j=0; j<3; j++) {
if (i+j == 2) break outer;
}
}
11. 재귀 호출 수와 최대 스택 깊이
재귀 문제는 총 호출 수와 동시에 쌓이는 최대 깊이를 구분한다.
void g(int n) {
if (n == 0) return;
g(n-1);
g(n-1);
}
g(3)은 총 15회 호출되지만 최대 스택 깊이는 4이다. 총 호출 수는 T(n)=1+2T(n-1), 최대 깊이는 한 경로만 따라가므로 n+1이다.
g(3)
├─ g(2)
│ ├─ g(1)
│ └─ g(1)
└─ g(2)
├─ g(1)
└─ g(1)
12. 호출 전·후 부수효과 추적
void print_order(int n) {
if (n == 0) return;
printf("%d", n); // 내려가는 단계
print_order(n-1);
printf("%d", n); // 돌아오는 단계
}
print_order(3)의 출력은 321123이다. 재귀 앞의 문장은 호출 순서대로, 재귀 뒤의 문장은 반환 순서대로 실행된다.
13. 꼬리 재귀와 최적화 전제
꼬리 재귀는 마지막 연산이 재귀 호출인 형태지만, Java와 Python은 일반적으로 꼬리 호출 최적화를 보장하지 않는다. C도 언어 표준이 최적화를 의무화하지 않는다. 따라서 꼬리 재귀라고 해서 스택 사용이 반드시 반복문과 같다고 단정하면 안 된다.
14. 함수 인수와 객체 변경
값 전달 공통점
- C: 정수값·포인터값의 복사
- Java: 기본값·참조값의 복사
- Python: 객체가 지역 이름에 바인딩
판독 기준
1. 매개변수 이름을 재대입했는가?
2. 매개변수가 가리키는 변경 가능한 객체를 수정했는가?
3. 호출자와 같은 객체를 공유하는가?
확인 문제
- 중첩 반복문 안의 일반
break는 어느 반복문을 종료하는가? - 두 번 재귀 호출하는
g(3)의 총 호출 수와 최대 스택 깊이는 각각 얼마인가? print_order(3)이 재귀 호출 전후에 n을 출력할 때 결과 문자열은 무엇인가?- 꼬리 재귀이면 Java와 Python에서 스택 사용이 반드시 일정해지는가?
- Java 함수가 객체 매개변수에 새 객체를 대입해도 호출자의 참조 변수가 바뀌지 않는 이유는 무엇인가?