함수 포인터란 무엇인가?
이전 글에서 포인터를 이렇게 설명했습니다.
포인터란 객체의 메모리 주소를 저장하는 변수입니다.
여기서 객체란 변수, 배열, 구조체, 클래스, 함수 등 메모리에 저장되는 모든 객체를 말합니다.
이러한 객체 중에서 함수 객체를 가리키는 변수가 함수 포인터입니다.
함수 포인터를 선언하려면 다음과 같습니다.
함수_반환_타입 (*포인터 명)(인자, 인자...);
함수 포인터는 가리키고자 하는 함수의 반환 타입, 인자의 타입을 그대로 따라야 합니다.
여기서 쓰인 *는 연산자가 아니고 포인터임을 표시하기 위한 기호입니다.
다음의 예는 add 함수를 가리키는 함수 포인터의 선언과 초기화 방법을 보여줍니다.
int add( int a, int b ){
return a+b;
}
int (*pFunc1)(int, int) = add; // 함수 포인터
int (*pFunc2)(int, int) = &add; // 주소 연산자 사용
배열명이 배열의 첫 번째 멤버를 가리키는 것과 마찬가지로, 함수명도 함수 객체의 주소를 가리킵니다.
그래서, 주소 연산자 &를 쓰더라도, 가리키는 것이 달라지지 않습니다.
참고로, 함수의 주소를 cout 객체를 통해 출력해 보고 싶은 분은 다음과 같이 하실 수 있습니다.
cout << (void*)add << "\n";
cout << (void*)&add << "\n";
데이터 타입을 강제로 바꾼 방법인데, 이렇게 하는 이유는 cout 객체의 << 연산자를 overloading 하는 과정에서 함수의 주소를 bool 타입으로 변환시켜 1을 출력하기 때문입니다.
그리고, 함수 포인터를 사용하는 것은 함수를 사용하는 것과 같은 방식으로 사용합니다.
선언이 조금 길뿐이지 사용법은 아주 간단합니다.
int add( int a, int b ){
return a+b;
}
int (*pFunc)(int, int) = add; // 함수 포인터
int res1 = pFunc(2,3); // add(2,3) = 5
int res2 = (*pFunc)(2,3); // 역참조 연산자 사용
마지막 줄은 역참조 연산자를 사용해서 함수를 호출할 수 있음을 보여줍니다.
함수 포인터의 typedef와 using 구문
함수 포인터의 typedef 선언 방식은 다음과 같습니다.
typedef 함수_반환_타입 (*별명)(인자, 인자...);
함수 포인터를 선언할 때와 방식이 같습니다. 단지, 함수 포인터 명 자리에 별명이 온다는 것이 다를 뿐이죠.
예전에 작성된 실제 사용되는 코드를 보면, 거의 대부분 typedef 구문을 사용한다는 것을 알게 될 것입니다.
선언이 너무 길어지면 코드가 길어지고, 실수할 가능성도 높아지기 때문이죠.
그리고, modern C++ 에선 typedef의 불편함을 보완하기 위해 using 키워드를 사용합니다.
typedef와 using의 차이점은 여기에 정리해 두었습니다.
using을 이용한 함수 포인터의 별명 선언 방식은 다음과 같습니다.
using 별명 = 함수_반환_타입 (*)(인자, 인자...);
위의 예를, using 구문을 사용하고, 함수를 추가하여 조금 확장해 보도록 하겠습니다.
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;
}
// 타입명 재설정
//typedef int (*myFUNC)(int, int); typedef를 사용했을 때
using myFUNC = int (*)(int, int);
int main(){
myFUNC pFunc1 = add;
myFUNC pFunc2 = subtract;
myFUNC pFunc3 = multiply;
int res1 = pFunc1(2,3); // 5
return 0;
}
함수 포인터의 배열
함수 포인터도 데이터 타입이므로 당연히 배열에 담을 수 있습니다.
함수 포인터의 배열의 선언은 다음과 같습니다.
함수_반환_타입 (*포인터 명[배열 멤버수])(인자, 인자...);
위의 예제에 divide 연산을 추가하고, 함수 포인터의 배열을 사용하는 방법을 보여드리겠습니다.
int divide(int a, int b){
if ( b == 0)
return 0;
else
return a / b;
}
int main(){
int (*pOper[4])(int, int); // 함수 포인터의 배열 선언
pOper[0] = add;
pOper[1] = subtract;
pOper[2] = multiply;
pOper[3] = divide;
int ret1 = pOper[0](3, 4);
int ret2 = (*pOper[3])(9, 3);
return 0;
}
역시, 선언이 복잡해 보일 뿐, 사용하는 것은 그리 복잡하지 않습니다.
그리고, 좀 더 간단하도록 using 구문을 사용하면 다음과 같이 변경할 수 있습니다.
//typedef int (*OPERATION)(int, int); // typedef를 사용할 때
using OPERATION = int (*)(int, int);
int main(){
OPERATION pOper[4] = { add, subtract, multiply, divide}; // 초기화
int ret1 = pOper[0](3, 4);
int ret2 = (*pOper[3])(9, 3);
return 0;
}
참고로, 위의 선언을 아래와 같이 선언할 수도 있습니다.
//typedef int (*OPERATION[4])(int, int); typedef를 사용할 때
using OPERATION = int (*[4])(int, int);
OPERATION pOper = { add, subtract, multiply, divide}; // 초기화
함수 포인터를 쓰는 이유
함수 포인터를 사용하는 이유로 여러 가지를 말할 수도 있지만, 한 마디로 하면 호출된 함수에게 함수를 전달하기 위해서입니다.
그리고, 호출된 함수는 함수 내에서 작업 중 또는 작업 후에 전달받은 함수를 호출해서 자신의 목적을 달성할 수 있습니다. 이렇게 호출한 함수 내에서 다시 호출되는 함수를 콜백(Callback) 함수라고 합니다.
이러한 방식은 함수를 모듈화 하고 재사용하기 수월하게 만듭니다.
또한, 전달하는 함수를 바꿈으로써 실시간으로 동적인 변화를 가져올 수 있습니다.
무슨 말인가 하는 사람도 아마 사용해 본 일이 있을 정도로 C언어의 강력하고 널리 사용되는 방식입니다.
예를 들어보겠습니다.
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;
}
//typedef int (*OPERATION)(int, int);
using OPERATION = int (*)(int, int);
void printResult( int a, int b, OPERATION oper){
int ret = oper(a,b);
cout << "operation result: " << ret << "\n";
}
int main() {
OPERATION oper = NULL; // 초기화
int num, a, b;
cin >> num >> a >> b;
switch(num){
case 1:
oper = add; break;
case 2:
oper = subtract; break;
case 3:
oper = multiply; break;
}
if ( oper){
printResult(a, b, oper); // 함수 포인터를 인자로 전달
}
return 0;
}
위의 함수는 사용자로부터 연산의 종류와 숫자를 받아 연산 결과를 출력하는 코드입니다.
아주 간단한 코드지만, 위의 코드는 강점을 갖고 있습니다.
우선, 나눗셈을 추가시키고 싶으면 나눗셈 함수를 추가하고 main함수의 switch 문을 수정하면 끝입니다.
printResult 함수를 건드릴 필요가 없죠.
게다가, printResult 함수는 더 이상 연산에 관여할 필요가 없기 때문에 출력 방식에만 집중할 수 있을 것입니다.
이렇게 함수를 전달하는 것으로 모듈화가 가능합니다.
실제 C++ 라이브러리에서 사용하는 예를 들어보겠습니다.
bool compare_by_ascending( int a, int b){
return a < b;
}
bool compare_by_descending( int a, int b){
return a > b;
}
//typedef bool (*COMPARE)(int, int);
using COMPARE = bool (*)(int, int);
void printResult( int* pStart, int* pEnd, COMPARE comp){
sort( pStart, pEnd, comp); // std::sort 함수
cout << "sort result: ";
int N = pEnd - pStart;
for( int i = 0; i < N; i++){
cout << pStart[i] << " ";
}
}
int main() {
int array[10] = { 2, 3, 4, 6, 1, 5, 7, 9, 8, 10};
int num;
cin >> num;
if ( num == 0){
printResult(array, array+10, compare_by_ascending); // 오름차순
}
else{
printResult(array, array+10, compare_by_descending); // 내림차순
}
return 0;
}
위 예제는 사용자의 입력을 받은 값이 0 이면 오름차순으로, 아니면 내림차순으로 array 배열을 정렬하는 코드입니다.
주목할 부분은 printResult 함수 내에서, std::sort 함수를 호출할 때, 함수 포인터를 넘겨주는 부분입니다.
std::sort 함수도 함수 내에서 다시 사용자 함수를 호출함으로써 다양한 결과를 낼 수 있도록 만들어져 있습니다.
예제처럼 입력받는 것이 아니라, 사용자가 "내림차순 정렬기능"을 선택하거나 반대로 "오름차순 정렬기능"을 선택하기 위해 버튼을 눌렀을 때, 실시간으로 결과를 보여줄 수 있도록 코딩을 할 수도 있을 것입니다.
이 글과 관련된 글들
클래스의 멤버 함수 포인터 (member function pointer)
'C, C++ > C++ 언어' 카테고리의 다른 글
[C++] 클래스의 초기화 리스트 (Initializer List)를 사용하는 이유 (0) | 2024.05.07 |
---|---|
[C++] const 포인터, const 멤버 함수 및 const 클래스 (0) | 2024.05.04 |
[C++] void 포인터의 개념과 사용법 (0) | 2024.05.01 |
[C++] 포인터로 배열에 접근하기 (1) | 2024.04.30 |
[C++] 포인터의 개념 및 사용하는 이유 (0) | 2024.04.29 |