함수 포인터( function pointer )
포인터( pointer )란 객체의 메모리 주소를 저장하는 변수입니다.
이러한 포인터 중에, 함수의 메모리 주소를 저장하는 포인터를 함수 포인터라고 합니다.
int var{ 5 };
int* ptr{ &var }; // int 타입 변수에 대한 포인터
위와 같이, 포인터를 선언하려면 포인터가 가리키는 객체의 타입과 *
기호를 사용합니다.
그리고, 함수 포인터를 선언하려면, 함수 포인터가 가리키는 함수의 타입과 *
기호가 필요합니다..
그런데, 여기서 함수의 타입( function type )은 무엇일까요?
함수를 정의하는데 반드시 필요한 것은, 함수의 이름과, 매개변수 타입 리스트, 그리고 반환 타입입니다.
( 매개변수의 이름은 경우에 따라 없을 수도 있습니다. )
이 중에서 함수의 이름을 제외한 나머지를 함수의 타입( function type )이라고 합니다.
int someFunc( int x, int y ); // 함수의 선언
int ( int, int ); // someFunc 함수의 타입
위의 someFunc 함수의 타입은 int (int,int)
이므로, 이 함수에 대한 포인터는 다음과 같이 선언할 수 있습니다.
int ( *funcPtr )( int, int ){ &someFunc }; // 함수 포인터의 정의
int* funcPtr2( int, int ); // 함수의 전방 선언
int ( *funcPtr3 )( int, int ){ someFunc }; // 암시적인 변환
위에서, 함수 포인터 funcPtr
앞의 괄호를 제거하면, 다음 문장처럼 int*
타입을 반환하는 함수의 전방 선언을 뜻하는 것이 되므로 주의해야 됩니다.
그리고, someFunc
앞에 &
연산자를 사용하여, 이 함수의 주소를 얻어서 함수 포인터를 초기화하는 것이 기본이지만, 마지막 문장처럼, 컴파일러는 필요한 경우 함수를 함수 포인터로 암시적으로 변환하기 때문에, &
를 사용하지 않더라도 잘못된 것은 아닙니다.
● 참고로, 함수의 프로토타입( prototype )은 함수의 선언( declaration )과 같은 말입니다.
그리고, 함수의 서명( signature )은 함수의 선언에서 반환 타입을 제외한 것으로, 이 서명에 해당하는 부분만이 함수를 오버로딩( overloading )할 때 고려되는 부분입니다.
int someFunc( int x, int y ); // sumeFunc( int, int ) 함수의 서명
bool someFunc( int x, int y ); // 이 두 함수의 서명은 동일
그래서, 위 두 함수는 다른 함수로 구분되지 않기 때문에, 오버로딩으로 보지 않고, 오류처리 합니다.
또한, 함수 포인터를 const 포인트로 선언하는 것은, 다른 포인터와 마찬가지로, *
뒤에 const
를 붙이면 됩니다.
int* const ptr{ &var }; // const 포인터
int ( *const funcPtr )( int, int ){ &someFunc }; // const 함수 포인터
그리고, 이러한 const 함수 포인터는 함수 포인터가 가리키는 대상 함수를 변경할 수 없는 포인터를 말하며, const 멤버 함수와는 완전히 다른 개념입니다.
[C++] 객체를 변경할 수 없는 const 멤버 함수
const 멤버 함수C++에서는 기본 타입에 const 키워드를 붙여 값을 변경할 수 없는 변수를 정의할 수 있습니다. 그리고, 이러한 변수는 값을 변경할 수 없으므로, 정의할 때 반드시 초기화 과정을 거
codingbonfire.tistory.com
● 참고로, 함수 포인터나 함수 포인터로 암시적으로 변환되는 함수를 std::cout
를 통해서 출력하면, 함수 포인터가 가리키는 함수의 주소를 출력할 것 같지만, C++은 함수 포인터 타입을 출력하는 방법을 모릅니다.
그리고, 이 경우 컴파일러는 함수 포인터 타입을 bool
타입으로 변환해서 출력합니다.
int add( int a, int b){
return a + b;
}
int main(){
int (*fncPtr)(int, int){ add }; // 함수 포인터
std::cout << add << '\n';
std::cout << fncPtr << '\n';
}
그런데, 위 fncPtr
포인터는 nullptr이 아니므로, 출력되는 값은 1
이 됩니다.
C++ 표준 라이브러리는 객체의 주소를 출력하기 위해서, void*
타입에 대해 오버로딩 된 <<
연산자를 제공하고 있기 때문에, 함수 포인터가 가리키는 함수의 주소를 출력하려면, 다음과 같이 함수 포인터의 타입을 변환해야 합니다.
int main(){
int (*fncPtr)(int, int){ add }; // 함수 포인터
std::cout << reinterpret_cast< void* >( add ) << '\n';
std::cout << ( void* )fncPtr << '\n'; // C-style cast
}
▼출력
0x7ff7de4916df
0x7ff7de4916df
함수 포인터의 역참조( dereference )
포인터가 가리키는 객체의 값을 얻거나 변경하기 위해서 역참조 연산자 *
를 사용하는 것과 마찬가지로, 함수 포인터가 가리키는 함수를 호출할 때도, *
연산자를 사용합니다.
int add( int a, int b){
return a + b;
}
int main(){
int (*fncPtr)(int, int){ add };
int val{ (*fncPtr)( 3, 5 ) };
std::cout << val << '\n';
val = fncPtr( 2, 4 ); // 암시적인 역참조
std::cout << val << '\n';
}
위의 함수 포인터 fncPtr
는 add 함수를 가리킵니다.
따라서, fncPtr
에 역참조 연산자를 사용해서, add 함수에 접근할 수 있습니다.
그런데, fncPtr( 2, 4 )
와 같이, 함수 포인터를 대상으로 ()
연산자를 실행하게 되면, 컴파일러는 암시적으로 함수 포인터를 역참조해서 add 함수를 호출하기 때문에, *
연산자를 생략할 수도 있습니다.
함수 포인터의 별명( alias )
포인터를 함수에 전달하는 것과 마찬가지로, 함수 포인터를 함수에 전달하는 것은 당연히 가능합니다.
그런데, 문제는 이 방법에 필요한 코드가 꽤나 익숙하기 힘든 형태라는 것입니다.
아래는 add 함수를 다른 함수에 전달하는 예를 보여줍니다.
int add( int a, int b){
return a + b;
}
int computeOperation( int a, int b, int (*operation)(int,int) ){
return operation( a, b);
}
int main(){
int var{ computeOperation( 2, 3, add ) }; // 함수 포인터를 전달
std::cout << var << '\n';
}
자세히 보면, 위의 computeOperation는 매개변수를 3
개 가진 평범함 함수입니다.
그런데, 얼핏 보면 3
개 이상의 매개변수를 가진 것처럼 보입니다.
이번엔 반대로, add와 같은 타입을 가진 함수를 반환하는 함수의 예를 보겠습니다.
enum operType{ // 기능의 종류
oper_add,
oper_minus,
oper_multiply,
oper_divide,
};
// 함수 포인터를 반환하는 함수
int (*getOperation(operType type))(int, int){
return add;
}
int main(){
int (*oper)(int,int){ getOperation(oper_add)};
std::cout << oper( 3,4) << '\n';
}
위의 getOperation은 기능의 종류에 따라, 함수 포인터를 반환하는 함수입니다.
이러한 함수는 정의하기도 힘들고, 코드를 읽는 것도 힘든데, 사용하는 것조차 간단하지 않습니다.
다행인 것은, C++에서는 using 키워드를 이용해서, 함수 포인트 타입 같은 복잡한 타입에 대해 별명을 만들 수 있는 기능을 제공합니다.
int add( int a, int b){
return a + b;
}
using OperPtr = int (*)(int, int); // 타입의 별명
위의 OpenPtr
은, add와 같은 함수를 가리키는, 함수 포인터 타입의 별명( alias )입니다.
이를 사용해서, 위의 코드들을 변경하면 다음과 같습니다.
int add( int a, int b){
return a + b;
}
using OperPtr = int (*)(int, int); // 타입의 별명
int computeOperation( int a, int b, OperPtr operation ){ // 별명을 사용
return operation( a, b);
}
enum operType{
oper_add,
oper_minus,
oper_multiply,
oper_divide,
};
OperPtr getOperation( operType type ){ // 별명을 사용
return add;
}
int main(){
int var{ computeOperation( 2, 3, add ) };
std::cout << var << '\n';
OperPtr oper { getOperation(oper_add) }; // 별명을 사용
std::cout << oper( 3,4) << '\n';
}
위에서 보듯이, 함수 포인터 타입의 별명을 사용하면, 우선 복잡한 코드( 괄호의 중복, 매개변수의 위치와 개수, 포인터임을 나타내는 기호...)를 작성해야 하는 문제에서 벗어날 수 있을 뿐 아니라, 함수 포인터 타입에 이름을 붙일 수 있게 돼서, 이 타입의 사용 목적을 좀 더 명확하게 전달할 수 있게 됩니다.
함수 포인터의 타입 추론( type deduction )
C++은 auto 키워드를 사용하여, 변수의 초기값으로부터 변수의 타입을 추론할 수 있습니다.
[C++] 자동으로 변수의 타입을 완성하는 auto 키워드
변수의 타입 추론( type deduction )C++ 에는 모든 값에 해당하는 타입이 있습니다.int main(){ 5; // int 3.14; // double 5L; // long 5.0f; // float ?? variable{ 5 }; // variable의 타입 ??}예를 들어, 위의 3.14는 double 타입을
codingbonfire.tistory.com
그리고 다음과 같이, 변수의 하나인 포인터( pointer )의 타입도 당연히 추론할 수 있습니다.
#include <array>
int main(){
std::array<int, 5 > arr{ 1, 2, 3, 4, 5 };
auto arrPtr{ &arr }; // std::array< int, 5 >* 타입
}
위에서는 arrPtr
을 std::array<int,5>*
타입으로 추론합니다.
함수 포인터도 이와 같은 방식을 통해, 기다란 타입을 입력하지 않고도 타입을 완성할 수 있습니다.
#include <array>
auto getSize( const std::array<int, 5 >& arr){
return arr.size();
}
int main(){
std::array<int, 5 > arr{ 1, 2, 3, 4, 5 };
using getSizePtr = std::size_t (*)( const std::array<int, 5>& );
getSizePtr getPtr1{ getSize }; // 함수 포인터 타입의 별명을 이용
auto getPtr2{ getSize }; // 함수 포인터 타입의 추론
std::cout << getPtr2( arr ) << '\n';
}
위의 경우 함수 포인터의 별명을 사용하는 것보다, auto를 사용하여, getSize의 함수 타입을 추론하는 것이 좀 더 편리하다는 것을 볼 수 있습니다.
( 물론, 위의 경우는 함수 포인터의 선언이 힘들다는 것과 auto를 통해서 이러한 부담을 조금 덜 수 있다는 것을 얘기하기 위해서 만든 예입니다. 이런 경우 당연히 arr.size()
함수를 사용하는 것이 제일 좋습니다. )
함수 포인터의 배열
포인터를 배열에 담는 것과 마찬가지로, 함수 포인터 또한 배열에 담을 수 있습니다.
int add(int a, int b){
return a+b;
}
int subtract(int a, int b){
return a-b;
}
int multiply(int a, int b){
return a*b;
}
int divide(int a, int b){
if ( b == 0)
return 0;
else
return a / b;
}
int main() {
int a{}, b{}, c{};
int* arrPtr[3]; // 포인터의 배열
arrPtr[0] = &a;
arrPtr[1] = &b;
arrPtr[2] = &c;
int (*operation[4])(int, int); // 함수 포인터의 배열
operation[0] = &add;
operation[1] = subtract; // &이 없어도 ok
operation[2] = multiply;
operation[3] = divide;
}
그렇지만, 별명을 사용하여 좀 더 알아보기 쉬운 선언을 하는 것이 좋습니다.
enum operType{ // 범위없는 enum
oper_add,
oper_minus,
oper_multiply,
oper_divide,
oper_num,
};
using OPERATION = int (*)(int, int); // 함수 포인터의 별명
int main() {
OPERATION oper[oper_num]; // 함수 포인터의 별명
oper[oper_add] = &add;
oper[oper_minus] = subtract; // &이 없어도 ok
oper[oper_multiply] = multiply;
oper[oper_divide] = divide;
}
혹은, 다음과 같이 4
개의 원소를 가진 배열을 별명으로 만들 수도 있습니다.
using OPERATION4 = int (*[4])(int, int); // 함수 포인터 배열의 별명
int main() {
OPERATION4 oper{ add, subtract, multiply, divide };
std::cout << oper[0]( 3, 4) << '\n';
}
함수 포인터를 사용하는 이유
함수 포인터( function pointer )를 사용하는 가장 큰 장점은 함수에 또 다른 함수를 포인터 형태로 전달할 수 있다는 것입니다.
이렇게 어떤 함수에 전달되는 다른 함수를 콜백( callback ) 함수라고 부르며, 콜백 함수를 전달받은 함수는 함수 내부에서 이 콜백 함수를 다시 호출함으로써 자신의 목적을 달성합니다.
이러한 방식의 접근은 함수들을 모듈화 하고 재사용하기 수월하게 만듭니다.
또한, 함수에 전달되는 콜백 함수를 바꿈으로써, 실시간에 수행되는 기능의 동적인 변화를 가져올 수 있습니다.
아래는 사용자의 입력에 따라, printResult 함수에, 실행하고자 하는 콜백 함수를 전달하는 예문입니다.
enum operType{ // 기능의 종류, unscoped enum
oper_add,
oper_minus,
oper_multiply,
oper_divide,
oper_num,
};
int add(int a, int b){
return a+b;
}
int subtract(int a, int b){
return a-b;
}
int multiply(int a, int b){
return a*b;
}
int divide(int a, int b){
if ( b == 0)
return 0;
else
return a / b;
}
using OPERATION = int (*)(int, int); // 함수 포인터의 별명
void printResult( int a, int b, OPERATION oper){
int ret = oper(a,b);
std::cout << "operation result: " << ret << "\n";
}
int main() {
int num, a, b;
std::cin >> num >> a >> b;
if ( num > oper_num ) return 0;
OPERATION operation[ oper_num ]{ add, subtract, multiply, divide };
printResult(a, b, operation[num] ); // 함수 포인터를 인자로 전달
}
위의 printResult 함수는 함수 포인터의 형태로 전달된 함수를 실행하고, 그 결과를 출력할 뿐, 전달된 함수가 어떤 기능을 수행하는지 알 필요가 없습니다.
이렇게 각 함수의 기능을 작게 나누고 고립시킬수록, 이러한 함수들을 작성하고 재사용하기 쉬워지는데, 이런 것들이 가능하려면 함수의 전달이라는 방법을 통해, 기능의 조립이 가능해야 합니다.
그리고, C++에서는 함수 포인터를 이용해서 함수를 전달할 수 있습니다.
정리
- 함수의 주소를 저장하는 변수를 함수 포인터( function pointer )라고 합니다.
- 함수 포인터는 함수 타입( function type )과 *기호를 사용하여 선언됩니다.
- 함수 포인터의 복잡한 타입은, ( using을 이용한 ) 타입의 별명을 정의하여 사용하기 쉽게 만들 수 있습니다.
- 함수 포인터 사용의 목적은 기능의 전달입니다.
이 글과 관련 있는 글들
몇 가지 값만 가질 수 있는 타입: unscoped enum