본문 바로가기

별걸다하는 IT/프로그래밍언어

[C언어 & 씨플플 강좌] 함수 포인터에 대해 알아보자! 함수 포인터 역참조, 함수 포인터 sizeof 측정?

반응형

[C언어 C++언어 완전정복 목차 링크]

안녕하세요 양햄찌 블로그입니다.

요새 포인터에 대한 글을 쭉 달리고 있네요 ㅎㅎ

포인터를 어려워하는 친구들이 많아 설명해주는 김에 겸사겸사 작성하는 중..

 

그동안 포스팅 했던 것들 중 포인터와 배열에 관한 것들만 한 번 쭉 정리해보자면,

 

1. 배열이란 배열 선언 및 초기화

2. 2차원 배열 및 선언방법 초기화  

2. 포인터란 무엇인가 

3. 배열과 포인터의 상관관계 

4. 포인터 배열과 배열 포인터. 배열포인터와 이차원배열의 관계 

5. 함수포인터 

 

요렇게 되겠습니다.

오늘 들어갈 부분은 위에 적혀있듯이 함수 포인터예요.

함수포인터란 무엇일까!

우리 함수 포인터에 들어가기 전, 함수에 대해 생각해볼까요?!

우리는 아무 생각이 없이 (물론 궁금해했던 분들도 있었겠지만) 함수를 정의한 후, 호출해서 사용했었죠.

컴퓨터가 함수를 호출한다는 것은 함수가 메모리에 존재한다는 얘기겠죠?!

네 어딘가에 저장되어 있기 때문에 컴퓨터가 호출해서 사용하는 것이고, 그렇게 때문에 포인터와 관련될 수 있는 겁니다.

 

[함수 이름의 정체]

#include <iostream>
using std::cout;
using std::endl;
void func()
{
	cout << "hello func~!\n";
}
int main()
{
	cout << "함수 이름 출력 :"<< func << endl;
	return 0;
}

간단하게 함수를 정의한 다음에 함수 이름을 출력해봤어요. 물론 C++의 출력함수인 cout이 아니라 printf("%p", func) 이렇게 printf함수를 사용해서 C스타일로 출력해줘도 돼요.

짠 주소 값이 출력되네요!!

배열이름이 배열의 주소를 가리키는 것처럼 함수 이름 또한 함수의 주소를 가리키는 것을 간단하게 확인해보실 수 있습니다. 물론 배열의 이름과 마찬가지로 함수 이름 또한 상수 형태로 변경할 수 없게 되어있어요.

함수와 함수 포인터

저렇게 함수 이름이 함수 주소를 가리킬 수 있다는건, 결국 함수를 가리키는 포인터가 정의될 수 있다는거.

배열 포인터는 포인터 데이터 타입이 배열인 거였죠~~ 

배열포인터로 배열에 접근할 수 있듯이 함수에 접근할 수 있도록 함수를 가리키는 포인터가 존재합니다.

포인터 변수에 함수 주소값을 저장하여 함수를 사용할 수 잇다는 것~! 고것이 함수 포인터예요.

 

[함수 포인터 선언 문법]

그럼 이제 중요한 부분은, 우리가 특정 함수를 가리키도록 어떻게 포인터를 선언할 것인가. 

함수를 구분하는 타입을 어떻게 정의할 것인가! 가 되겠죠?

int	add(int a, int b);
int	add(int a, int b, int c);
float	add(float a, float b);

자 저 세 개의 함수 add는 같은 함수일까요?

아니죠! 오버로딩 되어서 함수명은 같지만, 실질적으로 나중에 컴파일시 다 다른함수명을 갖게 되는, 다!른! 함수들이예요.

우리는 함수를 어떻게 구분하나요?? 매개변수의 개수와 타입, 그리고 리턴 타입으로 구분을 하죠.

함수를 가리키는 함수 포인터 또한 매개변수와 리턴타입을 이용해서 구분지을 수 있겠습니다.

요렇게 됩니다

두 개의 매개변수를 갖는 함수를 가리키고 싶다면? int (*pfunc)(int, int); 이렇게 되어야겠죠?

 

int* arr[5]; // -> 대괄호'[]' 배열포인터 
int* func(); // -> 괄호'()' 함수포인터 

배열포인터와 유사하지만 배열포인터는 연산자 대괄호 '[ ]'를 사용했다면 함수 포인터는 괄호 '( )'를 사용했음을 알 수 있어요.

sizeof (함수명)? 함수 저장 사이즈 크기 측정?

함수 이름도 포인터라면, 이 또한 하나의 타입이니까 sizeof로 측정해볼 수 있을까?

ISO규격을 확인해 보면 함수 타입에 sizeof를 사용하는 것을 금지하고 있어요.

 

The sizeof operator shall not be applied to an expression that has function or incomplete type. 

sizeof 연산자는 불완전한 타입이나 함수를 가리키는 표현에 적용되어서는 안된다.

 

비주얼스튜디오 실습 결과!

실제로 컴파일을 돌려보면 'sizeof 피연산자가 잘못되었습니다'라는 에러를 확인하실 수 있습니다.

함수포인터 하나 선언한 다음에 실제 함수 대입해보기 -함수포인터 사용법

int Getfactorial (int) 라는 함수가 있다고 가정해봅시다. 

그리고 새로운 함수포인터 pfunc을 정의한 후에 이 함수를 대입해서 사용해볼거예요.

