반응형
 

1. 함수 포인터란?

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

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

 

함수 포인터 역시 포인터 변수이다. 일반 포인터 변수와 다른 점은 일반 포인터 변수가 변수의 주소 값을 저장하는 반면에 함수 포인터는 함수의 주소 값을 저정한다. 함수는 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한 함수(혹은 알고리즘)의 작성을 가능하게 한다.

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

- 함수 이름 자체는 배열의 이름처럼 한번 정해지면 바꿀 수 없는 포인터 상수이다. 그러나 함수 포인터는 변경이 가능하며 필요할 때마다 다른 함수를 지시하도록 설정할 수 있다.

 

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

 

#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;

}


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

 

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 EA) : ");

        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에서 이중 인터럽트 벡터 지정 시에 유용하게 사용될 수 있다.

+ Recent posts