함수도 주소를 가지고 있다. 함수는 스택을 하나 새로 만드는 것을 리버싱이나 디버깅을 해봤다면 알 수 있는데, 함수의 주소는 해당하는 메모리 블록의 시작 주소다.
내가 읽고 있는 책에서는 함수 포인터의 활용의 예시로 시간 측정 함수를 예로 들고 있다. estimate()라는 함수가 있는데 함수 포인터를 인자로 받아서 상황에 따라 측정 기준을 바꿀 수 있다는 것이다.
함수 포인터를 사용하기 위해서는 다음과 같은 절차를 거치면 된다.
- 함수의 주소 얻기
- 함수를 지시하는 포인터 얻기
- 함수를 지시하는 포인터를 사용하여 그 함수를 호출하기
1. 함수의 주소 얻기
함수의 주소는 함수의 이름이다. 따라서 함수 포인터를 인자로 취하는 foo() 함수가 있다면 다음과 같이 호출할 수 있다.
foo(this_is_function);
2. 함수를 지시하는 포인터 얻기
우리가 어떤 데이터 형을 가리키는 포인터를 만들 때 이것이 어떤 데이터 형을 지시하는지 명시하듯이 함수 포인터도 함수의 시그니처를 명시해줘야 한다.
double foo(int); // 다음과 같은 함수가 있다면 double (*pf)(int); // 이것이 함수 포인터이다.
괄호가 꼭 있어야 한다. 괄호를 제외한 것은 double* 형을 반환하는 함수가 된다.
double foo(int);
int foo2(int);
double (*pf)(int);
pf = foo; // 가능
pf = foo2; // 불가능, 시그니처가 다르기 때문이다.
3. 함수를 지시하는 포인터를 사용하여 그 함수를 호출하기
호출하는 방법은 2가지가 있다.
double a = (*pf)(3);
double b = pf(3);
“pf와 (*pf)가 어떻게 동등하냐”라고 질문을 할 수 있는데 이게 역사적인 문제 때문에 둘 다 쓰이는 것이라고 한다. 양 측 입장은 다음과 같다.
첫 번째, pf는 함수 포인터이고 *pf가 함수니까 (*pf)(3)라고 하는 것이 맞다.
두 번째, 함수 이름이 그 함수를 지시하는 포인터이므로 그 함수를 지시하는 포인터도 함수 이름처럼 행동해야 한다. 따라서 pf(3)로 쓰여야 한다.
‘논리냐, 역사냐’인데 어차피 둘 다 가능한 거 불만 안 가지고 쓰면 되겠다.
변형
사실 함수 포인터 자체는 어렵지 않은데 원래 포인터가 복잡하게 사용되면 혼란스러우니까 다음 코드를 잘 이해하는 게 중요하다고 생각한다.
#include <iostream>
const double * f1(const double ar[], int n);
const double * f2(const double[], int);
const double * f3(const double *, int);
int main()
{
using namespace std;
double av[3] = { 1112.3, 1542.6, 2227.9 };
const double *(*p1) (const double *, int) = f1;
auto p2 = f2;
cout << "함수 포인터:\n";
cout << "주소 값\n";
cout << (*p1)(av, 3) << ": " << *(*p1)(av, 3) << endl;
cout << p2(av, 3) << ": " << *p2(av, 3) << endl;
const double *(*pa[3]) (const double*, int) = { f1, f2, f3 };
auto pb = pa;
// const double *(**pb) (const double *, int) = pa;
cout << "\n함수 포인터를 원소로 가지는 배열:\n";
cout << "주소 값\n";
for (int i = 0; i < 3; ++i)
cout << pa[i](av, 3) << ": " << *pa[i](av, 3) << endl;
cout << "\n함수 포인터를 가리키는 포인터:\n";
cout << "주소 값\n";
for (int i = 0; i < 3; ++i)
cout << pb[i](av, 3) << ": " << *pb[i](av, 3) << endl;
cout << "\n포인터를 원소로 가지는 배열을 가리키는 포인터:\n";
cout << "주소 값\n";
auto pc = &pa;
// const double* (*(*pc)[3]) (const double *, int) = &pa;
cout << (*pc)[0](av, 3) << ": " << *(*pc)[0](av, 3) << endl;
const double* (*(*pd)[3])(const double*, int) = &pa;
const double * pdb = (*pd)[1](av, 3);
cout << pdb << ": " << *pdb << endl;
cout << (*(*pd)[2])(av, 3) << ": " << *(*(*pd)[2]) (av, 3) << endl;
return 0;
}
const double * f1(const double ar[], int n)
{
return ar;
}
const double * f2(const double ar[], int n)
{
return ar + 1;
}
const double * f3(const double* ar, int n)
{
return ar + 2;
}
개인적으로 마지막 ‘포인터를 원소로 가지는 배열을 가리키는 포인터’ 부분이 이해하기 어려웠다. 내가 배열 포인터에 대한 이해나 경험이 부족해서 그런 것 같다. 배열 포인터 부분만 따로 놓고 설명하겠다.
const double* (*(*pd)[3])(const double*, int) = &pa;
const double * pdb = (*pd)[1](av, 3);
cout << pdb << ": " << *pdb << endl;
cout << (*(*pd)[2])(av, 3) << ": " << *(*(*pd)[2]) (av, 3) << endl;
pd는 배열 전체를 가리키는 포인터다. *pd는 배열이기 때문에 (*pd)[1](av, 3)과 같이 사용 가능한 것이다.
(*(*pd)[1])(av, 3) = (*pd)[1](av, 3)이라는데 이건 왜 그런지는 모르겠다.
그렇지만 솔직히 저렇게 복잡한 타입을 적기에는 실수할 여지도 많고 안 좋은 게 사실이다. 그래서 C++ 11에서 지원하게 된 것이 auto다. python처럼 타입이 런타임에서 바뀌는 것은 아니고 컴파일 타임에서 타입을 결정해주는 것이고 ‘이터레이터’나 지금과 같은 더러운 타입을 일일이 치는 수고를 덜어줄 수 있는 기능이라고 보면 되겠다.
알아둬야 할 것은 auto는 단일 값을 초기화할 때 사용할 수 있다는 점이다.
따라서 다음과 같은 문장은 auto를 적용할 수 없다.
const double *(*pa[3]) (const double*, int) = { f1, f2, f3 };
typedef라는 키워드도 있다. 타입 명을 가명으로 쓸 수 있게 해 준다. 그래서 함수 포인터처럼 긴 타입 명도 축소시킬 수 있다.
typedef const double* (*plz) (const double *, int);
plz pa[3] = {f1, f2, f3};
plz (*pd)[3] = &pa;
'개발' 카테고리의 다른 글
float4에서 w의 의미(feat. 동차좌표계) (0) | 2019.05.07 |
---|---|
decltype (0) | 2019.05.05 |
함수 매개변수 작성 시 주의점 (0) | 2019.05.05 |
2의 2000승 출력하기(C/C++) (0) | 2018.05.15 |
비선형 구조의 탐색 (0) | 2018.05.08 |
거듭제곱 빠르게 계산하기 (0) | 2018.05.07 |
댓글