함수 포인터( function pointer )는 함수를 지시하는 포인터( pointer )입니다.
[C++] 함수를 전달하기 위한 함수 포인터( function pointer )
함수 포인터( function pointer )포인터( pointer )란 객체의 메모리 주소를 저장하는 변수입니다.이러한 포인터 중에, 함수의 메모리 주소를 저장하는 포인터를 함수 포인터라고 합니다.int var{ 5 };int* pt
codingbonfire.tistory.com
그리고, 이러한 함수 포인터 중에서 클래스의 멤버 함수를 가리키는 포인터를 멤버 함수 포인터( member function pointer )라고 합니다.
윗글에서, 함수 포인터는 함수 타입과 *
기호를 사용해서 함수 포인터를 정의한다고 했습니다.
int add(int a, int b){ // 일반 함수
return a + b;
}
int main(){
// add 함수의 타입은 int(int,int)
static_assert(
is_same_v< decltype( add ), int(int,int) > == true);
int (*pFunc1)(int, int) = &add; // 일반 함수 포인터
cout << (*pFunc1)(2, 3) << endl; // 5
}
위의 add 함수의 타입은 int(int, int)
입니다.
그리고, 이러한 타입의 함수를 가리키는 함수 포인터는 int(*pFunc1)(int, int)
와 같이 정의할 수 있습니다.
참고로, 위의 decltype(add)
는 add 함수의 타입을 나타냅니다.
그리고, std::is_same_v
는 주어진 두 타입이 같은 타입인지 확인할 수 있는 함수 템플릿입니다.
이와 마찬가지로, 멤버 함수 포인터는 멤버 함수의 타입과 *
기호를 사용해서 정의됩니다.
class CSomething{
int m_nValue{};
public:
CSomething() = default;
CSomething(int val) : m_nValue(val){}
int add(int a, int b){ // 멤버 함수
return m_nValue + a + b;
}
};
int add(int a, int b){ // 일반 함수
return a + b;
}
int main(){
// CSomething::add 함수의 타입
static_assert(
is_same_v< decltype( CSomething::add ), int( CSomething::*)(int,int) > == true);
// 멤버 함수 포인터의 정의
int (CSomething::*pFunc2)(int, int) = CSomething::add;
CSomething c1(10);
cout << (c1.*pFunc2)(2, 3) << endl; // 5
}
위에서 보는 것처럼, add 멤버 함수의 타입은 int( CSomething::* )(int, int)
입니다.
따라서, 이 멤버 함수를 가리키는 멤버 함수 포인터는 다음과 같습니다.
// 비-정적 멤버 함수 포인터의 정의
int (CSomething::*pFunc2)(int, int) = &CSomething::add;
// & 연산자를 사용하지 않아도 괜찮음
int (CSomething::*pFunc3)(int, int) = CSomething::add;
그리고, 이 멤버 함수 포인터가 가리키는 add 함수는 일반 함수가 아니라, 클래스의 멤버 함수이기 때문에, 클래스명 CSomething
과 범위 지정 연산자( scope resolution operator ) ::
를 사용해서 add 함수가 멤버 함수임을 명시해야 합니다.
위의 첫 번째 문장에선 멤버 함수 포인터를 초기화하기 위해, 주소 연산자 &
를 사용하여 멤버 함수의 주소를 구했지만, &
연산자를 사용하지 않더라도 컴파일러는 함수명을 암시적으로 함수의 주소로 변환하기 때문에 오류가 되지 않습니다.
역참조 연산자 *를 통한 멤버 함수 호출
함수 포인터에 역참조 연산자 *
를 사용해, 가리키고 있는 함수를 호출하는 것과 마찬가지로, 멤버 함수 포인터에 *
연산자를 사용해 멤버 함수를 호출할 수 있습니다.
그렇지만, 비-정적 멤버 함수( non-static member function )는 클래스 객체를 통해서만 호출할 수 있다는 점이, 일반 함수 포인터를 사용할 때와 다른 부분입니다.
int main(){
int (*pFunc1)(int, int) = add; // 일반 함수 포인터
cout << (*pFunc1)(2, 3) << endl; // 5
// 비-정적 멤버 함수 포인터
int (CSomething::*pFunc2)(int, int) = CSomething::add;
CSomething c1(10); // 클래스 객체
cout << (c1.*pFunc2)(2, 3) << endl; // 5
}
그리고, 함수 포인터의 경우에는 역참조 연산자를 사용하지 않더라도, 컴파일러가 알아서 함수 포인터가 가리키는 함수를 호출하지만, 멤버 함수 포인터로부터 멤버 함수를 호출할 때는 반드시 역참조 연산자 *
를 사용해야 합니다.
int main(){
int (*pFunc1)(int, int) = add; // 일반 함수 포인터
cout << pFunc1(2, 3) << endl; // 역참조 연산자를 사용하지 않을 수 있음
// 비-정적 멤버 함수 포인터
int (CSomething::*pFunc2)(int, int) = CSomething::add;
CSomething c1(10); // 클래스 객체
cout << (c1.pFunc2)(2, 3) << endl; // error!. 역참조 연산자를 반드시 사용해야 됨
}
▼출력
error: 'class CSomething' has no member named 'pFunc2'
멤버 함수 포인터에 역참조 연산자를 사용하지 않으면, 컴파일러는 위의 pFunc2
를 클래스 CSomething
의 멤버 함수로 오해해서, 존재하지 않는 함수를 호출한 것에 대한 오류를 발생시키기 때문입니다.
그리고, 객체를 명시해야 멤버 함수 포인터를 통해서 그 객체의 멤버 함수에 접근할 수 있으므로, 이러한 멤버 함수 포인터를 클래스 내부에서 사용하려면, this
포인터를 사용해야 합니다.
class CSomething{
int m_nValue;
public:
CSomething(int val) : m_nValue(val){}
int add(int a, int b){ // 멤버 함수
return m_nValue + a + b;
}
int add2(int a, int b){
// 멤버 함수 포인터 정의
// 클래스 내의 함수이므로 add 앞에 something:: 을 붙일 필요없습니다.
int (CSomething::*pFunc)(int, int) = add;
// this 객체를 명시
return (this->*pFunc)(a,b);
}
};
this
는 포인터( pointer )이므로, 위의 (this->*pFunc)(a, b)
대신, this
에 역참조 연산자를 사용해서, (*this.*pFunc)(a, b)
형태로 클래스의 멤버 함수를 호출할 수도 있습니다.
참고로, this
포인터는 비-정적 멤버 함수가 호출될 때, 컴파일러가 전달해 주는 객체의 포인터입니다.
따라서, 정적 멤버 함수가 호출될 때는 매개변수를 통해서 객체를 전달받지 않으면, 멤버 함수 포인터를 사용할 수 없습니다.
멤버 함수 포인터를 사용한 예
당연히 멤버 함수의 주소를 저장하는 멤버 함수 포인터 또한 클래스 멤버가 될 수 있습니다.
이러한 멤버 함수 포인터는 주로 멤버 함수에 다른 멤버 함수를 전달하거나, 다음에 실행될 것으로 예상되는 멤버 함수의 주소를 저장했다가, 필요한 경우 그 멤버 함수를 호출하는 용도로 사용됩니다.
아래의 예문은, 설정된 기능을 멤버 함수 포인터에 저장했다가, doAction 함수를 호출하면 그 설정된 기능을 실행하는 클래스를 보여줍니다.
enum Action{ // 범위없는 enum
act_play,
act_pause,
act_stop,
};
class CAction{
private:
bool play(){ std::cout << "playing...\n"; }
bool pause(){ std::cout << "pause...\n"; }
bool stop(){ std::cout << "stopped...\n"; }
//bool (*m_funcPtr)(); // 함수 포인터
bool (CAction::*m_funcPtr)(); // 멤버 함수 포인터
void setFuncPtr( bool (CAction::*ptr)() ){ // 멤버 함수의 전달
m_funcPtr = ptr;
}
public:
CAction() : m_funcPtr(stop){} // 멤버 초기화 리스트
void setAction( Action ac){
switch(ac){
case act_play: setFuncPtr(play); break;
case act_pause: setFuncPtr(pause); break;
case act_stop: setFuncPtr(stop); break;
}
}
bool doAction(){
return (this->*m_funcPtr)();
}
};
int main(){
CAction action;
action.setAction( act_play);
action.doAction();
}
위에서 주의할 점은, 멤버 함수 포인터 m_funcPtr
의 타입이 bool (*)()
이 아니라 bool ( CAction::*)()
이라는 것입니다.
이것은 ( 눈에는 그렇게 보이지 않지만 ) 멤버 함수 play의 타입이 bool ( CAction::*)()
이기 때문입니다.
위 예문에서, 멤버 함수 포인터 타입의 별명( alias )을 만들면, 좀 더 읽기 쉬운 코드를 작성할 수 있습니다.
using FuncPtr = bool (CAction::*)(); // 멤버 함수 포인터의 별명
FuncPtr m_funcPtr; // 멤버 함수 포인터 정의
void setFuncPtr( FuncPtr ptr ){
m_funcPtr = ptr;
}
정적 멤버 함수 포인터( static member function pointer )
정적 멤버 함수는 클래스 내부에서 정의된다는 것을 제외하면, 일반 함수와 차이가 없습니다.
class CSomething{
public:
static int add(int a, int b){ // 정적 멤버 함수
cout << "static function called\n";
return a + b;
}
};
int add(int a, int b){ // 일반 함수
return a + b;
}
int main(){
static_assert(
is_same_v< decltype( add ), int(int,int) > == true);
static_assert(
is_same_v< decltype( CSomething::add ), int(int,int) > == true);
}
일반 함수 add와 정적 멤버 함수 CSomething::add
의 타입은 int (int, int)
로 동일합니다.
그렇기 때문에, 정적 멤버 함수 포인터가 클래스의 정적 멤버 함수를 가리킨다는 점을 제외하면 일반 함수 포인터와 마찬가지로 정의됩니다.
int main(){
int (*pFunc1)(int, int) = add; // 일반 함수 포인터
cout << pFunc1(2, 3) << endl; // 5
// 정적 멤버 함수 포인터
int (*pFunc2)(int, int) = CSomething::add;
cout << pFunc2(2, 3) << endl; // 5
}
위의 일반 함수 포인터 pFunc1
과 정적 멤버 함수 포인터 pFunc2
는 초기화 표현식만 다를 뿐, 선언하는 방법이나 사용법은 완전히 동일합니다.
정리
- 멤버 함수 포인터는 멤버 함수의 타입과 * 기호로 정의되는 함수 포인터입니다.
- 멤버 함수 포인터에 역참조 연산자를 사용하여 클래스 객체의 멤버 함수에 접근할 수 있습니다.
- 비-정적 멤버 함수 포인터가 가리키는 함수에 접근하려면, 클래스 객체를 통해야 합니다.
- 정적 멤버 함수 포인터는 일반 함수 포인터와 차이가 없습니다.
이 글과 관련 있는 글들
클래스 생성자의 멤버 초기화 리스트( member initializer list )
몇 가지 값만 가질 수 있는 타입: unscoped enum