C++ STL (뇌를 자극하는) - 2장. 함수 포인터
※ 제가 개인적으로 공부하는 것이라 요약하거나 책에서 빠진 내용이 있을 수 있습니다 ※
Section 1. 함수 포인터란
변수 : 값을 저장하는 메모리 공간의 이름
포인터 : 주소를 저장하는 메모리 공간의 이름
함수 포인터 : 함수의 시작 주소를 저장하는 포인터
함수 포인터는 함수 시그니처(반환 타입과 매개변수 리스트)와 같게 선언한다.
예를 들어, int func(int a, int b)인 함수의 함수 포인터는 (*pf)(int, int)로 선언.
함수 포인터의 선언과 사용 예제
void Print(int n)
{
cout << "정수: " << n << '\n';
}
int main()
{
void (*pf)(int); // void Print(int n)의 함수 포인터 선언
pf = Print; // 함수의 이름은 함수의 시작 주소
Print(10); // 1. 함수 호출
pf(10); // 2. 포인터를 이용한 함수 호출 1
(*pf)(10); // 3. 포인터를 이용한 함수 호출 2
// 위 세 개는 정수: 10 출력
cout << Print << '\n';
cout << pf << '\n';
cout << *pf << '\n;
// 위 세 개는 00CD1154 같은 함수 시작 주소 출력
}
Print, pf, *pf의 출력 모두 같은데 Print가 함수의 시작 주소이고, pf도 함수의 시작 주소이고, 메모리 접근 연산자(*)를 pf에 붙여도 마찬가지로 함수의 주소여서 다 같은 결과가 나온다.
Section 2. 함수 포인터의 종류
C++에서 함수는 1. 정적 함수, 2. 멤버 함수로 나눌 수 있다. 정적 함수는 전역 함수, namespace 내의 전역 함수, static 멤버 함수가 해당. 멤버 함수는 다시 객체와 주소로 각각 호출할 수 있으므로 총 함수 호출 방법은 세 가지다.
1. 정적 함수 호출 (정적 함수)
2. 객체로 멤버 함수 호출 (멤버 함수)
3. 객체의 주소로 멤버 함수 호출 (멤버 함수)
void Print()
{
cout << "정적 함수 Print()" << '\n';
}
class Point
{
public:
void Print()
{
cout << "멤버 함수 Print()" << '\n';
}
};
int main()
{
Point pt;
Point *p = &pt;
Print(); // 정적 함수 호출
pt.Print(); // 객체로 멤버 함수 호출
p->Print(); // 주소로 멤버 함수 호출
return 0;
}
정적 함수 호출
정적 함수 포인터 예제
void Print(int n)
{
cout << "전역 함수: " << n << '\n';
}
namespace A
{
void Print(int n)
{
cout << "namespace A 전역 함수: " << n << '\n';
}
}
class Point
{
public:
static void Print(int n)
{
cout << "Point 클래스의 정적 멤버 함수: " << n << '\n';
}
}
int main()
{
void (*pf)(int); // 정적 함수 포인터 선언
Print(10); // 1. namespace 없는 전역 함수 호출
A::Print(10); // 2. namespace A의 전역 함수 호출
Point::Print(10); // 3. Point 클래스의 정적 멤버 함수 호출
pf = Print;
pf = A::Print;
pf = Point::Point; // 다 됨, pf(10)하면 다 10 출력
}
함수 호출 규약
함수 호출 규약은 함수 호출 시 전달되는 인자의 순서나 함수가 종료될 때 함수의 스택을 정리하는 시점 등을 약속한 것.
stdcall, cdecl, thiscall, fastcall 등이 존재. C++은 정적 함수 기본 함수 호출 규약은 cdecl, 멤버 함수는 thiscall. 그래서 정적 함수 포인터 (모두 edecl 규약 따름)와 멤버 함수 포인터를 각기 다르게 선언.
객체와 주소로 멤버 함수 호출
시그니처가 void Point::Print(int n)인 멤버 함수의 포인터는 void (Point::*pf)(int)처럼 선언.
함수 호출은 멤버 함수 호출 방법에 따라 다르다.
- * 객체로 멤버 함수 호출 시에는 .* 연산자 사용. 예를 들어 (객체.*pf)(10))
- * 주소로 멤버 함수 호출 시에는 ->* 연산자 이용. 예를 들어 (주소->*pf)(10))
또한, 함수 호출 시 연산자 우선순위로 인해 객체와 .*, ->* 연산자 사이에 () 연산자를 사용해야 한다. 예제는 패스.
Section 3. 클라이언트 코드와 서버 코드
때때로 서버가 클라이언트를 호출해야 하는 경우도 있다. 이처럼 클라가 서버를 호출하면 콜(call)이라 하고 서버가 클라를 호출하면 콜백(callback)이라 한다.
콜백 메커니즘을 이용하면 알고리즘 정책을 클라이언트에서 유연하게 바꾸게 서버를 더욱 추상화가 가능. 또한, 대부분 GUI의 강력한 이벤트 기능도 콜백 메커니즘으로 구현. STL의 많은 알고리즘도 콜백을 이용해 클라이언트 정책을 반영. 윈도의 모든 프로시저(procedure)는 시스템이 호출하는 '콜백 함수'다.
서버가 미리 Client() 함수를 알고 호출하는 것은 불가능. 따라서 콜백 메커니즘을 구현하려면 클라이언트가 서버를 호출할 때 서버에 클라의 정보를 제공해야 함. 서버에 클라 정보 제공하는 대표적인 방법은 함수 포인터 매개변수를 이용해 콜백 함수의 주소를 전달하는 방법 (그 외에 함수 객체, 대리자, 전략 패턴 등을 사용).
함수 포인터를 이용한 콜백 메커니즘 구현
#include <algorithm>
using namespace std;
// 클라 //
void Print1(int n)
{
cout << n << ' ';
}
void Print2(int n)
{
cout << n*n << ' ';
}
void Print3(int n)
{
cout << "정수 : " << n << '\n';
}
int main()
{
int arr[5] = {10, 20, 30, 40, 50};
for_each(arr, arr+5, Print1); // Print1() 콜백 함수
cout << '\n' << '\n';
for_each(arr, arr+5, Print2); // Print2() 콜백 함수
cout << '\n' << '\n';
for_each(arr, arr+5, Print3); // Print3() 콜백 함수
return 0;
}
/* 출력
10 20 30 40 50
100 400 900 1600 2500
정수 : 10
~
정수 : 50
*/
클라는 서버 함수 for_each를 호출하지만, 세 번의 출력 결과는 클라에 의해 결정. 출력 정책은 클라만이 알고 있는 것.
이것만은 알고 갑시다!
1. 다음은 Print()의 주소를 저장하는 함수 포인터 정의. 빈 칸 채우세요.
void Print(int data) { ... }
- Print() 함수가 정적 함수나 전역 함수라면 함수 포인터 (_)와 같이 선언.
- void (*pf)(int)
- Print() 함수가 MyClass 클래스의 멤버 함수라면 함수 포인터는 (_)와 같이 선언.
- void (MyClass::*pf)(int)
- 함수 포인터 pf가 전역 함수 Print()의 주소라면 (_)처럼 전역 함수를 호출합니다.
- pf(10) 혹은 (*pf)(10)
- 함수 포인터 pf가 MyClass 클래스의 멤버 함수 Print()의 주소고, 객체가 obj라면 (_)처럼 멤버 함수를 호출합니다.
- (obj.*pf)(10)
2. 다음 빈 칸을 채우세요.
서버는 클라이언트의 정책을 반영하려고 서버 측 코드에서 클라이언트 측 함수를 호출합니다. 이때 서버가 호출하는 클라 함수를 가리켜 (_) 함수라 합니다.
- 콜백 함수