IT/C and C++

함수 포인터란?

KSI 2005. 7. 3. 01:40

1. 함수 포인터란?

- 프로그램에서함수의 이름은 메모리에 로드된 그 함수의 실행코드 영역의 시작주소를

   의한다.

- 함수에 대한 포인터는 바로 함수의 시작주소값을 갖는 포인터이다.

 

함수 포인터라는 것 역시 32비트 체제하에서 4바이트의 메모리를 갖는 포인터 변수 입니다. 일반 포인터 변수와 다른 점은 일반 포인터 변수가 변수들의 주소값을 저장하는 반면에 함수 포인터는 함수의 주소값을 갖는다는 것입니다. 함수는 code 부분입니다. 즉 프로그래머가 짠 코드가 컴파일 되어서 기계 코드로 변화된 것, 그것이 바로 code 입니다. 프로그램이 실행되기 위해서는 이 code가 메모리에 올라가 있어야 하는 것입니다. 여기서 어떤 함수의 호출은 이 code중에서 그 함수 부분으로 jump(이동) 하는 것이지요. 바로 이 함수 부분이라는 것이 그 함수의 주소값이 되는 것이고 이 함수의 주소값을 저장하는 포인터가 함수 포인터입니다.

C 언어의 경우 함수자체를 변수로 만들수는 없습니다. 대신 함수를 포인터하는 것은 가능한데, 이것을 통해서 함수를 포인터 처럼 사용할수 있으며, 이 포인터가 가리키고 있는 곳의 함수를 실행시킬수도 있습니다.

 

예) main 함수와 printf 함수의 시작 주소값을 출력합니다.

#include <stdio.h>

main()

{

    printf("address of main : %u \n", &main);

    printf("address of printf : %u \n", &printf);

}

 

2. 함수 포인터의 용도

- 함수에 접근하기 위해 사용된다.

- 함수에 함수 자체를 실인수로 전달하기 위해 사용된다.

- 함수의 처리결과가 함수일 때 그 함수에 대한 포인터를 돌려주기 위해 사용된다.

 

함수 포인터에 대한 연산은 허용되지 않습니다.

 

3. 함수 포인터의 선언

- 다른 포인터 변수와 마찬가지로 함수 포인터도 먼저 선언하고 사용해야 한다.

- 함수 포인터의 선언은 일반적으로 다음의 형식을 사용한다.

자료형 (*함수포인터명)(인자목록);

  이 형식은 명시된 자료형을 돌려주고, 인자목록에 포함된 인자를 받는 함수에 대한 

  포인터를 선언한다.

- 함수 포인터 선언의 구체적인 예

        ⓐ int (*f1)(int a);

        ⓑ char (*f2)(char *p[]);

        ⓒ void (*f3)();

  ⓐ 하나의 int형 인자를 받아들이고, int형 자료를 돌려주는 함수에 대한 포인터 f1을

      선언한다.

  ⓑ char형에 대한 포인터 배열을 인자로 받아, char형의 값을 돌려주는 함수에 대한

      포인터 f2를 선언한다.

  ⓒ아무런 인자도 받지 않고 결과값도 돌려주지 않는(void) 함수에 대한 포인터 f3를

      선언한다.

- 함수 포인터 선언과 포인터를 돌려주는 함수 선언의 차이

        ⓐ int (*f1)(int a);

        ⓑ int *f2(int a);

  ⓐ 함수 포인터 : 한 개의 int형 인자를 받아, int형 값을 결과로 돌려주는 함수에 대한

      포인터 f1을 선언한다.

  ⓑ 포인터를 돌려주는 함수의 선언 : 한 개의 int형 인자를 받아, int형 포인터 값을

      결과로 돌려주는 함수를 선언한다.

함수에 대한 포인터의 선언은 반드시 포인터의 이름과 간접연산자(*) 주위에 ( )를 사용해야 합니다.

 

4. 함수 포인터의 초기화

- 함수 포인터를 선언하고 나면, 이 포인터가 어떤 함수를 지시하도록 초기화해야 한다.

- 함수 포인터를 초기화할 때, 인자목록과 return 자료형이 일치해야 한다.

- 함수 이름은 이름 자체로 주소를 의미한다. 따라서 함수포인터에 함수의 주소값을

  초기화하려면 다음과 같이 한다.

  예)

  int add(int a, int b); => 함수의 prototype

  int (*f1)(int x, int y); => 함수 포인터 선언

  int add(int a, int b) { return a + b; }  => 실제 함수 정의 부분 
 
  f1 = add; => 적합

  f1 = &add; => 적합

  f1 = add(); => 오류(f1은 포인터, add()의 결과는 int)

  f1 = &add(); => 오류(&부적당)
 

 

5. 함수 포인터의 활용

- generic한 함수(혹은 알고리즘)의 작성을 가능하게 한다

- 잘 사용하면 유지/보수를 수월하게 한다.

- 함수의 이름 자체는 배열의 이름처럼 한번 정해지면 바꿀 수 없는 포인터 상수이다.

  그러나 함수 포인터는 변경이 가능하며 필요할 때마다 다른 함수를 지시하도록 설정할

  수 있다.

  예)

  #include <stdio.h>

  

  int add(int a, int b); /* 함수의 prototype */

  int sub(int a, int b);

  int mul(int a, int b);

  int div(int a, int b);

 

  main()

  {

      int (*fun)(int x, int y); /* 함수 포인터 선언 */

      int a, b;

      char c;

 

      printf("Input (num op num) : ");

      scanf("%d %c %d", &a, &c, &b);

 

      switch (c)

      {

            case '+' :

                fun = add;

                break;

            case '-' :

                fun = sub;

                break;

            case '*' :

                 fun = mul;

                 break;

            case '/' :

                 fun = div;

                 break;

      }

       printf("%d %c %d = %d\n", a, c, b, fun(a,b));

  }

 

  int add(int a, int b)

  {

      return a+b;

  }

 

  int sub(int a, int b)

  {

      return a-b;

  }

 

  int mul(int a, int b)

  {

      return a*b;

  }

 

  int div(int a, int b)

  {

      return a/b;

  }
 

  입력에 따라 함수포인터 fun에 지정되는 함수가 결정된다.


