함수, 변수 범위, 재귀
C프로그래밍 학습 절입니다.
2장 2절에서는 C에서 계산, 선택, 반복을 배웠습니다.
연산자 = 계산
조건문 = 선택
반복문 = 반복2장 3절에서는 코드를 덩어리로 묶어서 재사용하는 방법을 배웁니다.
이번 절의 핵심은 다음과 같습니다.
함수 = 코드를 기능별로 묶은 것
매개변수 = 함수에 전달하는 값
return = 함수가 결과를 돌려주는 것
변수 범위 = 변수를 사용할 수 있는 구역
재귀 = 함수가 자기 자신을 다시 호출하는 것이 내용은 C프로그래밍 출제범위 중 함수의 개념, 함수의 정의, 함수의 사용, 자료전달 방법, 되부름, 표준함수, 변수 범위, 지역변수와 전역변수, 정적변수, 레지스터변수, 외부변수, 선언과 정의, 헤더파일과 소스파일, 조건부 컴파일에 해당합니다.
함수 기본
함수란?
함수는 특정 일을 하는 코드 묶음입니다.
예를 들어 프로그램 안에서 “두 수를 더하는 일”이 자주 필요하다고 가정하겠습니다.
매번 이렇게 쓰면 귀찮습니다.
result = a + b;그래서 이 일을 함수로 만들어둘 수 있습니다.
int add(int a, int b) {
return a + b;
}이제 필요할 때마다 이렇게 부르면 됩니다.
result = add(3, 5);결과는 8입니다.
함수의 핵심
함수 = 입력을 받아서 일을 하고 결과를 돌려주는 코드 묶음수학 함수와 비슷하게 생각해도 됩니다.
f(x) = x + 1여기서 x에 3을 넣으면 4가 나옵니다.
C 함수도 비슷합니다.
int f(int x) {
return x + 1;
}왜 함수를 쓰는가?
함수를 쓰는 이유는 크게 네 가지입니다.
| 이유 | 설명 |
|---|---|
| 코드 재사용 | 같은 코드를 여러 번 쓰지 않아도 됨 |
| 코드 정리 | 기능별로 나눌 수 있음 |
| 오류 감소 | 한 곳만 고치면 여러 곳에 반영됨 |
| 이해 쉬움 | 프로그램 구조가 깔끔해짐 |
예를 들어 계산기 프로그램을 만든다고 가정하겠습니다.
함수 없이 만들면 모든 코드가 main 안에 몰립니다.
int main(void) {
// 더하기 코드
// 빼기 코드
// 곱하기 코드
// 나누기 코드
// 출력 코드
}함수로 나누면 훨씬 깔끔해집니다.
int add(int a, int b) { ... }
int sub(int a, int b) { ... }
int mul(int a, int b) { ... }
int div(int a, int b) { ... }즉 함수는 프로그램을 작은 부품으로 나누는 도구입니다.
함수의 기본 구조
C 함수는 보통 이렇게 생겼습니다.
반환형 함수이름(매개변수) {
실행할 문장;
return 반환값;
}int add(int a, int b) {
int result;
result = a + b;
return result;
}하나씩 살펴보겠습니다.
| 부분 | 의미 |
|---|---|
int | 함수가 정수 값을 돌려준다는 뜻 |
add | 함수 이름 |
int a, int b | 함수가 받을 값 |
{ } | 함수 몸체 |
return result; | result 값을 호출한 곳으로 돌려줌 |
함수 호출
함수를 만들어도 부르지 않으면 실행되지 않습니다.
함수를 사용하는 것을 함수 호출이라고 합니다.
int result;
result = add(3, 5);이 코드는 이렇게 동작합니다.
1. add 함수에게 3과 5를 전달합니다.
2. add 함수 안에서 3 + 5를 계산합니다.
3. 결과 8을 return합니다.
4. result 변수에 8이 저장됩니다.#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main(void) {
int result;
result = add(3, 5);
printf("%d", result);
return 0;
}8반환형
반환형은 함수가 어떤 종류의 값을 돌려주는지 나타냅니다.
| 반환형 | 의미 |
|---|---|
int | 정수를 돌려줌 |
float | 실수를 돌려줌 |
double | 더 정밀한 실수를 돌려줌 |
char | 문자 하나를 돌려줌 |
void | 돌려주는 값이 없음 |
int add(int a, int b) {
return a + b;
}이 함수는 정수를 돌려줍니다.
그래서 반환형이 int입니다.
void 함수
어떤 함수는 결과값을 돌려줄 필요가 없습니다.
그럴 때 반환형을 void로 씁니다.
void hello(void) {
printf("Hello");
}이 함수는 화면에 Hello를 출력할 뿐, 값을 돌려주지는 않습니다.
호출은 이렇게 합니다.
hello();#include <stdio.h>
void hello(void) {
printf("Hello");
}
int main(void) {
hello();
return 0;
}Hellovoid의 의미
| 위치 | 의미 |
|---|---|
반환형의 void | 돌려주는 값이 없음 |
매개변수의 void | 받는 값이 없음 |
void hello(void)받는 값도 없고, 돌려주는 값도 없는 함수매개변수와 인수
함수에서 헷갈리는 말이 있습니다.
매개변수
인수예를 살펴보겠습니다.
int add(int a, int b) {
return a + b;
}
result = add(3, 5);여기서는 다음과 같습니다.
| 구분 | 예 | 의미 |
|---|---|---|
| 매개변수 | int a, int b | 함수 정의에서 값을 받는 변수 |
| 인수 | 3, 5 | 함수 호출할 때 실제로 넣는 값 |
인수 = 실제로 넣는 값
매개변수 = 그 값을 받는 함수 안의 변수함수 실행 흐름
다음 코드를 살펴보겠습니다.
#include <stdio.h>
int square(int x) {
return x * x;
}
int main(void) {
int a = 4;
int result;
result = square(a);
printf("%d", result);
return 0;
}1. main 함수 시작
2. a에 4 저장
3. square(a) 호출
4. a의 값 4가 square 함수의 x로 전달
5. x * x 계산, 즉 4 * 4 = 16
6. return 16
7. result에 16 저장
8. 16 출력16함수 원형, 함수 선언
C에서는 함수를 사용하기 전에 함수가 어떤 모양인지 알려줘야 할 때가 있습니다.
예를 들어 다음 코드는 문제가 생길 수 있습니다.
#include <stdio.h>
int main(void) {
int result;
result = add(3, 5);
printf("%d", result);
return 0;
}
int add(int a, int b) {
return a + b;
}main 함수에서 add를 호출하는데, 컴파일러는 그 시점에서 add가 뭔지 아직 모릅니다.
그래서 함수 원형을 위에 적어줍니다.
int add(int a, int b);#include <stdio.h>
int add(int a, int b);
int main(void) {
int result;
result = add(3, 5);
printf("%d", result);
return 0;
}
int add(int a, int b) {
return a + b;
}int add(int a, int b);이 부분이 함수 선언 또는 함수 원형입니다.
선언과 정의
C에서 선언과 정의는 구분해야 합니다.
| 구분 | 의미 | 예 |
|---|---|---|
| 선언 | 이런 함수가 있다고 알려줌 | int add(int a, int b); |
| 정의 | 함수의 실제 내용을 작성함 | int add(int a, int b) { return a + b; } |
선언 = 예고편
정의 = 본편C프로그래밍 범위에도 프로그램 구조에서 선언과 정의, 헤더파일과 소스파일이 포함되어 있습니다.
return
return은 함수의 결과를 호출한 곳으로 돌려줍니다.
int add(int a, int b) {
return a + b;
}return a + b;는 a + b 값을 돌려준다는 뜻입니다.
그리고 return을 만나면 함수는 종료됩니다.
int test(void) {
return 10;
return 20;
}이 함수는 10을 돌려줍니다.
두 번째 return 20;은 실행되지 않습니다.
반환값이 없는 함수의 return
void 함수에서도 return을 쓸 수 있습니다.
void printPositive(int x) {
if (x <= 0) {
return;
}
printf("%d", x);
}여기서 return;은 값을 돌려주는 것이 아니라 함수를 즉시 끝내는 역할입니다.
void 함수의 return = 함수 종료값에 의한 전달
C 함수는 기본적으로 값에 의한 전달을 합니다.
말이 어렵지만 핵심은 간단합니다.
함수에 변수를 넘기면 원본이 넘어가는 것이 아니라 값의 복사본이 넘어갑니다.
예를 살펴보겠습니다.
#include <stdio.h>
void change(int x) {
x = 100;
}
int main(void) {
int a = 10;
change(a);
printf("%d", a);
return 0;
}출력은?
10왜 100이 아닐까?
a의 값 10이 change 함수의 x로 복사됩니다.
change 함수 안에서 x를 100으로 바꿉니다.
하지만 x는 복사본입니다.
main의 a는 그대로 10입니다.함수 안에서 매개변수를 바꿔도 원래 변수는 바뀌지 않습니다.이것이 C의 기본 자료전달 방식입니다.
원본을 바꾸려면?
원본 변수를 함수 안에서 바꾸고 싶으면 주소를 넘겨야 합니다.
주소를 다루는 것이 포인터입니다.
포인터는 2장 4절에서 자세히 배우지만, 이번 절에서는 간단히 살펴보겠습니다.
#include <stdio.h>
void change(int *p) {
*p = 100;
}
int main(void) {
int a = 10;
change(&a);
printf("%d", a);
return 0;
}100여기서는 &a로 a의 주소를 넘겼고, 함수 안에서 *p = 100;으로 그 주소에 있는 값을 바꿨습니다.
지금은 이렇게만 기억해 둡니다.
값만 넘기면 원본 안 바뀜
주소를 넘기면 원본 바꿀 수 있음표준 함수
C에는 이미 만들어져 있는 함수들이 있습니다.
예를 들어 우리는 이미 printf와 scanf를 썼습니다.
printf("Hello");
scanf("%d", &a);이런 함수들을 표준 함수라고 합니다.
대표 표준 함수는 다음과 같습니다.
| 함수 | 헤더 | 역할 |
|---|---|---|
printf | stdio.h | 출력 |
scanf | stdio.h | 입력 |
sqrt | math.h | 제곱근 |
pow | math.h | 거듭제곱 |
strlen | string.h | 문자열 길이 |
strcpy | string.h | 문자열 복사 |
#include <stdio.h>
#include <math.h>
int main(void) {
double x = sqrt(16.0);
printf("%f", x);
return 0;
}4.000000C프로그래밍 범위에는 함수 부분에 표준함수도 포함되어 있습니다.
변수의 범위와 저장 방식
변수 범위란?
변수 범위는 변수를 사용할 수 있는 구역입니다.
영어로는 scope라고 합니다.
예를 들어 어떤 변수는 함수 안에서만 사용할 수 있고, 어떤 변수는 여러 함수에서 사용할 수 있습니다.
C에서 중요한 변수 종류는 다음과 같습니다.
| 변수 종류 | 의미 |
|---|---|
| 지역변수 | 특정 함수나 블록 안에서만 사용 |
| 전역변수 | 함수 바깥에 선언되어 여러 함수에서 사용 |
| 정적변수 | 함수가 끝나도 값이 유지되는 변수 |
| 레지스터변수 | 레지스터 사용을 요청하는 변수 |
| 외부변수 | 다른 파일에 있는 전역변수를 참조 |
이번 절에서는 특히 지역변수, 전역변수, 정적변수가 중요합니다.
지역변수
지역변수는 함수나 블록 안에서 선언된 변수입니다.
void test(void) {
int x = 10;
printf("%d", x);
}여기서 x는 test 함수 안에서만 사용할 수 있습니다.
함수 밖에서는 사용할 수 없습니다.
void test(void) {
int x = 10;
}
int main(void) {
printf("%d", x); // 오류
return 0;
}왜냐하면 x는 test 함수 안의 지역변수이기 때문입니다.
지역변수 핵심
지역변수 = 선언된 블록 안에서만 살아 있음블록 범위
중괄호 { } 안에서 선언한 변수는 보통 그 블록 안에서만 쓸 수 있습니다.
int main(void) {
if (1) {
int x = 10;
printf("%d", x);
}
printf("%d", x); // 오류
return 0;
}x는 if 블록 안에서 선언되었기 때문에, 블록 밖에서는 사용할 수 없습니다.
{ } 안에서 만든 변수는 보통 그 { } 안에서만 사용 가능전역변수
전역변수는 함수 바깥에 선언한 변수입니다.
#include <stdio.h>
int global = 10;
void printGlobal(void) {
printf("%d\n", global);
}
int main(void) {
printGlobal();
printf("%d", global);
return 0;
}10
10global은 함수 바깥에 선언되어 있으므로 printGlobal에서도, main에서도 사용할 수 있습니다.
전역변수 핵심
전역변수 = 함수 바깥에 선언되어 여러 함수에서 사용 가능하지만 전역변수는 너무 많이 쓰면 프로그램이 복잡해집니다.
왜냐하면 여러 함수가 같은 변수를 마음대로 바꿀 수 있기 때문입니다.
지역변수와 전역변수 이름이 같으면?
다음 코드를 살펴보겠습니다.
#include <stdio.h>
int x = 100;
int main(void) {
int x = 10;
printf("%d", x);
return 0;
}출력은?
10왜냐하면 main 안의 지역변수 x가 전역변수 x보다 우선하기 때문입니다.
가까운 지역변수가 우선정적변수 static
정적변수는 static을 붙여 만듭니다.
함수 안에서 static으로 선언된 변수는 함수가 끝나도 값이 사라지지 않습니다.
#include <stdio.h>
void count(void) {
static int x = 0;
x++;
printf("%d ", x);
}
int main(void) {
count();
count();
count();
return 0;
}1 2 3왜 그럴까?
x가 일반 지역변수였다면 함수를 호출할 때마다 새로 만들어지고 사라졌을 것입니다.
하지만 static int x = 0;은 한 번만 초기화되고, 함수가 끝나도 값을 유지합니다.
첫 번째 count 호출: x = 0 → x++ → 1 출력
두 번째 count 호출: x는 이전 값 1 유지 → x++ → 2 출력
세 번째 count 호출: x는 이전 값 2 유지 → x++ → 3 출력정적변수 핵심
static 지역변수 = 범위는 지역변수처럼 함수 안, 수명은 프로그램 끝까지이 말이 중요합니다.
| 구분 | 사용 가능 범위 | 값 유지 |
|---|---|---|
| 일반 지역변수 | 함수 안 | 함수 끝나면 사라짐 |
| static 지역변수 | 함수 안 | 함수 끝나도 유지 |
| 전역변수 | 여러 함수 | 프로그램 끝까지 유지 |
정적변수 시험형 예제
다음 코드를 살펴보겠습니다.
#include <stdio.h>
int sSum(int count) {
static int sTotal = 0;
sTotal += count;
return sTotal;
}
int main(void) {
int count;
int sTotal = 0;
for (count = 1; count <= 10; count++) {
sTotal = sSum(count);
}
printf("%d", sTotal);
return 0;
}sSum 함수 안의 sTotal은 static 변수입니다.
따라서 호출이 끝나도 값이 유지됩니다.
반복 흐름은 다음과 같습니다.
| count | static sTotal 변화 |
|---|---|
| 1 | 0 + 1 = 1 |
| 2 | 1 + 2 = 3 |
| 3 | 3 + 3 = 6 |
| 4 | 6 + 4 = 10 |
| 5 | 10 + 5 = 15 |
| 6 | 15 + 6 = 21 |
| 7 | 21 + 7 = 28 |
| 8 | 28 + 8 = 36 |
| 9 | 36 + 9 = 45 |
| 10 | 45 + 10 = 55 |
55객체지향프로그래밍 예시문제에도 static int sTotal = 0;을 이용해 누적합을 계산하는 코드가 나옵니다. 거기서 정적변수는 호출 사이에 값을 유지하기 때문에 1+2+...+10 = 55가 됩니다.
레지스터변수 register
register는 변수를 CPU 레지스터에 저장해달라고 요청하는 키워드입니다.
register int i;레지스터는 CPU 내부의 매우 빠른 저장공간입니다.
하지만 현대 컴파일러는 최적화를 알아서 잘 하기 때문에, register를 쓴다고 무조건 레지스터에 저장되는 것은 아닙니다.
시험에서는 이 정도로 기억하면 충분합니다.
register 변수 = 빠른 접근을 위해 레지스터 사용을 요청하는 변수C프로그래밍 예시문제에서도 register는 예약어로 나옵니다.
외부변수 extern
extern은 다른 파일에 있는 전역변수를 사용하겠다고 알려주는 키워드입니다.
예를 들어 파일이 두 개 있다고 가정하겠습니다.
file1.c
int score = 100;file2.c
extern int score;score라는 전역변수가 다른 파일 어딘가에 정의되어 있습니다.
나는 그것을 사용하겠습니다.입니다.
시험에서는 깊게 들어가기보다 이렇게 기억하면 충분합니다.
extern = 다른 파일의 전역변수를 참조할 때 사용변수 종류 비교
정리하면 이렇게 됩니다.
| 변수 종류 | 선언 위치 | 사용 가능 범위 | 값 유지 |
|---|---|---|---|
| 지역변수 | 함수나 블록 안 | 선언된 블록 안 | 블록 종료 시 사라짐 |
| 전역변수 | 함수 바깥 | 여러 함수 | 프로그램 끝까지 유지 |
| static 지역변수 | 함수 안 | 선언된 함수 안 | 프로그램 끝까지 유지 |
| register 변수 | 함수 안 | 선언된 블록 안 | 일반 지역변수와 비슷 |
| extern 변수 | 다른 파일 참조 | 선언 위치에 따라 사용 | 원래 전역변수의 수명 |
가장 중요하게 암기할 내용은 다음과 같습니다.
지역변수 = 안에서만
전역변수 = 밖에서 선언, 여러 함수에서
static = 값 유지
extern = 다른 파일 변수 참조
register = 빠른 저장 요청재귀 함수
재귀란?
이제 이번 절의 두 번째 큰 주제입니다.
재귀는 함수가 자기 자신을 다시 호출하는 것입니다.
void hello(void) {
hello();
}이런 식으로 자기 자신을 부르면 재귀입니다.
하지만 위 코드는 위험합니다.
왜냐하면 끝나는 조건이 없어서 무한히 자기 자신을 부르기 때문입니다.
재귀에는 반드시 종료 조건이 있어야 합니다.
재귀의 기본 구조
재귀 함수는 보통 이렇게 생겼습니다.
반환형 함수이름(매개변수) {
if (종료 조건) {
return 값;
}
return 자기자신(더 작은 문제);
}핵심은 두 가지입니다.
| 요소 | 의미 |
|---|---|
| 종료 조건 | 재귀를 멈추는 조건 |
| 재귀 호출 | 자기 자신을 다시 호출 |
종료 조건이 없으면 무한 호출이 됩니다.
팩토리얼
재귀를 배울 때 가장 대표적인 예가 팩토리얼입니다.
팩토리얼은 !로 표시합니다.
5! = 5 × 4 × 3 × 2 × 1 = 120n! = n × (n-1) × (n-2) × ... × 10! = 1
1! = 1factorial(n) = n × factorial(n-1)
factorial(1) = 1int factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}printf("%d", factorial(5));팩토리얼 실행 흐름
factorial(5)를 따라가 살펴보겠습니다.
factorial(5)
= 5 * factorial(4)
= 5 * 4 * factorial(3)
= 5 * 4 * 3 * factorial(2)
= 5 * 4 * 3 * 2 * factorial(1)
= 5 * 4 * 3 * 2 * 1
= 120120재귀는 처음에는 아래로 계속 내려갔다가, 종료 조건을 만나면 다시 위로 올라오며 계산이 끝납니다.
내려감: 5 → 4 → 3 → 2 → 1
올라옴: 1 → 2 → 6 → 24 → 120재귀와 스택
재귀 함수가 호출되면 함수 호출 정보가 메모리의 스택에 쌓입니다.
이 부분은 나중에 자료구조의 스택과 연결됩니다.
factorial(5)
factorial(4)
factorial(3)
factorial(2)
factorial(1)이렇게 호출이 쌓입니다.
그리고 factorial(1)이 끝나면 역순으로 돌아갑니다.
factorial(1) 종료
factorial(2) 종료
factorial(3) 종료
factorial(4) 종료
factorial(5) 종료즉 재귀는 자료구조의 스택과 아주 밀접합니다.
자료구조 범위에도 순환 알고리즘, 스택, 서브루틴 주소 복귀가 포함되어 있습니다.
재귀 예제: 1부터 n까지 합
1부터 n까지 합을 구하는 함수를 재귀로 만들어 보겠습니다.
sum(n) = n + sum(n-1)
sum(1) = 1int sum(int n) {
if (n == 1) {
return 1;
}
return n + sum(n - 1);
}printf("%d", sum(5));sum(5)
= 5 + sum(4)
= 5 + 4 + sum(3)
= 5 + 4 + 3 + sum(2)
= 5 + 4 + 3 + 2 + sum(1)
= 5 + 4 + 3 + 2 + 1
= 1515중요한 재귀 시험형 예제
다음 함수를 살펴보겠습니다.
int rSum(int value) {
int sum = 0;
if (value != 10)
sum += rSum(value + 1);
else
sum += value;
return sum;
}그리고 이렇게 호출합니다.
rSum(0);이 코드는 얼핏 보면 0+1+2+...+10을 더할 것 같지만 아닙니다.
주의해서 봐야 합니다.
if (value != 10)
sum += rSum(value + 1);
else
sum += value;value가 10이 아닐 때는 자기 자신의 결과만 더합니다.
현재 value를 더하지 않습니다.sum += value;를 실행합니다.
따라서 흐름은 이렇게 됩니다.
rSum(0) = rSum(1)
rSum(1) = rSum(2)
rSum(2) = rSum(3)
...
rSum(9) = rSum(10)
rSum(10) = 1010입니다.
객체지향프로그래밍 예시문제에 이 재귀 함수와 static 누적 함수가 함께 나오며, rSum(0)의 결과는 10이고, static 누적합 결과는 55라서 출력은 10, 55가 됩니다.
왜 rSum 결과가 55가 아닌가?
많이 헷갈리는 부분입니다.
만약 0부터 10까지 더하는 함수라면 이렇게 생겨야 합니다.
int rSum(int value) {
if (value == 10) {
return 10;
}
return value + rSum(value + 1);
}이렇게 해야 현재 value도 더합니다.
rSum(0) = 0 + rSum(1)
rSum(1) = 1 + rSum(2)
...
rSum(10) = 100 + 1 + 2 + ... + 10 = 55하지만 예시문제의 코드는 현재 value를 더하지 않고 재귀 결과만 전달합니다.
sum += rSum(value + 1);그래서 최종적으로 10만 돌아옵니다.
재귀와 반복문 비교
재귀로 할 수 있는 일은 반복문으로도 할 수 있는 경우가 많습니다.
반복문으로 1부터 n까지 합
int sumLoop(int n) {
int i;
int sum = 0;
for (i = 1; i <= n; i++) {
sum += i;
}
return sum;
}재귀로 1부터 n까지 합
int sumRec(int n) {
if (n == 1) {
return 1;
}
return n + sumRec(n - 1);
}비교는 다음과 같습니다.
| 구분 | 반복문 | 재귀 |
|---|---|---|
| 방식 | 같은 코드를 반복 | 자기 자신을 호출 |
| 장점 | 메모리 사용이 적은 편 | 문제 구조가 깔끔할 수 있음 |
| 단점 | 복잡한 구조는 코드가 길어질 수 있음 | 호출이 많으면 스택 사용 증가 |
| 대표 사용 | 단순 반복 | 트리, 그래프, 분할정복 |
자료구조에서 트리 순회, 그래프 탐색, 퀵 정렬, 병합 정렬 등을 배울 때 재귀가 다시 나옵니다.
무한 재귀
종료 조건이 없으면 무한 재귀가 됩니다.
void f(void) {
f();
}이 코드는 끝없이 자기 자신을 호출합니다.
또 이런 코드도 위험합니다.
int factorial(int n) {
return n * factorial(n - 1);
}종료 조건이 없기 때문입니다.
int factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}재귀는 반드시 이것을 기억해야 합니다.
재귀에는 종료 조건이 있어야 합니다.
문제가 점점 작아져야 합니다.함수 호출과 지역변수의 관계
함수가 호출될 때마다 그 함수의 지역변수는 새로 만들어집니다.
int f(int n) {
int x = n;
return x;
}f(1), f(2), f(3)을 호출하면 각각의 호출마다 별도의 x가 있다고 이해하면 됩니다.
재귀에서는 이 점이 특히 중요합니다.
int sum(int n) {
if (n == 1) {
return 1;
}
return n + sum(n - 1);
}sum(5)의 n과 sum(4)의 n은 서로 다른 지역변수입니다.
sum(5)의 n = 5
sum(4)의 n = 4
sum(3)의 n = 3함수 이름은 같지만 호출마다 다른 공간이 생깁니다.
컴파일 단위와 문제 풀이
헤더파일과 소스파일
프로그램이 커지면 모든 함수를 한 파일에 넣지 않습니다.
보통 기능별로 파일을 나눕니다.
main.c
math_utils.c
math_utils.h헤더파일 .h
함수 선언을 넣습니다.
// math_utils.h
int add(int a, int b);
int sub(int a, int b);소스파일 .c
함수 정의를 넣습니다.
// math_utils.c
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}main 파일
필요한 헤더를 포함합니다.
#include <stdio.h>
#include "math_utils.h"
int main(void) {
printf("%d", add(3, 5));
return 0;
}시험에서는 깊은 프로젝트 구조보다 이 정도를 기억하면 충분합니다.
헤더파일 = 선언 중심
소스파일 = 정의 중심조건부 컴파일
조건부 컴파일은 특정 조건에 따라 컴파일할 코드를 선택하는 것입니다.
대표적으로 #if, #ifdef, #ifndef, #endif가 있습니다.
#define DEBUG
#ifdef DEBUG
printf("debug mode");
#endifDEBUG가 정의되어 있으면 안의 코드가 포함됩니다.
또 헤더파일 중복 포함을 막을 때도 씁니다.
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
#endif처음에는 어렵게 느껴질 수 있습니다. 지금은 이렇게만 기억하자.
조건부 컴파일 = 컴파일 전에 조건에 따라 코드를 포함하거나 제외하는 기능C프로그래밍 범위에도 프로그램 구조 부분에 조건부 컴파일이 포함됩니다.
함수 문제를 푸는 방법
시험에서 함수 문제가 나오면 다음 순서로 추적하면 됩니다.
1단계: main부터 시작
C 프로그램은 보통 main에서 시작합니다.
2단계: 함수 호출을 만날 때 함수로 이동
result = add(3, 5);를 만나면 add 함수로 갑니다.
3단계: 인수가 매개변수로 어떻게 들어가는지 확인
3 → a
5 → b4단계: 함수 안의 지역변수를 따로 생각
함수 안의 a, b, result는 그 함수의 것입니다.
5단계: return 값을 호출한 자리로 가져오기
return a + b;면 호출한 자리로 결과가 돌아갑니다.
6단계: static 변수인지 확인
static이 있으면 이전 호출의 값이 유지됩니다.
7단계: 재귀면 종료 조건부터 찾기
재귀 문제는 반드시 종료 조건을 먼저 찾아라.
if (n == 1) return 1;코드 추적 예제
코드 추적 예제 1
다음 코드의 출력은?
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main(void) {
int x = 2;
int y = 3;
printf("%d", add(x, y));
return 0;
}x = 2
y = 3
add(x, y) 호출
a = 2, b = 3
return 2 + 3 = 55코드 추적 예제 2
다음 코드의 출력은?
#include <stdio.h>
void change(int x) {
x = 20;
}
int main(void) {
int a = 10;
change(a);
printf("%d", a);
return 0;
}a의 값 10이 x로 복사됩니다.
change 안에서 x는 20이 됩니다.
하지만 main의 a는 바뀌지 않습니다.10C는 기본적으로 값에 의한 전달코드 추적 예제 3
다음 코드의 출력은?
#include <stdio.h>
void count(void) {
static int x = 0;
x++;
printf("%d", x);
}
int main(void) {
count();
count();
count();
return 0;
}첫 호출: x = 1 출력
둘째 호출: x = 2 출력
셋째 호출: x = 3 출력123코드 추적 예제 4
다음 코드의 출력은?
#include <stdio.h>
int f(int n) {
if (n == 1) {
return 1;
}
return n + f(n - 1);
}
int main(void) {
printf("%d", f(4));
return 0;
}f(4) = 4 + f(3)
f(3) = 3 + f(2)
f(2) = 2 + f(1)
f(1) = 1f(4) = 4 + 3 + 2 + 1 = 1010자주 혼동되는 출제 포인트
혼동 포인트 1. 함수는 호출해야 실행됩니다
함수를 정의만 해두면 실행되지 않습니다.
void hello(void) {
printf("Hello");
}이렇게만 있으면 출력되지 않습니다.
반드시 호출해야 합니다.
hello();혼동 포인트 2. return을 만나면 함수는 끝납니다
return 10;
return 20;두 번째 return은 실행되지 않습니다.
혼동 포인트 3. 값에 의한 전달
void change(int x) {
x = 100;
}이렇게 해도 원래 변수는 바뀌지 않습니다.
혼동 포인트 4. 지역변수는 함수 밖에서 못 씁니다
void f(void) {
int x = 10;
}x는 f 안에서만 쓸 수 있습니다.
혼동 포인트 5. 지역변수와 전역변수 이름이 같으면 지역변수가 우선
int x = 100;
int main(void) {
int x = 10;
printf("%d", x);
}출력은 100이 아니라 10입니다.
혼동 포인트 6. static 변수는 값이 유지됩니다
static int x = 0;함수가 끝나도 값이 사라지지 않습니다.
혼동 포인트 7. 재귀는 종료 조건이 필요합니다
int f(int n) {
return f(n - 1);
}이런 코드는 끝나지 않습니다.
혼동 포인트 8. 재귀 함수가 현재 값을 더하는지 확인해야 합니다
sum += rSum(value + 1);이 코드는 현재 value를 더하지 않습니다.
return value + rSum(value + 1);이 코드는 현재 value도 더합니다.
이번 절의 핵심 정리
함수 기본형
반환형 함수이름(매개변수) {
문장;
return 값;
}함수 예
int add(int a, int b) {
return a + b;
}함수 호출
result = add(3, 5);함수 선언
int add(int a, int b);값에 의한 전달
함수에 값의 복사본이 전달됩니다.
원본 변수는 바뀌지 않습니다.지역변수
함수나 블록 안에서만 사용 가능전역변수
함수 바깥에 선언, 여러 함수에서 사용 가능static 변수
함수가 끝나도 값 유지재귀
함수가 자기 자신을 호출
반드시 종료 조건 필요핵심 한 문장
이번 절의 핵심을 한 문장으로 정리하면 다음과 같습니다.
함수는 코드를 기능별로 묶어 재사용하게 해주고, 변수 범위는 변수를 사용할 수 있는 구역을 정하며, 재귀는 함수가 자기 자신을 호출해서 문제를 작게 나누는 방법입니다.
함수 = 코드 묶음
scope = 변수 사용 구역
static = 값 유지
재귀 = 자기 자신 호출확인 문제
문제 1
함수의 역할로 가장 알맞은 것은?
① 변수를 무조건 전역으로 만듭니다 ② 코드를 기능별로 묶어 재사용하게 합니다 ③ 반복문만 실행하게 합니다 ④ 자료형을 없앱니다
문제 2
다음 함수의 반환형은?
int add(int a, int b) {
return a + b;
}① add
② int
③ a
④ return
문제 3
다음 코드의 출력 결과는?
int add(int a, int b) {
return a + b;
}
int main(void) {
printf("%d", add(2, 5));
return 0;
}① 2 ② 5 ③ 7 ④ 25
문제 4
반환값이 없는 함수를 만들 때 사용하는 반환형은?
① int
② char
③ void
④ return
문제 5
C언어의 기본 자료 전달 방식으로 알맞은 것은?
① 참조에 의한 전달만 사용합니다 ② 값에 의한 전달을 기본으로 합니다 ③ 전역변수만 전달합니다 ④ 함수는 값을 전달할 수 없습니다
문제 6
다음 코드의 출력 결과는?
void change(int x) {
x = 100;
}
int main(void) {
int a = 10;
change(a);
printf("%d", a);
return 0;
}① 0 ② 10 ③ 100 ④ 오류
문제 7
함수 안에서 선언되어 그 함수 안에서만 사용할 수 있는 변수는?
① 전역변수 ② 지역변수 ③ 외부변수 ④ 헤더변수
문제 8
함수가 끝나도 값을 유지하는 지역변수를 만들 때 사용하는 키워드는?
① extern
② register
③ static
④ return
문제 9
다음 코드의 출력 결과는?
void count(void) {
static int x = 0;
x++;
printf("%d", x);
}
int main(void) {
count();
count();
count();
return 0;
}① 111
② 123
③ 000
④ 333
문제 10
재귀 함수에 반드시 필요한 것은?
① 전역변수 ② 종료 조건 ③ switch문 ④ printf문
문제 11
다음 코드의 출력 결과는?
int f(int n) {
if (n == 1) {
return 1;
}
return n + f(n - 1);
}
int main(void) {
printf("%d", f(4));
return 0;
}① 4 ② 6 ③ 10 ④ 24
문제 12
다음 설명으로 알맞은 것은?
함수가 자기 자신을 다시 호출하는 것① 반복 ② 재귀 ③ 대입 ④ 컴파일
정답과 해설은 절별 확인문제 정답해설에서 확인합니다.
다음 2장 4절은 배열, 문자열, 포인터, 구조체입니다. 여기서부터 C에서 가장 헷갈리는 부분으로 들어갑니다. 특히 배열은 자료구조의 시작, 포인터는 메모리 주소, 구조체는 서로 다른 자료를 하나로 묶는 방법입니다.