C 언어 | 고급 기능 | 함수의 포인터

함수에 주소가 존재한다. 함수 주소를 포인터에 저장하여 호출할 함수를 변수로 교환 할 수 있다. 이것으로 호출하는 함수를 실행할 때 변화시킬 수 있다.

함수 주소의 저장

변수에 주소가 존재하듯이, 함수에도 주소라는 것이 존재한다. 함수도 결국 기계어이다, 즉 2진수 문자로 표현된 데이터가 저장되는 것이다. 함수를 실행하기 위해서는, 함수 자체의 처리 정보를 포함한 데이터를 메모리에 로드해야 한다. CPU는 기계어를 처리할 때에 메모리에 로드된 기계어를 CPU에 통합해야 한다.

물리적 컴퓨터의 사양에 따라 의존되지만, 일반적으로 CPU는 실행해야 할 기계어의 메모리 주소를 나타내는 레지스터를 보유하고, 하나의 기계어를 실행할 때마다 레지스터의 값을 증가하는 구조되어 있다. 즉, 기계어 레벨에서 명령마다 포인터가 존재하는 것과 같다.

C 언어에서 문장에 주소는 존재하지 않는다. 문장의 위치를 나타내는 것에 라벨을 사용했었다. 문장 단위의 이동을 하려면 이것으로 충분하기 때문이다. 그러나 함수의 경우는 함수명에 의한 호출은 충분하지 않을 수 있다. 함수명을 사용하여 함수를 호출하는 방법은 호출할 함수가 컴파일시에 결정되어야 한다는 것을 나타낸다. 동시에 호출할 함수를 실행할 때에 동적으로 변경할 수 없는 것을 의미한다. 이것에는 프로그램의 가능성을 크게 제한해 버리는 것이다.

예를 들어, 실행시에 호출할 함수를 동적으로 변경시킬 수 있다면, 기능을 바꾸거나 부분적인 갱신이 가능한 유연성 있는 시스템을 구축할 수 있다. 이를 실현하기 위한 기능이 함수의 주소이다.

호출할 함수의 메모리상의 위치를 시작점(entry point)이라고 한다. 시스템이 호출할 프로그램의 최초의 실행 지점을 어플리케이션 엔트리 포인트라고 한다고 하는 것은 “처음하는 C 언어“에서 설명했던 대로이다. 모든 함수에는 주소가 존재한다.

그리고 함수에 주소가 존재한다는 사실은 동시에 함수를 포인터로 처리할 수있는 것을 나타낸다. 포인터는 주소를 저장하는 수단이며, 이 사실은 함수를 변수처럼 취급하는 큰 가능성을 나타낸다. 함수의 주소를 저장하는 함수 포인터는 다음과 같이 선언한다.

함수 포인터 선언

반환형식 (* 식별자) (파라미터형 목록)

이는 대부분이 함수의 선언과 비슷하지만, 식별자 주변을 괄호로 묶는 점이 다르다. 예를 들어 문자 배열에는 인수를 값을 반환하는 함수의 포인터는 int (*pf)(const char*)와 같이 될 것이다. int *pf(const char*)로 작성하는 경우는 int에 대한 포인터를 반환하는 함수로 해석되기 때문에, 이러한 의미는 전혀 다른 것임을 이해하자.

변수의 주소를 얻을 경우는 & 연산자를 사용했지만, 함수의 주소는 함수 이름을 지정한다. 함수의 이름만을 지정했을 경우, 그것은 함수의 주소를 나타내는 것이다. 다음과 같은 함수가 선언되어 있다고 하자.

int Function(int , void *);

이 경우 다음과 같이 포인터에 주소를 대입할 수 있다.

int (*pf)(int , void*) = Function;

이것으로 함수의 포인터 pf에 Function() 함수의 주소를 대입한 것이다. 포인터에서 함수를 호출하는 경우는 일반 함수처럼 포인터 이름과 인수 목록을 지정하거나 * 연산자를 사용하여 간접 참조를 명시적으로 나타내는 두 가지 방법이 있다.

i = pf(iValue , pointer);
i = (* pf)(iValue , pointer);

어느 것을 사용하는가는 개발자의 취향이지만, 일반적으로 일반 함수와 함수 포인터를 구별할 필요가 없기 때문에 함수 포인터도 일반 함수처럼 호출한다.

코드1

#include <stdio.h>

void f(void) {
 printf("Kitty on your lap\n");
}

int main() {
  void (*fp)(void) = f;
 fp();
 (*fp)();
  return 0;
}

코드1에는 인수를 받지 않고 값을 반환하지 않는 간단한 함수를 저장 fp 포인터를 선언하고 이 포인터를 f() 함수의 주소로 초기화한다. 이 포인터는 f() 함수의 엔트리 포인트를 저장하고 있기 때문에, fp 포인터를 역참조하여 함수를 호출할 수 있다.

이러한 포인터에 의한 함수 호출은 실전에 어떻게 도움이 될 수 있을까? 하나는 앞서 설명한 바와 같이 실행할 때까지 호출할 함수가 결정되지 않은 경우에 유효하다. 예를 들어, 시스템에 함수의 주소를 전달하여 필요에 따라 시스템에서 특정 함수를 호출하여 받을 수 있다. 이러한 콜백 기능은 많은 시스템에서 채택되고 있다. Microsoft Windows와 같은 그래픽 이벤트 구동에서는 사용자가 버튼을 누르는 액션을 통해 작업을 수행하기 위해, 이벤트가 발생할 때까지 프로그램을 대기시킬 필요가 있다. 그래서 이벤트가 발생했을 때 호출되기 바라는 함수의 포인터를 미리 시스템에 등록해 두는 방법이 이용되고 있다.