- 함수 포인터의 배열: 여러개의 함수포인터를 배열에 저장하여 사용할 수 있다.

  int (*fun[3])(int, int);

  int 형 자료 두 개를 입력으로 받아, int형 결과를 돌려주는 함수포인터 3개를 저장할 수

  있는 배열이다.

  예) 두 정수 a,b를 읽어 합,차,곱을 구하는 예제로 함수포인터의 배열을 사용한다.

  #include <stdio.h>


  int add(int a, int b); /* 함수의 prototype */

  int sub(int a, int b);

  int mul(int a, int b);

 

  main()

  {

      char op[] = {'+', '-', '*'};

      int (*fun[])(int x, int y) = {add, sub, mul};

      int a, b;

 

      printf("Input number(2) : ");

      scanf("%d %d", &a, &b);

 

      for (i = 0; i < 3; i++)

      printf("%d %c %d = %d\n", a, op[i], b, fun[i](a,b));

  }

 

  int add(int a, int b)

  {

      return a+b;

  }

 

  int sub(int a, int b)

  {

      return a-b;

  }

 

  int mul(int a, int b)

  {

      return a*b;

  }
 
- 함수 포인터를 인자로 전달하기: 함수명을 인자로 전달하거나 함수 포인터 자체를

  함수의 인자로 보내고 받을 수 있다. 함수명을 실인수로 사용할 경우, 호출당한

  함수의 가인수는 함수 포인터가 된다.

  예)
  #include <stdio.h>


  int print_add(int a, int b)

  {

      printf("%d + %d = %d\n", (a, b, a+b);

  }

  int add(void (*fp)(int, int), int x, int y)

  {

      fp(x, y);

  }

 

  main()

  {

      add(print_add, 10, 5);

  }
 
- 함수의 주소 값 전달

  add(think);     => think(); 함수의 시작 번지 값이 add() 함수의 매개변수이다.

  add(think());  => think(); 함수의 리턴 값이 add() 함수의 매개변수이다.

- 함수 포인터를 이용해 특정 번지로 점프하기

  예1)

  #include <stdio.h>

 

  void main(void)

  {

      unsigned int goaddr = 0x8120;  /* 8120H 번지임을 나타낸다. */

      void (*gofunc)(void);              /* 함수 포인터 선언 */

 

      gofunc = (void(*)()) goaddr;    /* 초기화 */

      (*gofunc)();                          /* 함수포인터 실행 */

  }

 

  위 예에서는 void (*gofunc)(void); 로 선언된 함수 포인터가 실제적으로 가리켜야 할

  목적지 함수가 따로 없는 것처럼 보인다. 그러나 잘 보면 목적 함수는 다음과 같음을

  알 수 있다.

  void (*goaddr)();     : 목적함수

  그리고 위 목적함수는 하나의 형(type)으로써 뒤의 goaddr을 cast한다.

  이제 (*gofunc)(); 로 실행되면 컴퓨터의 PC(Program Counter) 또는

  IP(Instruction Pointer)는   (*gofunc)(); 함수로부터 void (*goaddr)(); 함수로 넘어간다.

  그리고 void (*goaddr)(); 함수의 시작번지는 0x8120 번지가 되는 것이다. 따라서

  컴퓨터의 PC는 0x8120번지로 점프하게 되는 결과를 낳는다.

 

  예2)
  #include <stdio.h>


  void main(void)

  {

      void (*gofunc)(void);          /* 함수 포인터 선언 */

      gofunc = (void (*)()) 0x8120;   /* 시작주소 초기화 */

     

      (*gofunc)();                   /* 함수포인터 실행 */

  }


  위 프로그램은 번거롭게 goaddr이라는 변수를 생략하고 직접 (*gofunc)();의 시작주소를

  지정하였다. 이로서 확실히 알 수 있는 것은 포인터 함수의 시작주소의 캐스팅 형이

  (void (*)())이 된다는 것이다.

 

  예3)단 한줄로 나타내보자.

  #include <stdio.h>


  void main(void)

  {

      (*((void (*)()) 0x8120))();

  }


  이 한줄은 바로 0x8120번지로 점프하라는 명령어와 같다!

  그러나 TurboC++ 3.0 에서는 위 명령이 유효하나 IC96에서는 무효하다.

  (다음과 같은 에러 메시지가 뜬다.)

  iC-96 FATAL ERROR --

  internal error: invalid directionary access, case 3

  COMPILATION TERMINATED


  그러나 방법은 있다. 아래와 같이 하면 IC96에서 에러 없이 멋지게 만들어낼 수 있다.

 

  #include <80c196.h>         /* 표준 인클루드 파일 */

  void main()

  {

       (*(void (*)(void))(*(void (**)(void))0x8120))();

  }

 

  실제로 위와 같은 선언은 특히 Embedded System Programming에서 많이 사용되는

  방법이다. 특히 80C196에서 이중 인터럽트 벡터 지정 시에 유용하게 사용될 수 있다.

 

출처 : 네이버 카페 임베디드시스템(Device Control)