같은 일이 여러 곳에
고쳐야 할 곳이 흩어져 있으면 하나를 빠뜨리기 쉽습니다.
double r1 = 2.0;
double r2 = 3.5;
double r3 = 5.0;
printf("넓이: %.2f\n", 3.14159 * r1 * r1); // 같은 식이
printf("넓이: %.2f\n", 3.14159 * r2 * r2); // 세 군데에
printf("넓이: %.2f\n", 3.14159 * r3 * r3); // 흩어져 있다세 줄이 문제가 아닙니다
- 원주율을 더 정밀하게 바꾸려면 세 곳을 모두 고쳐야 합니다. 한 곳을 빠뜨리면 그 줄만 다른 결과를 출력합니다.
- 읽는 사람이
3.14159 * r * r을 하나씩 살펴보아야 「아, 원의 넓이구나」를 알 수 있습니다. - 이 계산이 필요한 곳이 열 군데가 되면 같은 식을 열 번 써야 합니다.
일에 이름을 붙여 한곳에 둡니다
함수(function)는 하나의 일을 하는 코드를 묶어 이름을 붙여 둔 것입니다. 묶어 두면 그 일이 필요한 곳에서 이름만 부르면 되고, 고칠 일이 생기면 묶어 둔 한 곳만 고치면 됩니다.
함수는 줄 수를 줄이려고만 쓰는 것이 아닙니다. 한 번만 쓰는 코드도 함수로 분리하면 좋을 때가 있습니다. 함수 이름이 「무엇을 하는 코드인지」를 알려 주기 때문입니다.
사실 우리는 함수를 이미 쓰고 있었습니다.
printf도 strlen도 누군가 미리 만들어 이름을 붙여 둔 함수입니다.
#include는 그런 함수들을 쓰는 데 필요한 정보가 모여 있는 파일을 포함하겠다는 표시였습니다.
이 차시에서는 함수를 가져다 쓰는 쪽에서 직접 만드는 쪽으로 넘어갑니다.
스스로 확인
지금까지 쓴 코드 가운데 같은 식이 두 번 이상 나온 곳을 하나 떠올려 보세요. 그 일을 함수로 만든다면 이름을 무엇이라 붙이겠습니까?
정의와 호출
원의 넓이를 구하는 세 줄을 그대로 함수로 옮겨 봅시다.
#include <stdio.h>
double area(double r) { // 반환형 / 함수 이름 / 매개변수
return 3.14159 * r * r; // 값을 계산해 호출한 곳으로 반환한다
}
int main(void) {
printf("넓이: %.2f\n", area(2.0));
printf("넓이: %.2f\n", area(3.5));
printf("넓이: %.2f\n", area(5.0));
return 0;
}
화면에 출력되는 결과는 『같은 일이 여러 곳에』 절의 코드와 똑같습니다.
하는 일이 달라진 것이 아니라 같은 일을 다르게 정리했을 뿐입니다.
이제 원주율은 한 곳에만 있고, 함수를 쓰는 쪽에서는 area라는 이름만 보고도
무엇을 구하는지 알 수 있습니다.
이렇게 함수를 만드는 것을 함수 정의라고 하고, 함수 이름을 써서 실행하는 것을 호출(call)이라고 합니다.
| 부분 | 예시 | 의미 |
|---|---|---|
| 반환형 | double |
함수가 결과로 돌려줄 값의 자료형. 결과를 돌려주는 것을 반환(return)이라고 합니다. 반환할 값이 없으면 void입니다. |
| 함수 이름 | area |
호출할 때 쓰는 이름. 무슨 일을 하는지 드러나게 짓습니다. |
| 매개변수 | double r |
호출할 때 전달받은 값을 저장할 변수. 함수가 일을 하는 데 필요한 값을 밖에서 받습니다. |
| 중괄호 안 | return …; |
실제로 하는 일. return이 값을 호출한 곳으로 반환합니다. |
area(2.0)은 결국 값 하나입니다
printf("%.2f\n", area(2.0))에서는 area(2.0)이 먼저 계산됩니다.
함수가 실행되다가 return이 값을 반환하면, 호출한 곳에 그 값을 쓴 것처럼 실행이 이어집니다.
area(2.0)이 반환하는 값은 12.56636이므로 printf("%.2f\n", 12.56636)이라고 쓴 것과 같아지고, 화면에는 12.57이 출력됩니다.
그래서 함수의 반환값은 변수에 저장할 수도 있고 다른 계산에 쓸 수도 있습니다. 값을 쓸 수 있는 곳이라면 어디에든 함수 호출을 쓸 수 있습니다.
main도 함수였습니다
int main(void)를 다시 보면 방금 배운 세 부분이 그대로 있습니다 —
반환형은 int, 함수 이름은 main이고, 매개변수는 없습니다.
괄호 안의 void가 「매개변수가 없다」는 뜻이었던 것입니다.
끝의 return 0;도 이제 이해할 수 있습니다.
「정상적으로 끝났다」는 뜻의 0을 프로그램을 실행한 운영 체제에 반환하는 것입니다.
지금까지 의미를 모르고 쓰던 두 줄이 함수의 규칙을 따르고 있었습니다.
한 가지 지킬 것이 있습니다. 함수를 호출하기 전에 「이런 함수가 있다」고 알려 두어야 합니다.
area를 main 위에 정의한 것이 그래서입니다.
아래에 정의하고 싶다면 위에 함수의 첫 줄만 한 번 더 써서 미리 알려 둡니다.
이 줄을 함수 원형(function prototype)이라고 합니다.
#include <stdio.h>
int square(int n); // 함수 원형: 「이런 함수가 있다」고 미리 알려 둔다
int main(void) {
printf("%d\n", square(6)); // 36
return 0;
}
int square(int n) { // 함수 정의는 아래에 쓴다
return n * n;
}두 방법 다 맞습니다. 함수가 몇 개 안 되면 위에 정의하는 것이 간단하고, 여러 함수가 서로를 호출하기 시작하면 함수 원형만 위에 모아 두는 편이 깔끔합니다. 함수 원형은 중괄호 없이 세미콜론으로 끝냅니다.
스스로 확인
섭씨를 화씨로 바꾸는 함수를 만든다면 반환형·함수 이름·매개변수에 각각 무엇을 써야 할까요?
매개변수와 반환
매개변수도 반환값도 없을 수 있습니다.
#include <stdio.h>
int bigger(int a, int b) { // 매개변수가 둘이면 쉼표로 구분한다
if (a > b) {
return a; // return을 만나면 「바로」 함수가 끝난다
}
return b;
}
int main(void) {
printf("%d\n", bigger(7, 3)); // 7
printf("%d\n", bigger(2, 9)); // 9
return 0;
}매개변수가 여럿이면 쉼표로 구분합니다
매개변수마다 자료형을 따로 써야 합니다.
int a, b가 아니라 int a, int b입니다.
호출할 때는 쓴 순서대로 값이 전달됩니다 —
bigger(7, 3)이면 a에 7, b에 3이 저장됩니다.
함수를 정의할 때 쓴 a·b를 매개변수(parameter),
호출할 때 실제로 넣은 7·3을 인자(argument)라고 합니다.
호출하면 인자의 값이 매개변수에 차례로 저장됩니다.
return을 만나면 함수가 바로 끝납니다
bigger에는 else가 없습니다. 없어도 되는 이유는
return a;를 만나면 아래 줄로 내려가지 않고 함수가 끝나기 때문입니다.
아래 return b;까지 실행되었다는 것은 조건이 거짓이었다는 뜻입니다.
그래서 return은 함수 안에 여러 개 있어도 됩니다.
다만 한 번 호출할 때 실제로 반환되는 값은 하나뿐입니다 —
처음 만나는 return에서 함수가 끝나기 때문입니다.
반환할 값이 없는 함수도 있습니다. 화면에 출력만 하고 끝나는 함수가 그렇습니다.
이때는 반환형으로 void를 씁니다.
#include <stdio.h>
void star(int n) { // 매개변수는 있지만 반환값은 없다
for (int i = 0; i < n; i = i + 1) {
printf("*");
}
printf("\n");
}
void line(void) { // 매개변수도 반환값도 없다
printf("--------\n");
}
int main(void) {
line();
star(3);
star(5);
line();
return 0;
}| 함수의 첫 줄 | 매개변수 | 반환값 |
|---|---|---|
double area(double r) |
있다 | 있다 |
void star(int n) |
있다 | 없다 |
void line(void) |
없다 | 없다 |
같은 void가 두 곳에 쓰입니다
앞의 void는 「반환값이 없다」,
괄호 안의 void는 「매개변수가 없다」는 뜻입니다.
void line(void)는 둘 다인 경우입니다.
같은 단어라도 쓰인 위치에 따라 의미가 다르니 어디에 있는지를 봅니다.
반환값이 없는 함수는 값이 필요한 곳에 쓸 수 없습니다.
printf("%d", line())처럼 쓸 수 없고, line();처럼 문장 하나로 호출합니다.
스스로 확인
bigger로 세 수 가운데 가장 큰 값을 구하고 싶다면 어떻게 하겠습니까? 함수를 고치는 방법과 두 번 호출하는 방법을 모두 생각해 보세요.
전달되는 것은 사본
함수 안에서 바꾼 값은 밖에도 남을까요?
함수에 변수를 인자로 전달했습니다. 함수 안에서 그 값을 바꿨습니다. 호출한 쪽의 변수는 어떻게 될까요?
#include <stdio.h>
void change(int n) { // 여기 n은 「사본」이다
n = 100;
printf("함수 안: %d\n", n); // 100
}
int main(void) {
int n = 7; // 여기 n과 위의 n은 서로 다른 변수다
change(n);
printf("함수 밖: %d\n", n); // 7 — 그대로다
return 0;
}안에서는 100인데 밖에서는 7입니다
함수를 호출할 때 전달되는 것은 변수 자체가 아니라 그 변수에 저장된 값의 사본입니다.
change(n)은 7이라는 값만 전달합니다.
함수 안의 n은 그 값을 받아 새로 만든 다른 변수이고,
그것을 100으로 바꿔도 원래 변수는 바뀌지 않습니다.
이처럼 인자의 값을 복사해 전달하는 방식을 값에 의한 전달(call by value)이라고 합니다.
사물함으로 말하면 안에 든 것의 복사본을 건넨 셈이라, 받은 쪽이 복사본을 고쳐 써도 원본은 그대로입니다.
이름이 같아도 다른 변수입니다
위 코드에는 n이 둘 있습니다. 이름이 같은데도 서로 영향을 주지 않았습니다.
함수 안에서 선언한 변수는 그 함수에 속해서, 함수가 끝나면 사라지고
다른 함수에서는 보이지도 않습니다. 이런 변수를 지역 변수(local variable)라고 하며, 매개변수도 지역 변수입니다.
덕분에 함수를 만들 때 다른 함수에서 어떤 변수 이름을 썼는지 걱정하지 않아도 됩니다.
i든 sum이든 마음대로 쓸 수 있습니다.
변수 이름이 서로 충돌하지 않는 것이 함수로 나누는 큰 장점 가운데 하나입니다.
이 성질이 문제가 되는 대표적인 예가 두 변수의 값을 맞바꾸는 일입니다.
#include <stdio.h>
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
printf("함수 안: a=%d b=%d\n", a, b); // 2 1 — 안에서는 바뀌었다
}
int main(void) {
int x = 1;
int y = 2;
swap(x, y);
printf("함수 밖: x=%d y=%d\n", x, y); // 1 2 — 밖은 그대로다
return 0;
}함수 안에서는 분명히 바뀌었습니다
swap의 논리에는 잘못이 없습니다. 함수 안에서 출력해 보면 a=2 b=1로 제대로 맞바뀌어 있습니다.
그런데 바뀐 것은 사본이고, 함수가 끝나면 그 사본은 사라집니다.
x와 y의 값은 처음부터 끝까지 그대로입니다.
변수의 값만 전달해서는 함수가 호출한 쪽의 변수를 바꿀 수 없습니다.
반환할 수 있는 값이 하나뿐이니 return으로 두 값을 한꺼번에 반환할 수도 없습니다.
함수 안에서 다른 함수를 호출할 수 있습니다
아래 프로그램에서는 show가 is_prime을 호출합니다.
일을 잘게 나누고 위에서 조합하는 것이 함수를 쓰는 보람입니다.
is_prime은 「소수인가」만 판단하고,
show는 「출력할지 말지」만 정하고,
main은 「어디까지 확인할지」만 다룹니다.
#include <stdio.h>
int is_prime(int n) { // 소수면 1, 아니면 0을 반환한다
if (n < 2) {
return 0;
}
for (int i = 2; i < n; i = i + 1) {
if (n % i == 0) {
return 0; // 나누어떨어지면 바로 끝낸다
}
}
return 1;
}
void show(int n) {
if (is_prime(n)) { // 함수 안에서 다른 함수를 호출할 수 있다
printf("%d ", n);
}
}
int main(void) {
int last;
printf("어디까지 볼까요? ");
scanf("%d", &last);
for (int n = 2; n <= last; n = n + 1) {
show(n);
}
printf("\n");
return 0;
}
is_prime이 1과 0을 반환하고 그 값을 if문이 그대로 조건으로 쓰는 것을 보세요.
「참이면 1, 거짓이면 0」이라는 규칙이 함수의 반환값과 그대로 대응합니다.
스스로 확인
is_prime은 나누어떨어지는 수를 찾을 때까지 n을 2부터 n - 1까지의 수로 차례로 나누어 봅니다. 이 수를 모두 확인할 필요가 없다면 어디까지만 확인하면 될까요?
핵심 정리
일에 이름을 붙인다
코드를 함수로 묶어 두면 고칠 곳이 한 곳이 되고 함수 이름이 하는 일을 알려 준다. 줄 수를 줄이려고만 쓰는 것이 아니다.
세 부분을 갖춘다
반환형 · 함수 이름 · 매개변수를 쓰고 중괄호 안에 할 일을 쓴다.
반환값이나 매개변수가 없으면 void를 쓴다. main도 같은 규칙을 따른다.
호출한 곳으로 값이 반환된다
return은 값을 반환하며 그 즉시 함수를 끝낸다.
한 번 호출할 때 반환되는 값은 하나다.
전달되는 것은 사본
함수 안의 변수는 이름이 같아도 다른 변수(지역 변수)다. 그래서 변수의 값만 전달해서는 호출한 쪽의 변수를 바꿀 수 없다.
확인 퀴즈
배운 내용을 확인해 봅시다.
1. void line(void)에서 두 void는 각각 무슨 뜻인가?
2. int n = 7;을 change(n)으로 전달하고 함수 안에서 n = 100;을 실행했다. 함수가 끝난 뒤 main의 n은?
3. 함수 안에 return이 두 개 있을 때 알맞은 설명은?