또는 개발자가 자유롭게 기능을 확장할 수 있는 시스템을 구축할 때에도 사용된다. 함수의 포인터 배열을 제공하고, 필요에 따라서 이를 개발자가 함수의 포인터를 등록한다. 시스템은 처리을 할 때에 배열로부터 순서대로 함수를 호출한다. 이 함수의 연계에 의해 함수의 호출을 자유롭게 연쇄시킬 수 있기 때문에, 시스템은 확장성이 높고 유연하게 된다. 이것은 객체 지향의 상속과 오버라이드의 개념에 연결된다.

코드2

#include <stdio.h>
#define ADD_FUNC 2
#define REMOVE_FUNC 4
#define RESULT_OK 0
#define RESULT_ERROR 1

typedef void(* REGISTERPROC)(const char* , int);

int RegisterFunc(REGISTERPROC func , int mode) {
  static int iLen = 0;
  static REGISTERPROC procs[256];
 int iCount;
 char removed;

 switch(mode) {
  case ADD_FUNC:  /*함수의 추가*/
    if (iLen == 256) return RESULT_ERROR;

   /*같은 함수는 추가할 수 없다*/
   for(iCount = 0 ; iCount < iLen ; iCount++)
      if (procs[iCount] == func) return RESULT_ERROR;

   procs[iLen] = func;
   iLen++;

   for(iCount = 0 ; iCount < iLen ; iCount++)
      (*procs[iCount])("Add" , iCount);
   break;
  case REMOVE_FUNC: /*함수의 삭제*/
    removed = 0;
    for(iCount = 0 ; iCount < iLen ; iCount++) {
      if (procs[iCount] == func) {
        for(;iCount < iLen - 1 ; iCount++)
          procs[iCount] = procs[iCount + 1];
        iLen--;

       for(iCount = 0 ; iCount < iLen ; iCount++)
          (*procs[iCount])("Remove" , iCount);

        removed = 1;
        break;
      }
   }
   if (!removed) return RESULT_ERROR;
    break;
  default:
    return RESULT_ERROR;
  }

 return RESULT_OK;
}

void Function1(const char * msg , int iValue) {
 printf("Function1 : msg = %s , iValue = %d\n" , msg , iValue);
}
void Function2(const char * msg , int iValue) {
 printf("Function2 : msg = %s , iValue = %d\n" , msg , iValue);
}
void Function3(const char * msg , int iValue) {
 printf("Function3 : msg = %s , iValue = %d\n" , msg , iValue);
}

int main() {
  printf("Add Function1\n");
 RegisterFunc(Function1 , ADD_FUNC);

 printf("\nAdd Function2\n");
  RegisterFunc(Function2 , ADD_FUNC);

 printf("\nAdd Function3\n");
  RegisterFunc(Function3 , ADD_FUNC);

 printf("\nRemove Function2\n");
 RegisterFunc(Function2 , REMOVE_FUNC);

  printf("\nRemove Function3\n");
 RegisterFunc(Function3 , REMOVE_FUNC);

  return 0;
}

코드2는 함수의 포인터가 실전에서 어떤 효과를 만들어 주는지를 알기위한 시험적인 콜백 설정 함수 RegisterFunc() 함수를 작성하고 있다. RegisterFunc() 함수는 첫번째 인수에 REGISTERPROC 형의 함수의 포인터를 두번째 파라미터에 추가하거나 삭제할지 여부를 나타내는 값을 지정한다. 함수가 추가 또는 삭제에 성공하면 0 이외를, 그렇지 않으면 0을 반환한다.

먼저, 이 프로그램에서 흥미로운 것은 함수의 형태에 별명을 붙이고 있다. 함수의 포인터 타입에는 typedef 기억 클래스 지정자를 사용하여 이름을 지정할 수 있다. 이 경우는 함수명에 해당하는 부분이 포인터 형의 별명이 된다. REGISTERPFOC 형은 첫번째 인는 문자열에 대한 포인터를 두번째 인수에 숫자를 받는다. 이 함수는 발생한 이벤트를 나타내는 문자열과 등록된 함수 체인의 인덱스를 받는다.

RegisterFunc() 함수는 최대로 256의 함수를 등록할 수 있다. 함수를 등록할 경우 두번째 인수에 ADD_FUNC를 지정하고, 해제하는 경우는 REMOVE_FUNC을 지정한다. 함수가 등록되면 RegisterFunc() 함수는 등록하는 함수 및 이미 등록된 함수를 호출하고 이벤트로 통지한다. 해제하려면 해제된 함수를 제외하고 등록된 함수에 이벤트를 통지한다.

실행 결과를 보면 추가 또는 해제 할 때마다, RegisterFunc() 함수는 등록되어 있는 함수를 호출하여 추가 및 제거가 있었다는 것을 전하고 있는 것을 확인할 수 있다. 이와 같이 시스템에 등록하고 기능을 확장하는 같은 방법을 사용하는 경우, 등록하는 함수를 사용자 정의 콜백 함수라고 부른다. 설계 분야가 되므로 자세한 내용은 피하고 있지만, 고도의 시스템은 개발자가 시스템의 확장을 간단히 실현할 수 있도록, 이러한 동적 확장 수단을 제공해야 한다. 그러면 개발자는 시스템의 실체를 알 필요 없이, 원하는 기능만을 확장할 수 있기 때문이다.




최종 수정 : 2017-11-26