#include <iostream>
using namespace std;
int GetFactorial(int num) 
{
	int i, fact = 0;
	for (i = 1, fact = 1; i <= num; i++)
		fact *= i;
	return fact;
}
int main()
{
	int (*pfunc)(int); //함수포인터 선언
	pfunc = &GetFactorial; //포인터변수에 GetFactorial 주소값 저장.
	
	// GetFactorial 수행
	cout << "GetFactorial(5) result :" << GetFactorial(5) << endl;
	// pfunc 수행 (pfunc은 주소값이므로 역참조로 함수 수행) 
	cout << "(*pfunc)(5) result :\t" << (*pfunc)(5) << endl; 
    
	cout << "pfunc(5) result :\t" << pfunc(5) << endl;  //동일해

	return 0;
}

실행해봅시다. 

함수를 직접 호출하나, 함수포인터에 함수를 대입해서 호출하나, 값은 동일한 것을 확인할 수 있어요!

이미지로 생각해보면 요렇게 되는거겠죠? 근데 왜 두번째 결과와 세 번째 결과는 동일할까요?

함수 포인터의 역참조 값이 함수 주소 값과 같을 수가 없을 텐데..!

함수 포인터 역참조 Dereferencing of a function pointer!

함수도 메모리에 저장된다면, 당연히 그 메모리주소를 통해서 접근할수도 있겠죠!

함수포인터를 하나 선언해놓고 여러가지 테스트를 진행해봅시다.

#include <stdio.h>
int add(int a, int b)
{
	return a + b;
}
int main()
{
	printf("%p\n", add);
	printf("%p\n", &add);
	printf("%p\n", *add);
	printf("%p\n", **add);
	printf("%p\n", ***add);
	printf("%p\n", ********add);
	return 0;
}

함수명을 출력해보기도 하고, 레퍼런스 붙여서 주소값을 출력해보기도 하고, 역참조를 출력해보기도 하고.. 역참조를 n번해서 출력도 해봤어요. 결과가 어떻게 될 것 같나요??

짠 실행 결과 이 모든 출결과가 하나의 주소를 출력하고 있음을 확인할 수 있습니다.

 

왜 이렇게 됐을까요?

사실 함수명이 포인터 변수라면, 함수를 실행시키려면 함수포인터를 역참조 해야하는게 맞잖아요.

그런데 왜 함수명을 수행하면 함수 내부 로직이 실행될까요?

예를 들어 간단한 printf를 호출할 때 printf 가 포인터 변수라면 함수를 실행하기 위해서는 (*printf)("Hello world") 이렇게 가는게 맞지 않을까요..! 

맞습니다. 실제로 수행해보면 에러나지 않고 잘 수행됩니다. ㅎㅎ 

 

근데 매번 이렇게 하는게 어렵고 비효율적이죠. ㅎㅎ 이유는 단순해요. 그냥 printf("hello")쓰는게 훨씬 쉽기 때문에 굳이 (*printf)("hello") 안써도 자동으로 변환될 수 있도록 편의를 위해 일케 정의해놓은거랍니다.

 

함수 특성상 이런걸 매번 따질 이유가 없기 때문에 직관적으로 쉽게 언어를 사용하기 위해

역참조를 하던, 그대로 출력하던 무한 역참조를 하던,, 같은 주소를 가리키게 해놓은것!

test2(s);
(*test2)(s);
(***********test2)(s);

따라서 위의 결과도 모두 같습니다.

 

This is just a quirk of C. There's no other reason but the C standard just says that dereferencing or taking the address of a function just evaluates to a pointer to that function, and dereferencing a function pointer just evaluates back to the function pointer. This behavior is (thus obviously) very different from how the unary & and * operators works for normal variables.

이건 C언어의 별난 특징인데요, 뭐 특별한 이유가 있는건 아니고 C스탠다드를 보면 함수의 역참조나 함수의 주소 표시를 함수 포인터 값을 가리키게 하도록 나와있어요. 이러한 점은 분명이 &나 *연산자가 일반적으로 변수에 작용하는 방식과는 많이 다르죠.

 

따라서 앞서 예제로 사용했던 GetFactorial 코드에서도 

pfunc = &GetFactorial; //포인터변수에 GetFactorial 주소값 저장.
pfunc = GetFactorial; //결과 동일
pfunc = *GetFactorail; //결과 동일	

요렇게 또한 대체될 수도 있습니다. 물론 주로 간편하고 직관적인 pfunc = GetFactorial 을 많이 쓰겠죠?

// pfunc 수행 (pfunc은 주소값이므로 역참조로 함수 수행) 
cout << "(*pfunc)(5) result :\t" << (*pfunc)(5) << endl; 
cout << "pfunc(5) result :\t" << pfunc(5) << endl; 

그래서 이 두 개 결과가 같았던 거예요. 마치 진짜 함수처럼 호출할 수 있는거죠 :) 

 

이와 같은 특징으로, 실제 호출할 함수 이름을 몰라도, 함수의 주소값을 안다면, 함수포인터를 이용해서 함수를 호출할 수 있답니다. 대표적인 예시로는 qsort 함수의 compare 함수포인터를 생각해볼 수 있죠 ㅎㅎ

 

오늘은 간단하게 함수 포인터에 대해 알아봤어요. 공감이나 댓글은 항상 제게 큰 힘이 됩니다.

다음 포스팅에서 또 봐요~!

반응형