함수(function)는 이름을 붙여 둔 코드 묶음이고, 부르면 받은 값으로 일한 뒤 return으로 값 하나를 부른 자리에 돌려줍니다. 인자를 넘기는 방법은 값을 복사해 넘기는 값 전달(call by value)과, 변수의 자리를 넘겨 함수가 그 변수를 바꾸게 하는 참조 전달(call by reference)로 나뉩니다. 실행이 함수로 갔다가 부른 줄로 돌아오는 차례와 무엇이 복사되는지를 놓치면 출력을 틀립니다. 아래 그림은 함수를 부르면 칸 묶음이 쌓이고, 주소를 넘긴 칸만 바뀌며, 돌려준 값이 부른 자리로 가는 모습입니다.
1함수를 만들고 부르기
그림의 코드에 결과를 찍는 12번 줄을 더했습니다.
#include <stdio.h>
int up(int n, int *p) {
n = n + 1;
*p = *p + 1;
return n * 10;
}
int main(void) {
int a = 1;
int b = 5;
int c = up(a, &b);
printf("%d %d %d\n", a, b, c);
return 0;
}
1 6 20- 3번 줄 맨 앞
int는 돌려줄 값의 자료형입니다. 돌려줄 값이 없으면void라 적습니다. n과p는 매개변수(parameter)로, 부를 때 넘긴 인자(argument)를 받는 칸입니다.- 실행은 main의 첫 줄부터이고, up은 11번 줄에서 부를 때만 실행됩니다.
- 부르면 그 함수의 칸 묶음인 프레임(frame)이 생기고, 인자 값이 매개변수 칸에 복사됩니다.
2값 전달과 참조 전달
값 전달은 인자의 사본을 넘겨, 함수가 매개변수를 바꿔도 부른 쪽 변수는 그대로입니다. 참조 전달은 변수의 자리를 넘겨, 함수가 부른 쪽 변수를 바꿉니다. 자료에 따라 값에 의한 호출·참조에 의한 호출이라고도 부릅니다. C의 인자는 늘 복사되어 넘어갑니다. C에서 참조 전달은 변수의 주소를 값으로 넘기는 것이고, C 과목은 이것을 주소 전달이라 부릅니다.
- 4번 줄의 n은 a의 사본이라, 2가 되어도 a는 1 그대로입니다(값 전달).
&b는 b 칸의 주소이고, 주소를 받는 매개변수는int *p처럼 별을 붙입니다.- 5번 줄의
*p는 p가 가리키는 칸, 곧 main의 b 칸 자체라 b가 6이 됩니다(참조 전달). - 배열을 넘기면 첫 칸의 주소가 넘어가, 함수가 바꾼 칸이 부른 쪽 배열에 남습니다. 포인터는 32단원에서 배웁니다.
3돌려준 값은 부른 자리로
return이 값을 돌려주면 프레임이 걷히고, 실행은 부른 줄로 돌아옵니다.
6번 줄이 돌려준 20이 11번 줄의 up(a, &b) 자리에 들어가 int c = 20;이 됩니다.
printf("%d\n", up(a, &b));처럼 호출을 printf 안에 쓰면, printf가 돌려받은 20을 찍습니다.
함수 이름이 무엇이든 그 자리에 들어가는 것은 돌려준 값입니다.
4지역 변수와 전역 변수
함수 안에서 선언한 변수와 매개변수는 그 함수의 프레임에 든 지역 변수(local variable)입니다. 부를 때 생기고 함수가 끝나면 수명이 끝나며, 이름이 같아도 다른 함수의 칸과 섞이지 않습니다. 함수 밖에 선언한 전역 변수(global variable)는 모든 함수가 함께 쓰는 칸 하나이고, 값을 주지 않으면 0에서 시작합니다. 범위와 수명, static은 32단원에서 더 배웁니다.
예제
두 값을 맞바꾸는 함수 둘
한 파일을 두 덩이로 나눠 보입니다.
#include <stdio.h>
void swap1(int a, int b) {
int t = a;
a = b;
b = t;
}
void swap2(int *a, int *b) {
int t = *a;
*a = *b;
*b = t;
}
int main(void) {
int x = 1;
int y = 2;
swap1(x, y);
printf("%d %d\n", x, y);
swap2(&x, &y);
printf("%d %d\n", x, y);
return 0;
}
무엇이 찍힐까요? 답을 정한 뒤 펼쳐 보세요.
실행 결과 보기
1 2
2 1swap1은 x·y의 사본인 a·b만 맞바꾸고 끝나, 돌아온 뒤 x·y는 그대로입니다(값 전달).
swap2는 x·y의 주소를 받아 *a·*b로 main의 칸을 맞바꿉니다(참조 전달). 맞바꾸기 세 줄은 배열에서 본 꼴입니다.
같은 코드를 Java·Python으로
| C | Java | Python | |
|---|---|---|---|
| 함수 머리 | int up(int n) | static int up(int n) | def up(n): |
| 돌려줄 값이 없으면 | void | void | None을 돌려줌 — print(f(x))가 None을 찍음 |
| 부른 쪽 변수 바꾸기 | 주소를 넘김 | 못 함 | 못 함 |
| 배열·리스트를 넘기면 | 바꾼 칸이 부른 쪽에 남음 | 같음 | 같음 |
시험 포인트
- 필기: 함수가 든 코드의 결과를 고릅니다.
return값은 부른 식의 자리에 들어가고 실행은 부른 줄로 돌아옵니다. 값 전달은 사본이라 부른 쪽 변수가 그대로이고, C에서 주소(&x)나 배열을 넘기면 함수가 부른 쪽 칸을 바꿀 수 있습니다. 지역 변수는 그 함수의 칸, 전역 변수는 모든 함수가 함께 쓰는 칸입니다. - 실기: 출력을 그대로 씁니다(값 전달과 참조 전달 출제 주제).
a = f(x)·printf("%d", f(x))·Python의print(f(x))에 들어가는 것은 함수 이름이 아니라 f가 돌려준 값입니다. 이름은 값 전달(Call by Value)·참조 전달(Call by Reference)로 씁니다.
확인 문제
1. 아래 함수를 x가 1일 때 부릅니다. 부른 뒤 main의 x가 바뀌는 호출을 고르세요.
void f1(int n) {
n = 5;
}
void f2(int *p) {
*p = 5;
}
int f3(int n) {
return n + 5;
}
- ①
f1(x); - ②
f2(&x); - ③
f3(x); - ④
f1(x + 4);
정답
②입니다. f2는 x의 주소를 받아 *p로 main의 x 칸에 5를 넣습니다(참조 전달).
- ① n은 x의 사본이라 n만 5가 되고 x는 1 그대로입니다.
- ③ f3은 6을 돌려주지만 그 값을 x에 담지 않았습니다.
x = f3(x);라야 x가 바뀝니다. - ④ x + 4를 계산한 값 5의 사본을 넘길 뿐입니다.
2. 1절 코드의 11번 줄을 int c = up(b, &a);로 바꿨습니다. 출력을 그대로 쓰세요. 답을 끝까지 쓴 뒤 정답을 펼치세요.
정답
받는 답:
2 5 60
이번에는 b의 값 5가 n에 복사되고, a의 주소가 p에 넘어갑니다. *p로 바뀌는 칸은 a라 a가 2, n은 6이 되어 60을 돌려줍니다. b는 5 그대로입니다.
- 틀리는 꼴
2 6 60: n을 바꾼 4번 줄도 부른 쪽 b를 바꾼다고 봤습니다. n은 b의 사본입니다. - 틀리는 꼴
1 6 20: 바꾸기 전의 결과입니다. 주소가 넘어간 쪽이 바뀝니다.
3. 다음 코드의 호출을 표로 따라갔습니다. (가)~(다)를 채운 뒤 출력을 그대로 쓰세요. 답을 끝까지 쓴 뒤 정답을 펼치세요.
| 부른 곳 | 부른 뒤 total | 돌려준 값 | 그 뒤 r |
|---|---|---|---|
9번 줄 add(3) | 3 | 6 | 6 |
10번 줄 add(4) | (가) | (나) | (다) |
#include <stdio.h>
int total;
int add(int x) {
total = total + x;
return total * 2;
}
int main(void) {
int r = add(3);
r = r + add(4);
printf("%d %d\n", r, total);
return 0;
}
정답
받는 답: (가) 7 · (나) 14 · (다) 20, 출력은 아래와 같습니다.
20 7total은 전역 변수라 0에서 시작하고, 두 호출이 같은 칸에 더합니다. 둘째 호출은 3에 4를 더한 7의 두 배 14를 돌려주고, r은 6 + 14 = 20입니다.
- 틀리는 꼴
14 7: 10번 줄에서 r에 돌려준 값만 담는다고 봤습니다. 앞의 r 6에 더합니다. - 틀리는 꼴
14 4: 부를 때마다 total이 0에서 다시 시작한다고 봤습니다. 전역 칸은 하나뿐이라 값이 남습니다.
4. 다음 Python 코드의 출력을 그대로 쓰세요. [1, 2]는 C의 배열처럼 0번부터 번호로 칸을 꺼내 쓰는 리스트입니다. 답을 끝까지 쓴 뒤 정답을 펼치세요.
def put(a, x):
a[0] = x
return x * 2
def show(n):
print(n)
b = [1, 2]
print(put(b, 5))
print(show(b[0]))
print(b)
정답
받는 답:
10
5
None
[5, 2]print(put(b, 5))는 put이 돌려준 10을 찍습니다. put은 넘겨받은 리스트의 0번 칸을 5로 바꿔, b도 [5, 2]가 됩니다.
show는 5를 찍기만 하고 돌려주는 값이 없어 None을 돌려주고, 바깥 print가 그 None을 찍습니다.
- 틀리는 꼴
[5, 2]로 시작: 함수 이름이 put이라 바뀐 리스트가 찍힌다고 봤습니다. 찍히는 것은 돌려준 값 10입니다. - 틀리는 꼴
None줄이 없음: show 안의 print가 찍은 5를 돌려준 값으로 봤습니다. 찍는 것과 돌려주는 것은 다릅니다. - 틀리는 꼴 마지막 줄
[1, 2]: 리스트도 사본이 넘어간다고 봤습니다. 함수가 바꾼 칸은 b에 남습니다.
5. 함수에 인자를 넘기는 두 방법 가운데 (가)·(나)에 맞는 이름을 한글 이름과 영문으로 함께 쓰세요. 답을 끝까지 쓴 뒤 정답을 펼치세요.
- (가) 함수가 매개변수를 고쳐도 부른 쪽 변수는 그대로 남습니다. 사본을 받기 때문입니다.
- (나) 함수가 부른 쪽 변수의 자리(C에서는 주소)를 받아 그 변수를 직접 고칩니다.
정답
받는 답: (가) 값 전달(Call by Value) · (나) 참조 전달(Call by Reference)
(가)의 실마리는 '사본을 받아 부른 쪽이 그대로', (나)는 '자리를 받아 부른 쪽을 고친다'입니다. 값에 의한 호출·참조에 의한 호출이라는 이름도 받습니다.
- 같은 이름의 다른 꼴: (나) 주소 전달 — C 과목이 C의 꼴을 부르는 이름입니다. 시험 답은 참조 전달(Call by Reference)로 씁니다.
- 틀리는 꼴 값 전달 — (나)에: C가 주소도 복사해 넘기는 것은 맞지만, 자리(주소)를 넘겨 부른 쪽 변수를 고치는 방식은 참조 전달이라 부릅니다.
처음이라면: C 언어 과목(준비 중)의 '함수 만들고 부르기' · '매개변수는 복사본이다' · '주소를 넘겨 바깥 칸 바꾸기' 레슨에서 같은 규칙을 처음부터 천천히 봅니다.