함수에도 주소가 있다
메모리에 저장되는 것은 데이터만이 아닙니다.
프로그램을 실행하면 변수만 메모리에 올라가는 것이 아닙니다. 「무엇을 하라」는 명령어도 메모리에 올라가야 컴퓨터가 하나씩 읽어 실행합니다. 함수는 그 명령어를 묶어 놓은 것이므로 함수도 메모리 어딘가에 있고, 따라서 주소를 가집니다. 함수의 주소를 저장하는 포인터를 함수 포인터(function pointer)라고 합니다.
배열 이름과 같은 이야기입니다
배열 이름은 식에 쓰이면 첫 요소의 주소로 바뀝니다.
함수 이름도 식에 쓰이면 그 함수의 주소로 바뀝니다.
그래서 &를 붙이지 않고 함수 이름을 그대로 씁니다.
&twice라고 써도 같은 주소를 얻습니다.
닮은 점이 하나 더 있습니다. 함수 이름에도 다른 값을 대입할 수 없습니다.
twice = square;라고 쓸 수 없습니다.
함수 이름은 함수를 나타내는 이름일 뿐 값을 저장하는 변수가 아니기 때문입니다.
주소를 저장해 두고 바꿔 쓰려면 포인터 변수가 따로 있어야 합니다.
#include <stdio.h>
int twice(int n) {
return n * 2;
}
int main(void) {
int (*fp)(int) = twice; // 함수의 주소를 저장하는 포인터. &를 붙이지 않아도 된다
printf("%d\n", twice(10)); // 20
printf("%d\n", fp(10)); // 20 — 같은 함수가 호출된다
if (fp == twice) { // fp에 저장된 주소가 twice의 주소와 같은지 비교한다
printf("같은 곳을 가리킵니다\n");
}
return 0;
}fp(10)은 twice(10)과 똑같이 실행됩니다
fp에 저장된 것은 twice의 주소입니다.
그 주소에 괄호를 붙여 쓰면 「그 주소에 있는 명령어를 실행하라」, 곧 함수 호출이 됩니다.
그래서 결과가 같습니다.
fp == twice가 참인 것도 확인해 두세요.
fp에 저장된 주소와 twice의 주소가 같다는 뜻입니다.
포인터끼리 ==로 비교하면 두 포인터가 같은 곳을 가리키는지 알 수 있습니다.
스스로 확인
fp = square;로 바꾼 뒤 fp(10)을 호출하면 결과는 무엇일까요? twice는 그대로일까요?
선언 읽는 법
처음 보면 선언의 형태가 낯섭니다.
int (*fp)(int);는 지금까지 본 선언과 형태가 다릅니다.
세 부분으로 나누어 읽으면 뜻이 보입니다.
| 부분 | 정하는 것 | 읽는 방법 |
|---|---|---|
(*fp) |
이름과, 포인터라는 것 | fp는 포인터인데 |
(int) |
가리킬 함수의 매개변수 | 정수 하나를 받아서 |
맨 앞 int |
가리킬 함수의 반환형 | 정수를 반환하는 함수를 가리킨다 |
가리킬 함수의 반환형과 매개변수가 자료형을 정합니다
int *가 「정수를 가리키는 포인터」였듯,
함수 포인터의 자료형은 「이런 반환형과 매개변수를 가진 함수를 가리키는 포인터」입니다.
자료형을 이루는 것은 반환형과 매개변수의 자료형입니다.
그러니 반환형이나 매개변수가 다른 함수의 주소는 대입할 수 없습니다.
int (*fp)(int)에 정수 둘을 받는 함수를 대입하려 하면
컴파일러가 경고나 오류를 냅니다. int *의 자료형이 「몇 바이트를 어떻게 읽을지」를 정했듯
함수 포인터의 자료형은 「어떻게 호출할지」를 정합니다.
괄호를 빼면 아예 다른 것이 됩니다
int *fp(int);이라고 쓰면 「정수를 받아 정수를 가리키는 포인터(int *)를 반환하는 함수」를
선언한 것이 됩니다. 변수가 아니라 함수입니다.
그래서 fp = twice;라고 대입하려 하면
함수 이름에는 대입할 수 없으므로 컴파일 오류가 납니다.
괄호는 *를 이름과 먼저 묶으라는 표시입니다.
선언에서 뒤에 붙는 ()는 앞의 *보다 먼저 이름과 묶이므로, 괄호가 없으면 함수 선언으로 읽히기 때문입니다.
식에서 포인터 p를 먼저 역참조한 뒤 인덱스를 붙이려고 (*p)[i]처럼 괄호를 씌우는 것도 같은 이유입니다.
대괄호도 별표보다 먼저 묶여, 괄호가 없으면 *(p[i])로 읽힙니다.
호출할 때는 fp(10)으로 씁니다
(*fp)(10)이라고 쓴 코드도 볼 수 있습니다. 두 방법 모두 같은 함수를 호출합니다.
「역참조해서 호출한다」는 뜻을 드러내려고 앞의 방법을 쓰기도 하는데,
이 강의노트에서는 짧은 쪽으로 통일합니다.
스스로 확인
실수 둘을 받아 실수를 반환하는 함수를 가리키려면 어떻게 선언해야 할까요? 세 부분을 하나씩 채워 보세요.
동작을 전달하기
함수 포인터를 왜 쓰는지가 여기서 드러납니다.
지금까지 함수에 인자로 전달한 것은 처리할 대상, 곧 숫자나 주소였습니다.
함수 포인터를 전달하면 「무엇을 할지」까지 전달할 수 있습니다.
이렇게 다른 함수에 인자로 전달되어 그 함수 안에서 호출되는 함수를 콜백 함수(callback function)라고 합니다.
아래 코드에서는 twice와 square가 apply의 콜백 함수입니다.
#include <stdio.h>
int twice(int n) {
return n * 2;
}
int square(int n) {
return n * n;
}
void apply(int data[], int len, int (*f)(int)) { // 「무엇을 할지」를 함수 포인터로 받는다
for (int i = 0; i < len; i = i + 1) {
printf("%d ", f(data[i]));
}
printf("\n");
}
int main(void) {
int data[5] = {1, 2, 3, 4, 5};
apply(data, 5, twice); // 2 4 6 8 10
apply(data, 5, square); // 1 4 9 16 25
return 0;
}같은 함수가 두 가지 일을 합니다
apply는 「배열을 하나씩 순회하며 출력한다」는 뼈대만 갖고 있습니다.
각 요소에 무엇을 할지는 정해 두지 않았습니다.
그것은 호출한 쪽이 함수 포인터로 전달합니다.
함수 포인터가 없다면 apply_twice와 apply_square를
따로 만들어야 했을 것입니다.
순회하는 부분은 한 글자도 다르지 않은데도 말입니다.
반복되는 코드를 함수로 묶는다는 생각을
한 단계 더 밀고 나간 것입니다.
호출한 쪽에서는 함수 이름만 씁니다
apply(data, 5, twice)에서 twice 뒤에 괄호가 없는 것을 보세요.
twice(…)라고 쓰면 「지금 호출해서 그 반환값을 전달하라」가 되고,
twice라고만 쓰면 「이 함수의 주소를 전달하라」가 됩니다.
괄호 하나가 「호출」과 「주소 전달」을 구분합니다.
스스로 확인
각 요소를 출력하지 않고 f를 적용한 값으로 바꿔 저장하고 싶다면 apply를 어떻게 고쳐야 할까요? 매개변수 data에는 이미 배열의 주소가 전달되어 있습니다.
골라 쓰기
변수로 만들 수 있으면 배열로도 만들 수 있습니다.
함수 포인터도 변수이므로 여러 개를 배열로 만들 수 있습니다. 그러면 인덱스 하나로 「어느 동작을 쓸지」를 고를 수 있습니다.
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int mul(int a, int b) {
return a * b;
}
int main(void) {
int (*op[3])(int, int) = {add, sub, mul}; // 함수의 주소를 저장하는 「배열」
char name[3] = {'+', '-', '*'};
int a = 12;
int b = 5;
for (int i = 0; i < 3; i = i + 1) {
printf("%d %c %d = %d\n", a, name[i], b, op[i](a, b));
}
return 0;
}int (*op[3])(int, int)을 나누어 읽습니다
이름 op에서 시작합니다. op[3]이므로 op는 요소가 세 개인 배열이고,
앞의 *로 보아 각 요소는 포인터이며, 그 포인터가 가리킬 함수는 정수 둘을 받아 정수를 반환합니다.
이름에서 시작해 안에서 밖으로 읽어 나가면 아무리 긴 선언도 풀립니다.
op[i](a, b)는 인덱스가 i인 요소에 저장된 주소로 함수를 호출하는 것입니다.
if문이나 switch문으로 분기하는 대신 인덱스 하나로 고르는 것이라,
동작을 추가할 때 분기문을 고치지 않고 배열에 함수를 더하면 됩니다.
인덱스가 범위를 벗어나면 더 위험합니다
배열의 범위 밖을 읽으면 엉뚱한 값이 나올 수 있습니다. 함수 포인터 배열에서는 그 엉뚱한 값을 주소로 삼아 「실행」하려 듭니다. 인덱스를 사용자 입력처럼 프로그램 밖에서 받는다면 범위를 먼저 검사해야 합니다.
필요할 때만 씁니다
함수 포인터는 코드를 읽는 사람에게서 「지금 어떤 함수가 호출되는지」를 감춥니다.
op[i](a, b)라는 줄만 봐서는 더하는지 곱하는지 알 수 없습니다.
경우가 두세 가지뿐이라면 if문으로 하나씩 쓰는 편이 읽기 쉽습니다.
함수 포인터가 쓸모 있는 때는 「동작을 나중에 정해야 할 때」입니다. 사용자가 동작을 고르게 하거나, 뼈대만 만들어 두고 세부 동작을 나중에 정할 때가 그렇습니다. 쓸 수 있다고 늘 쓰는 것이 낫지는 않습니다.
스스로 확인
나눗셈을 더하려면 어디를 고쳐야 할까요? 0으로 나누는 경우는 어디서 막아야 할까요?
핵심 정리
함수 이름도 주소로 바뀐다
함수의 명령어도 메모리에 저장되므로 함수에도 주소가 있다.
함수 이름은 식에서 그 주소로 바뀌므로 & 없이 그대로 쓰고,
배열 이름처럼 다른 값을 대입할 수 없다.
반환형과 매개변수가 자료형을 정한다
반환형과 매개변수의 자료형이 「어떤 함수를 가리킬 수 있는지」를 정한다. 반환형이나 매개변수가 다른 함수의 주소는 대입할 수 없다.
괄호가 뜻을 정한다
int (*fp)(int)는 포인터 변수, int *fp(int)는 함수 선언이다.
인자로 쓸 때도 twice는 함수의 주소, twice()는 호출한 결과다.
동작을 나중에 정한다
뼈대는 한 번만 만들고 무엇을 할지는 호출한 쪽이 콜백 함수로 전달한다. 다만 읽는 사람에게서 「지금 어떤 함수가 호출되는지」를 감추므로 필요할 때만 쓴다.
확인 퀴즈
배운 내용을 확인해 봅시다.
1. int (*fp)(int);와 int *fp(int);의 차이는?
2. apply(data, 5, twice);에서 twice 뒤에 괄호를 붙여 twice()로 쓰면?
3. 함수 포인터를 쓰기에 알맞은 상황은?